initial commit: standalone SoftEther VPN client in Go
Built-in DHCP (raw Ethernet frames through tunnel), automatic reconnection, host route management, classless static routes (option 121/249), DNS config. Single static binary, Linux only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
829ca73b1b
340 changed files with 199140 additions and 0 deletions
236
pkg/client/client.go
Normal file
236
pkg/client/client.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
// Package client implements the SoftEther VPN client handshake and session management.
|
||||
//
|
||||
// The connection flow follows the SoftEther protocol:
|
||||
// 1. TLS connect (standard TLS, looks like HTTPS)
|
||||
// 2. Upload signature ("VPNCONNECT" string via HTTP POST)
|
||||
// 3. Download Hello (server sends version info + random challenge)
|
||||
// 4. Upload Auth (client sends credentials + node info via HTTP POST)
|
||||
// 5. Download Welcome (server sends session info if auth succeeds)
|
||||
// 6. Enter tunneling mode (raw TCP block framing, no more HTTP)
|
||||
//
|
||||
// Reference: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7145 (ClientConnectToServer flow)
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"git.sagar.ch/sagar/softether-go/pkg/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
clientStr = "SoftEther VPN Client"
|
||||
clientVer = 502
|
||||
clientBuild = 5187
|
||||
|
||||
// Auth types matching SoftEther's CLIENT_AUTHTYPE_* constants.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Cedar.h#L554
|
||||
authTypePassword uint32 = 1
|
||||
authTypePlainPassword uint32 = 2
|
||||
)
|
||||
|
||||
// Config holds the connection parameters for a SoftEther VPN session.
|
||||
type Config struct {
|
||||
Host string // Server hostname or IP
|
||||
Port int // Server port (default: 443)
|
||||
Hub string // Virtual hub name
|
||||
Username string // Authentication username
|
||||
Password string // Password (hashed or sent plain depending on PlainPassword)
|
||||
PlainPassword bool // If true, send password as plaintext (AuthType 2)
|
||||
InsecureSkipVerify bool // Skip TLS certificate verification
|
||||
}
|
||||
|
||||
// Session represents an established SoftEther VPN session.
|
||||
// After Connect() returns, the underlying Conn is ready for TCP block framing.
|
||||
type Session struct {
|
||||
Conn *protocol.Conn
|
||||
SessionKey [sha1Size]byte // 20-byte session key from server
|
||||
SessionKey32 uint32 // 32-bit session key
|
||||
SessionName string // Server-assigned session name (e.g. "SID-ADMIN-1")
|
||||
ConnectionName string // Server-assigned connection name
|
||||
}
|
||||
|
||||
// Connect performs the full SoftEther handshake and returns an established session.
|
||||
// The returned Session's Conn is ready for TCP block framing (tunnel mode).
|
||||
func Connect(cfg Config) (*Session, error) {
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 443
|
||||
}
|
||||
|
||||
// Step 1: TLS connection
|
||||
conn, err := protocol.DialTLS(cfg.Host, cfg.Port, cfg.InsecureSkipVerify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Upload signature
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7208
|
||||
if err := protocol.UploadSignature(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("signature: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Download Hello — server sends version info and a random challenge
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7250 (ClientDownloadHello)
|
||||
helloPack, err := protocol.RecvPack(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("hello recv: %w", err)
|
||||
}
|
||||
if e := helloPack.GetError(); e != 0 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("hello error: %d", e)
|
||||
}
|
||||
|
||||
serverRandom := helloPack.GetData("random")
|
||||
if len(serverRandom) != sha1Size {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("invalid server random size: %d", len(serverRandom))
|
||||
}
|
||||
var random [sha1Size]byte
|
||||
copy(random[:], serverRandom)
|
||||
|
||||
// Step 4: Upload Auth — send credentials and client metadata
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7289 (ClientUploadAuth)
|
||||
authPack := buildAuthPack(cfg, random)
|
||||
|
||||
if err := protocol.SendPack(conn, authPack); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("auth send: %w", err)
|
||||
}
|
||||
|
||||
// Step 5: Download Welcome — server sends session info if auth succeeded
|
||||
welcomePack, err := protocol.RecvPack(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("welcome recv: %w", err)
|
||||
}
|
||||
|
||||
if e := welcomePack.GetError(); e != 0 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("auth failed, error code: %d", e)
|
||||
}
|
||||
|
||||
sess := &Session{
|
||||
Conn: conn,
|
||||
SessionName: welcomePack.GetStr("session_name"),
|
||||
ConnectionName: welcomePack.GetStr("connection_name"),
|
||||
}
|
||||
|
||||
sessionKeyData := welcomePack.GetData("session_key")
|
||||
if len(sessionKeyData) == sha1Size {
|
||||
copy(sess.SessionKey[:], sessionKeyData)
|
||||
}
|
||||
sess.SessionKey32 = welcomePack.GetInt("session_key_32")
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
// buildAuthPack constructs the authentication Pack sent to the server.
|
||||
// This includes credentials, client version, connection options, node info, and OS info.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7289
|
||||
func buildAuthPack(cfg Config, random [sha1Size]byte) *protocol.Pack {
|
||||
p := &protocol.Pack{}
|
||||
|
||||
// Login method and credentials
|
||||
p.AddStr("method", "login")
|
||||
p.AddStr("hubname", cfg.Hub)
|
||||
p.AddStr("username", cfg.Username)
|
||||
|
||||
if cfg.PlainPassword {
|
||||
// Plain password auth (AuthType 2): password sent as-is over TLS
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L5548
|
||||
p.AddInt("authtype", authTypePlainPassword)
|
||||
p.AddStr("plain_password", cfg.Password)
|
||||
} else {
|
||||
// Hashed password auth (AuthType 1): SecurePassword = SHA0(SHA0(password) + server_random)
|
||||
hashedPw := HashPassword(cfg.Password)
|
||||
securePw := SecurePassword(hashedPw, random)
|
||||
p.AddInt("authtype", authTypePassword)
|
||||
p.AddData("secure_password", securePw[:])
|
||||
}
|
||||
|
||||
// Client version info
|
||||
p.AddStr("client_str", clientStr)
|
||||
p.AddInt("client_ver", clientVer)
|
||||
p.AddInt("client_build", clientBuild)
|
||||
|
||||
// Protocol: 0 = TCP (CONNECTION_TCP), 1 = UDP
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Cedar.h#L522
|
||||
p.AddInt("protocol", 0)
|
||||
|
||||
// Hello exchange fields
|
||||
p.AddStr("hello", clientStr)
|
||||
p.AddInt("version", clientVer)
|
||||
p.AddInt("build", clientBuild)
|
||||
p.AddInt("client_id", 0)
|
||||
|
||||
// Connection options
|
||||
p.AddInt("max_connection", 1)
|
||||
p.AddBool("use_encrypt", true)
|
||||
p.AddBool("use_compress", false)
|
||||
p.AddBool("half_connection", false)
|
||||
p.AddBool("require_bridge_routing_mode", false)
|
||||
p.AddBool("require_monitor_mode", false)
|
||||
p.AddBool("qos", true)
|
||||
p.AddBool("support_bulk_on_rudp", true)
|
||||
p.AddBool("support_hmac_on_bulk_of_rudp", true)
|
||||
p.AddBool("support_udp_recovery", true)
|
||||
p.AddInt("rudp_bulk_max_version", 2)
|
||||
|
||||
// Machine unique ID (random for this session)
|
||||
var uniqueID [sha1Size]byte
|
||||
randBytes := make([]byte, 64)
|
||||
rand.Read(randBytes)
|
||||
uniqueID = sha0(randBytes)
|
||||
p.AddData("unique_id", uniqueID[:])
|
||||
|
||||
// Brand string for connection limit
|
||||
p.AddStr("branded_ctos", "Branded_VPN")
|
||||
|
||||
// Node info — describes the client machine to the server.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L8034 (OutRpcNodeInfo)
|
||||
hostname, _ := os.Hostname()
|
||||
p.AddStr("ClientProductName", clientStr)
|
||||
p.AddStr("ServerProductName", "")
|
||||
p.AddStr("ClientOsName", runtime.GOOS)
|
||||
p.AddStr("ClientOsVer", runtime.GOARCH)
|
||||
p.AddStr("ClientOsProductId", "")
|
||||
p.AddStr("ClientHostname", hostname)
|
||||
p.AddStr("ServerHostname", "")
|
||||
p.AddStr("ProxyHostname", "")
|
||||
// Note: HubName is NOT added here because "hubname" (lowercase) is already
|
||||
// present from the auth fields. SoftEther's AddElement rejects duplicate names
|
||||
// (case-insensitive), and PackRead returns NULL on AddElement failure.
|
||||
// p.AddStr("HubName", cfg.Hub)
|
||||
p.AddData("UniqueId", uniqueID[:16])
|
||||
p.AddInt("ClientProductVer", clientVer)
|
||||
p.AddInt("ClientProductBuild", clientBuild)
|
||||
p.AddInt("ServerProductVer", 0)
|
||||
p.AddInt("ServerProductBuild", 0)
|
||||
p.AddIP4("ClientIpAddress", 0)
|
||||
p.AddData("ClientIpAddress6", make([]byte, 16))
|
||||
p.AddInt("ClientPort", 0)
|
||||
p.AddIP4("ServerIpAddress", 0)
|
||||
p.AddData("ServerIpAddress6", make([]byte, 16))
|
||||
p.AddInt("ServerPort2", 0)
|
||||
p.AddIP4("ProxyIpAddress", 0)
|
||||
p.AddData("ProxyIpAddress6", make([]byte, 16))
|
||||
p.AddInt("ProxyPort", 0)
|
||||
|
||||
// OS version info (non-Windows)
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L8079 (OutRpcWinVer)
|
||||
p.AddBool("V_IsWindows", false)
|
||||
p.AddBool("V_IsNT", false)
|
||||
p.AddBool("V_IsServer", false)
|
||||
p.AddBool("V_IsBeta", false)
|
||||
p.AddInt("V_VerMajor", 0)
|
||||
p.AddInt("V_VerMinor", 0)
|
||||
p.AddInt("V_Build", 0)
|
||||
p.AddInt("V_ServicePack", 0)
|
||||
p.AddStr("V_Title", "Linux")
|
||||
|
||||
return p
|
||||
}
|
||||
137
pkg/client/crypto.go
Normal file
137
pkg/client/crypto.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// SHA-0 implementation for SoftEther VPN password authentication.
|
||||
//
|
||||
// SoftEther uses SHA-0 (the original, withdrawn FIPS-180 hash — NOT SHA-1) for
|
||||
// password hashing and the SecurePassword challenge-response. SHA-0 differs from
|
||||
// SHA-1 only in that the message schedule expansion does NOT include a left-rotate
|
||||
// by 1 bit (see transform() below).
|
||||
//
|
||||
// Auth flow:
|
||||
// 1. Client hashes the plaintext password: HashedPassword = SHA0(password)
|
||||
// 2. Server sends a 20-byte random challenge in the Hello pack
|
||||
// 3. Client computes: SecurePassword = SHA0(HashedPassword + ServerRandom)
|
||||
// 4. Client sends SecurePassword in the auth pack
|
||||
//
|
||||
// Reference C implementation:
|
||||
// https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Encrypt.c#L1088 (Sha0)
|
||||
// https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Sam.c#L105 (SecurePassword)
|
||||
package client
|
||||
|
||||
const sha1Size = 20
|
||||
|
||||
func rol(bits int, value uint32) uint32 {
|
||||
return (value << bits) | (value >> (32 - bits))
|
||||
}
|
||||
|
||||
type sha0ctx struct {
|
||||
count uint64
|
||||
buf [64]byte
|
||||
state [8]uint32
|
||||
}
|
||||
|
||||
func (c *sha0ctx) init() {
|
||||
c.state[0] = 0x67452301
|
||||
c.state[1] = 0xEFCDAB89
|
||||
c.state[2] = 0x98BADCFE
|
||||
c.state[3] = 0x10325476
|
||||
c.state[4] = 0xC3D2E1F0
|
||||
c.count = 0
|
||||
}
|
||||
|
||||
func (c *sha0ctx) transform() {
|
||||
var W [80]uint32
|
||||
|
||||
p := 0
|
||||
t := 0
|
||||
for ; t < 16; t++ {
|
||||
W[t] = uint32(c.buf[p])<<24 | uint32(c.buf[p+1])<<16 | uint32(c.buf[p+2])<<8 | uint32(c.buf[p+3])
|
||||
p += 4
|
||||
}
|
||||
// SHA-0: W[t] = W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16] (NO rotate)
|
||||
// SHA-1 would be: W[t] = rol(1, W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16])
|
||||
for ; t < 80; t++ {
|
||||
W[t] = W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]
|
||||
}
|
||||
|
||||
A, B, C, D, E := c.state[0], c.state[1], c.state[2], c.state[3], c.state[4]
|
||||
for t = 0; t < 80; t++ {
|
||||
tmp := rol(5, A) + E + W[t]
|
||||
if t < 20 {
|
||||
tmp += (D ^ (B & (C ^ D))) + 0x5A827999
|
||||
} else if t < 40 {
|
||||
tmp += (B ^ C ^ D) + 0x6ED9EBA1
|
||||
} else if t < 60 {
|
||||
tmp += ((B & C) | (D & (B | C))) + 0x8F1BBCDC
|
||||
} else {
|
||||
tmp += (B ^ C ^ D) + 0xCA62C1D6
|
||||
}
|
||||
E = D
|
||||
D = C
|
||||
C = rol(30, B)
|
||||
B = A
|
||||
A = tmp
|
||||
}
|
||||
|
||||
c.state[0] += A
|
||||
c.state[1] += B
|
||||
c.state[2] += C
|
||||
c.state[3] += D
|
||||
c.state[4] += E
|
||||
}
|
||||
|
||||
func (c *sha0ctx) update(data []byte) {
|
||||
i := int(c.count & 63)
|
||||
c.count += uint64(len(data))
|
||||
for _, d := range data {
|
||||
c.buf[i] = d
|
||||
i++
|
||||
if i == 64 {
|
||||
c.transform()
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sha0ctx) final() {
|
||||
cnt := c.count * 8
|
||||
c.update([]byte{0x80})
|
||||
for c.count&63 != 56 {
|
||||
c.update([]byte{0x0})
|
||||
}
|
||||
for i := 0; i < 8; i++ {
|
||||
c.update([]byte{byte(cnt >> ((7 - i) * 8))})
|
||||
}
|
||||
p := 0
|
||||
for i := 0; i < 5; i++ {
|
||||
c.buf[p] = byte(c.state[i] >> 24)
|
||||
c.buf[p+1] = byte(c.state[i] >> 16)
|
||||
c.buf[p+2] = byte(c.state[i] >> 8)
|
||||
c.buf[p+3] = byte(c.state[i])
|
||||
p += 4
|
||||
}
|
||||
}
|
||||
|
||||
func sha0(data []byte) [sha1Size]byte {
|
||||
var c sha0ctx
|
||||
c.init()
|
||||
c.update(data)
|
||||
c.final()
|
||||
var out [sha1Size]byte
|
||||
copy(out[:], c.buf[:sha1Size])
|
||||
return out
|
||||
}
|
||||
|
||||
// SecurePassword computes the challenge-response: SHA0(HashedPassword + ServerRandom).
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Sam.c#L105
|
||||
func SecurePassword(hashedPassword, random [sha1Size]byte) [sha1Size]byte {
|
||||
buf := make([]byte, sha1Size*2)
|
||||
copy(buf, hashedPassword[:])
|
||||
copy(buf[sha1Size:], random[:])
|
||||
return sha0(buf)
|
||||
}
|
||||
|
||||
// HashPassword computes SHA0(password) — the first step of SoftEther password auth.
|
||||
// The result is what SoftEther stores server-side as "HashedPassword".
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Sam.c#L91 (HashPassword)
|
||||
func HashPassword(password string) [sha1Size]byte {
|
||||
return sha0([]byte(password))
|
||||
}
|
||||
129
pkg/client/tunnel.go
Normal file
129
pkg/client/tunnel.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCP block framing constants.
|
||||
// After the HTTP handshake completes, SoftEther switches to raw TCP framing:
|
||||
//
|
||||
// Send: uint32(numBlocks) + [uint32(blockSize) + blockData]...
|
||||
// Recv: same format
|
||||
// Keepalive: uint32(0xFFFFFFFF) + uint32(randSize) + randData
|
||||
//
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1654 (TcpSockRecv)
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1761 (TcpSockSend)
|
||||
const (
|
||||
keepAliveMagic uint32 = 0xFFFFFFFF // Magic value indicating a keepalive packet
|
||||
maxKeepaliveSize uint32 = 512 // Max random data in keepalive
|
||||
keepAliveInterval = 3 * time.Second
|
||||
)
|
||||
|
||||
// Tunnel handles bidirectional TCP block framing for Ethernet frames over a
|
||||
// SoftEther VPN session. Each "block" is one Ethernet frame.
|
||||
type Tunnel struct {
|
||||
sess *Session
|
||||
stopCh chan struct{}
|
||||
stopped sync.Once
|
||||
}
|
||||
|
||||
// NewTunnel creates a tunnel from an established session.
|
||||
// Call StartKeepalive() before reading/writing frames.
|
||||
func NewTunnel(sess *Session) *Tunnel {
|
||||
return &Tunnel{
|
||||
sess: sess,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the keepalive goroutine and closes the underlying connection.
|
||||
func (t *Tunnel) Close() error {
|
||||
t.stopped.Do(func() { close(t.stopCh) })
|
||||
return t.sess.Conn.Close()
|
||||
}
|
||||
|
||||
// ReadFrames reads a batch of Ethernet frames from the server.
|
||||
// Returns nil (no error) for keepalive packets. Blocks until data arrives.
|
||||
func (t *Tunnel) ReadFrames() ([][]byte, error) {
|
||||
var numBlocks uint32
|
||||
if err := binary.Read(t.sess.Conn, binary.BigEndian, &numBlocks); err != nil {
|
||||
return nil, fmt.Errorf("read num blocks: %w", err)
|
||||
}
|
||||
|
||||
// Keepalive: server sends 0xFFFFFFFF + uint32(size) + random data
|
||||
if numBlocks == keepAliveMagic {
|
||||
var size uint32
|
||||
if err := binary.Read(t.sess.Conn, binary.BigEndian, &size); err != nil {
|
||||
return nil, fmt.Errorf("read keepalive size: %w", err)
|
||||
}
|
||||
if _, err := io.CopyN(io.Discard, t.sess.Conn, int64(size)); err != nil {
|
||||
return nil, fmt.Errorf("discard keepalive: %w", err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
frames := make([][]byte, 0, numBlocks)
|
||||
for i := uint32(0); i < numBlocks; i++ {
|
||||
var size uint32
|
||||
if err := binary.Read(t.sess.Conn, binary.BigEndian, &size); err != nil {
|
||||
return nil, fmt.Errorf("read block size: %w", err)
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
if _, err := io.ReadFull(t.sess.Conn, buf); err != nil {
|
||||
return nil, fmt.Errorf("read block data: %w", err)
|
||||
}
|
||||
frames = append(frames, buf)
|
||||
}
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
// WriteFrames sends a batch of Ethernet frames to the server.
|
||||
func (t *Tunnel) WriteFrames(frames [][]byte) error {
|
||||
if err := binary.Write(t.sess.Conn, binary.BigEndian, uint32(len(frames))); err != nil {
|
||||
return fmt.Errorf("write num blocks: %w", err)
|
||||
}
|
||||
for _, frame := range frames {
|
||||
if err := binary.Write(t.sess.Conn, binary.BigEndian, uint32(len(frame))); err != nil {
|
||||
return fmt.Errorf("write block size: %w", err)
|
||||
}
|
||||
if _, err := t.sess.Conn.Write(frame); err != nil {
|
||||
return fmt.Errorf("write block data: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartKeepalive sends periodic keepalive packets to prevent the server from
|
||||
// timing out the connection. Must be called after the session enters tunnel mode.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1779
|
||||
func (t *Tunnel) StartKeepalive() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(keepAliveInterval)
|
||||
defer ticker.Stop()
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
size := uint32(rng.Intn(int(maxKeepaliveSize))) + 1
|
||||
if err := binary.Write(t.sess.Conn, binary.BigEndian, keepAliveMagic); err != nil {
|
||||
return
|
||||
}
|
||||
if err := binary.Write(t.sess.Conn, binary.BigEndian, size); err != nil {
|
||||
return
|
||||
}
|
||||
randData := make([]byte, size)
|
||||
rng.Read(randData)
|
||||
if _, err := t.sess.Conn.Write(randData); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
448
pkg/dhcp/dhcp.go
Normal file
448
pkg/dhcp/dhcp.go
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
// Package dhcp implements a minimal DHCP client operating on raw Ethernet frames.
|
||||
//
|
||||
// It constructs DHCP DISCOVER/REQUEST as complete Ethernet frames (including
|
||||
// IP and UDP headers) and parses DHCP OFFER/ACK responses. This allows the
|
||||
// VPN client to obtain an IP address without an external dhcpcd dependency.
|
||||
//
|
||||
// The DHCP exchange is performed over the VPN tunnel: frames are sent via
|
||||
// the tunnel's WriteFrames and responses are identified from incoming tunnel frames.
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Route is a classless static route from DHCP option 121/249.
|
||||
type Route struct {
|
||||
Dest net.IPNet
|
||||
Gateway net.IP
|
||||
}
|
||||
|
||||
// Lease holds the result of a successful DHCP exchange.
|
||||
type Lease struct {
|
||||
ClientIP net.IP
|
||||
SubnetMask net.IPMask
|
||||
Gateway net.IP
|
||||
DNS []net.IP
|
||||
Routes []Route // Classless static routes (option 121/249)
|
||||
LeaseTime time.Duration
|
||||
ServerIP net.IP // DHCP server identifier
|
||||
}
|
||||
|
||||
const (
|
||||
bootRequest = 1
|
||||
bootReply = 2
|
||||
hwEthernet = 1
|
||||
hwAddrLen = 6
|
||||
dhcpMagic = 0x63825363
|
||||
clientPort = 68
|
||||
serverPort = 67
|
||||
maxDHCPSize = 576
|
||||
|
||||
// DHCP message types (option 53)
|
||||
dhcpDiscover = 1
|
||||
dhcpOffer = 2
|
||||
dhcpRequest = 3
|
||||
dhcpAck = 5
|
||||
dhcpNak = 6
|
||||
|
||||
// DHCP options
|
||||
optSubnetMask = 1
|
||||
optRouter = 3
|
||||
optDNS = 6
|
||||
optLeaseTime = 51
|
||||
optMessageType = 53
|
||||
optServerID = 54
|
||||
optRequestedIP = 50
|
||||
optParamRequest = 55
|
||||
optClasslessRoutes = 121
|
||||
optMSClasslessRoutes = 249
|
||||
optEnd = 255
|
||||
)
|
||||
|
||||
// Client performs DHCP over raw Ethernet frames sent through a VPN tunnel.
|
||||
type Client struct {
|
||||
MAC net.HardwareAddr // Client MAC address (from TAP device)
|
||||
xid uint32 // Transaction ID
|
||||
frameCh chan []byte // Channel to receive candidate DHCP response frames
|
||||
}
|
||||
|
||||
// NewClient creates a DHCP client for the given MAC address.
|
||||
func NewClient(mac net.HardwareAddr) *Client {
|
||||
return &Client{
|
||||
MAC: mac,
|
||||
xid: rand.Uint32(),
|
||||
frameCh: make(chan []byte, 16),
|
||||
}
|
||||
}
|
||||
|
||||
// FeedFrame should be called with every Ethernet frame received from the tunnel.
|
||||
// It checks if the frame is a DHCP response and queues it for processing.
|
||||
func (c *Client) FeedFrame(frame []byte) {
|
||||
if !isDHCPResponse(frame, c.xid) {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case c.frameCh <- append([]byte(nil), frame...):
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Run performs a full DHCP exchange (DISCOVER→OFFER→REQUEST→ACK).
|
||||
// sendFrame is called to transmit each Ethernet frame through the tunnel.
|
||||
// Returns the lease on success.
|
||||
func (c *Client) Run(sendFrame func([]byte) error, timeout time.Duration) (*Lease, error) {
|
||||
// Send DISCOVER
|
||||
disc := c.buildDiscover()
|
||||
if err := sendFrame(disc); err != nil {
|
||||
return nil, fmt.Errorf("send discover: %w", err)
|
||||
}
|
||||
|
||||
// Wait for OFFER
|
||||
offer, err := c.waitForType(dhcpOffer, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("waiting for offer: %w", err)
|
||||
}
|
||||
|
||||
offeredIP := net.IP(offer.yiaddr[:])
|
||||
serverID := offer.getOptionIP(optServerID)
|
||||
if serverID == nil {
|
||||
return nil, fmt.Errorf("offer missing server identifier")
|
||||
}
|
||||
|
||||
// Send REQUEST
|
||||
req := c.buildRequest(offeredIP, serverID)
|
||||
if err := sendFrame(req); err != nil {
|
||||
return nil, fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
|
||||
// Wait for ACK
|
||||
ack, err := c.waitForType(dhcpAck, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("waiting for ack: %w", err)
|
||||
}
|
||||
|
||||
lease := &Lease{
|
||||
ClientIP: net.IP(ack.yiaddr[:]).To4(),
|
||||
ServerIP: ack.getOptionIP(optServerID),
|
||||
SubnetMask: net.IPMask(ack.getOptionRaw(optSubnetMask)),
|
||||
Gateway: ack.getOptionIP(optRouter),
|
||||
DNS: ack.getOptionIPs(optDNS),
|
||||
}
|
||||
if lt := ack.getOptionUint32(optLeaseTime); lt > 0 {
|
||||
lease.LeaseTime = time.Duration(lt) * time.Second
|
||||
}
|
||||
if lease.SubnetMask == nil {
|
||||
lease.SubnetMask = net.CIDRMask(24, 32)
|
||||
}
|
||||
|
||||
// Parse classless static routes (option 121, falls back to 249)
|
||||
if routes := parseClasslessRoutes(ack.getOptionRaw(optClasslessRoutes)); len(routes) > 0 {
|
||||
lease.Routes = routes
|
||||
} else if routes := parseClasslessRoutes(ack.getOptionRaw(optMSClasslessRoutes)); len(routes) > 0 {
|
||||
lease.Routes = routes
|
||||
}
|
||||
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
func (c *Client) waitForType(msgType byte, timeout time.Duration) (*dhcpMsg, error) {
|
||||
deadline := time.After(timeout)
|
||||
for {
|
||||
select {
|
||||
case <-deadline:
|
||||
return nil, fmt.Errorf("timeout waiting for DHCP message type %d", msgType)
|
||||
case frame := <-c.frameCh:
|
||||
msg, err := parseDHCPFrame(frame)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if msg.getOptionByte(optMessageType) == msgType {
|
||||
return msg, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dhcpMsg is a parsed DHCP message.
|
||||
type dhcpMsg struct {
|
||||
op byte
|
||||
xid uint32
|
||||
yiaddr [4]byte
|
||||
siaddr [4]byte
|
||||
options []dhcpOption
|
||||
}
|
||||
|
||||
type dhcpOption struct {
|
||||
code byte
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (m *dhcpMsg) getOption(code byte) *dhcpOption {
|
||||
for i := range m.options {
|
||||
if m.options[i].code == code {
|
||||
return &m.options[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dhcpMsg) getOptionByte(code byte) byte {
|
||||
if o := m.getOption(code); o != nil && len(o.data) >= 1 {
|
||||
return o.data[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *dhcpMsg) getOptionIP(code byte) net.IP {
|
||||
if o := m.getOption(code); o != nil && len(o.data) >= 4 {
|
||||
return net.IP(o.data[:4]).To4()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dhcpMsg) getOptionIPs(code byte) []net.IP {
|
||||
o := m.getOption(code)
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
var ips []net.IP
|
||||
for i := 0; i+3 < len(o.data); i += 4 {
|
||||
ips = append(ips, net.IP(o.data[i:i+4]).To4())
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func (m *dhcpMsg) getOptionRaw(code byte) []byte {
|
||||
if o := m.getOption(code); o != nil {
|
||||
return o.data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dhcpMsg) getOptionUint32(code byte) uint32 {
|
||||
if o := m.getOption(code); o != nil && len(o.data) >= 4 {
|
||||
return binary.BigEndian.Uint32(o.data)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// buildDiscover creates a full Ethernet frame containing a DHCP DISCOVER.
|
||||
func (c *Client) buildDiscover() []byte {
|
||||
opts := []dhcpOption{
|
||||
{optMessageType, []byte{dhcpDiscover}},
|
||||
{optParamRequest, []byte{optSubnetMask, optRouter, optDNS, optLeaseTime, optClasslessRoutes, optMSClasslessRoutes}},
|
||||
}
|
||||
return c.buildFrame(opts, [4]byte{}, nil)
|
||||
}
|
||||
|
||||
// buildRequest creates a full Ethernet frame containing a DHCP REQUEST.
|
||||
func (c *Client) buildRequest(requestedIP, serverID net.IP) []byte {
|
||||
opts := []dhcpOption{
|
||||
{optMessageType, []byte{dhcpRequest}},
|
||||
{optRequestedIP, requestedIP.To4()},
|
||||
{optServerID, serverID.To4()},
|
||||
{optParamRequest, []byte{optSubnetMask, optRouter, optDNS, optLeaseTime, optClasslessRoutes, optMSClasslessRoutes}},
|
||||
}
|
||||
return c.buildFrame(opts, [4]byte{}, serverID.To4())
|
||||
}
|
||||
|
||||
// buildFrame constructs a complete Ethernet/IP/UDP/DHCP frame.
|
||||
func (c *Client) buildFrame(opts []dhcpOption, ciaddr [4]byte, _ net.IP) []byte {
|
||||
// Build DHCP payload
|
||||
dhcp := make([]byte, 240) // fixed DHCP header
|
||||
dhcp[0] = bootRequest
|
||||
dhcp[1] = hwEthernet
|
||||
dhcp[2] = hwAddrLen
|
||||
binary.BigEndian.PutUint32(dhcp[4:8], c.xid)
|
||||
copy(dhcp[12:16], ciaddr[:])
|
||||
copy(dhcp[28:34], c.MAC)
|
||||
binary.BigEndian.PutUint32(dhcp[236:240], dhcpMagic)
|
||||
|
||||
// Append options
|
||||
for _, o := range opts {
|
||||
dhcp = append(dhcp, o.code, byte(len(o.data)))
|
||||
dhcp = append(dhcp, o.data...)
|
||||
}
|
||||
dhcp = append(dhcp, optEnd)
|
||||
|
||||
// Pad to minimum size
|
||||
for len(dhcp) < 300 {
|
||||
dhcp = append(dhcp, 0)
|
||||
}
|
||||
|
||||
// Build UDP header (8 bytes)
|
||||
udpLen := uint16(8 + len(dhcp))
|
||||
udp := make([]byte, 8)
|
||||
binary.BigEndian.PutUint16(udp[0:2], clientPort)
|
||||
binary.BigEndian.PutUint16(udp[2:4], serverPort)
|
||||
binary.BigEndian.PutUint16(udp[4:6], udpLen)
|
||||
// checksum = 0 (optional for IPv4 UDP)
|
||||
|
||||
// Build IPv4 header (20 bytes, no options)
|
||||
ipLen := uint16(20 + len(udp) + len(dhcp))
|
||||
ip := make([]byte, 20)
|
||||
ip[0] = 0x45 // version=4, IHL=5
|
||||
binary.BigEndian.PutUint16(ip[2:4], ipLen)
|
||||
binary.BigEndian.PutUint16(ip[6:8], 0) // flags=0, fragment=0
|
||||
ip[8] = 64 // TTL
|
||||
ip[9] = 17 // protocol = UDP
|
||||
copy(ip[12:16], net.IPv4zero.To4()) // src = 0.0.0.0
|
||||
copy(ip[16:20], net.IPv4bcast.To4()) // dst = 255.255.255.255
|
||||
// IP header checksum
|
||||
binary.BigEndian.PutUint16(ip[10:12], ipChecksum(ip))
|
||||
|
||||
// Build Ethernet header (14 bytes)
|
||||
eth := make([]byte, 14)
|
||||
copy(eth[0:6], net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) // dst = broadcast
|
||||
copy(eth[6:12], c.MAC) // src
|
||||
binary.BigEndian.PutUint16(eth[12:14], 0x0800) // EtherType = IPv4
|
||||
|
||||
frame := make([]byte, 0, len(eth)+len(ip)+len(udp)+len(dhcp))
|
||||
frame = append(frame, eth...)
|
||||
frame = append(frame, ip...)
|
||||
frame = append(frame, udp...)
|
||||
frame = append(frame, dhcp...)
|
||||
return frame
|
||||
}
|
||||
|
||||
// isDHCPResponse checks if an Ethernet frame is a DHCP response (UDP from port 67 to 68)
|
||||
// matching our transaction ID.
|
||||
func isDHCPResponse(frame []byte, xid uint32) bool {
|
||||
if len(frame) < 14+20+8+240 { // eth + ip + udp + dhcp minimum
|
||||
return false
|
||||
}
|
||||
// EtherType must be IPv4
|
||||
if binary.BigEndian.Uint16(frame[12:14]) != 0x0800 {
|
||||
return false
|
||||
}
|
||||
ipStart := 14
|
||||
// IP protocol must be UDP (17)
|
||||
if frame[ipStart+9] != 17 {
|
||||
return false
|
||||
}
|
||||
ihl := int(frame[ipStart]&0x0f) * 4
|
||||
udpStart := ipStart + ihl
|
||||
if len(frame) < udpStart+8 {
|
||||
return false
|
||||
}
|
||||
srcPort := binary.BigEndian.Uint16(frame[udpStart : udpStart+2])
|
||||
dstPort := binary.BigEndian.Uint16(frame[udpStart+2 : udpStart+4])
|
||||
if srcPort != serverPort || dstPort != clientPort {
|
||||
return false
|
||||
}
|
||||
dhcpStart := udpStart + 8
|
||||
if len(frame) < dhcpStart+240 {
|
||||
return false
|
||||
}
|
||||
// Check xid
|
||||
pktXid := binary.BigEndian.Uint32(frame[dhcpStart+4 : dhcpStart+8])
|
||||
return pktXid == xid
|
||||
}
|
||||
|
||||
// parseDHCPFrame extracts a DHCP message from a full Ethernet frame.
|
||||
func parseDHCPFrame(frame []byte) (*dhcpMsg, error) {
|
||||
ipStart := 14
|
||||
ihl := int(frame[ipStart]&0x0f) * 4
|
||||
dhcpStart := ipStart + ihl + 8 // skip IP + UDP headers
|
||||
data := frame[dhcpStart:]
|
||||
|
||||
if len(data) < 240 {
|
||||
return nil, fmt.Errorf("dhcp packet too short")
|
||||
}
|
||||
|
||||
msg := &dhcpMsg{
|
||||
op: data[0],
|
||||
xid: binary.BigEndian.Uint32(data[4:8]),
|
||||
}
|
||||
copy(msg.yiaddr[:], data[16:20])
|
||||
copy(msg.siaddr[:], data[20:24])
|
||||
|
||||
// Parse options (after magic cookie at offset 236)
|
||||
magic := binary.BigEndian.Uint32(data[236:240])
|
||||
if magic != dhcpMagic {
|
||||
return nil, fmt.Errorf("bad DHCP magic: %x", magic)
|
||||
}
|
||||
|
||||
opts := data[240:]
|
||||
for len(opts) > 0 {
|
||||
code := opts[0]
|
||||
if code == optEnd {
|
||||
break
|
||||
}
|
||||
if code == 0 { // padding
|
||||
opts = opts[1:]
|
||||
continue
|
||||
}
|
||||
if len(opts) < 2 {
|
||||
break
|
||||
}
|
||||
length := int(opts[1])
|
||||
if len(opts) < 2+length {
|
||||
break
|
||||
}
|
||||
msg.options = append(msg.options, dhcpOption{
|
||||
code: code,
|
||||
data: append([]byte(nil), opts[2:2+length]...),
|
||||
})
|
||||
opts = opts[2+length:]
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// parseClasslessRoutes decodes RFC 3442 classless static routes.
|
||||
// Encoding: [mask_width] [significant dest octets...] [gateway 4 bytes]
|
||||
// e.g. /0 = 1+4 bytes (default route), /25 = 1+4+4 bytes
|
||||
func parseClasslessRoutes(data []byte) []Route {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
var routes []Route
|
||||
for len(data) > 0 {
|
||||
if len(data) < 1 {
|
||||
break
|
||||
}
|
||||
maskWidth := int(data[0])
|
||||
data = data[1:]
|
||||
if maskWidth > 32 {
|
||||
break
|
||||
}
|
||||
// Number of significant destination octets
|
||||
nOctets := (maskWidth + 7) / 8
|
||||
if len(data) < nOctets+4 {
|
||||
break
|
||||
}
|
||||
var dest [4]byte
|
||||
copy(dest[:], data[:nOctets])
|
||||
data = data[nOctets:]
|
||||
gw := net.IP(append([]byte(nil), data[:4]...)).To4()
|
||||
data = data[4:]
|
||||
|
||||
routes = append(routes, Route{
|
||||
Dest: net.IPNet{
|
||||
IP: net.IP(dest[:]).To4(),
|
||||
Mask: net.CIDRMask(maskWidth, 32),
|
||||
},
|
||||
Gateway: gw,
|
||||
})
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func ipChecksum(header []byte) uint16 {
|
||||
var sum uint32
|
||||
for i := 0; i < len(header)-1; i += 2 {
|
||||
sum += uint32(binary.BigEndian.Uint16(header[i : i+2]))
|
||||
}
|
||||
if len(header)%2 == 1 {
|
||||
sum += uint32(header[len(header)-1]) << 8
|
||||
}
|
||||
for sum > 0xffff {
|
||||
sum = (sum & 0xffff) + (sum >> 16)
|
||||
}
|
||||
return ^uint16(sum)
|
||||
}
|
||||
154
pkg/protocol/http.go
Normal file
154
pkg/protocol/http.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package protocol
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// HTTP constants matching SoftEther's protocol layer.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.h
|
||||
const (
|
||||
httpVPNTarget = "/vpnsvc/vpn.cgi" // Pack exchange endpoint
|
||||
httpVPNTarget2 = "/vpnsvc/connect.cgi" // Signature upload endpoint
|
||||
httpContentType2 = "application/octet-stream" // Content-Type for Pack POST/response
|
||||
httpContentType3 = "image/jpeg" // Content-Type for signature POST
|
||||
)
|
||||
|
||||
// Conn wraps a TLS connection with buffered reading for HTTP protocol exchange.
|
||||
// The SoftEther protocol layers HTTP over TLS, using standard HTTP/1.1 POST
|
||||
// requests and responses to exchange binary Pack payloads.
|
||||
type Conn struct {
|
||||
raw net.Conn
|
||||
tls *tls.Conn
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// DialTLS establishes a TLS connection to a SoftEther server.
|
||||
// SoftEther servers listen on port 443 (or 5555, etc.) and accept TLS connections
|
||||
// that look like normal HTTPS traffic.
|
||||
func DialTLS(host string, port int, insecureSkipVerify bool) (*Conn, error) {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
raw, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tcp dial: %w", err)
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(raw, &tls.Config{
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
})
|
||||
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
raw.Close()
|
||||
return nil, fmt.Errorf("tls handshake: %w", err)
|
||||
}
|
||||
|
||||
return &Conn{
|
||||
raw: raw,
|
||||
tls: tlsConn,
|
||||
reader: bufio.NewReader(tlsConn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying TCP connection.
|
||||
func (c *Conn) Close() error {
|
||||
return c.raw.Close()
|
||||
}
|
||||
|
||||
// Read implements io.Reader, reading from the buffered TLS stream.
|
||||
func (c *Conn) Read(p []byte) (int, error) {
|
||||
return c.reader.Read(p)
|
||||
}
|
||||
|
||||
// Write implements io.Writer, writing to the TLS stream.
|
||||
func (c *Conn) Write(p []byte) (int, error) {
|
||||
return c.tls.Write(p)
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote IP address as a string.
|
||||
func (c *Conn) RemoteAddr() string {
|
||||
if addr, ok := c.raw.RemoteAddr().(*net.TCPAddr); ok {
|
||||
return addr.IP.String()
|
||||
}
|
||||
return c.raw.RemoteAddr().String()
|
||||
}
|
||||
|
||||
// UploadSignature sends the VPN protocol signature to the server.
|
||||
// The server validates this to confirm the client speaks the SoftEther protocol.
|
||||
// The full watermark GIF (src/Cedar/WaterMark.c) can be sent, but the server
|
||||
// also accepts the short string "VPNCONNECT" as a valid signature.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7208 (ClientUploadSignature)
|
||||
func UploadSignature(conn *Conn) error {
|
||||
body := []byte("VPNCONNECT")
|
||||
|
||||
// Build raw HTTP request matching SoftEther's ClientUploadSignature format.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "POST %s HTTP/1.1\r\n", httpVPNTarget2)
|
||||
fmt.Fprintf(&buf, "Host: %s\r\n", conn.RemoteAddr())
|
||||
fmt.Fprintf(&buf, "Content-Type: %s\r\n", httpContentType3)
|
||||
fmt.Fprintf(&buf, "Connection: Keep-Alive\r\n")
|
||||
fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(body))
|
||||
fmt.Fprintf(&buf, "\r\n")
|
||||
buf.Write(body)
|
||||
|
||||
if _, err := conn.Write(buf.Bytes()); err != nil {
|
||||
return fmt.Errorf("write signature: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendPack sends a Pack as an HTTP POST request body.
|
||||
// The Pack is serialized to binary and sent as the body of a POST to /vpnsvc/vpn.cgi.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.c#L1160 (HttpClientSend)
|
||||
func SendPack(conn *Conn, p *Pack) error {
|
||||
p.AddDummyValue()
|
||||
body, err := p.Bytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pack serialize: %w", err)
|
||||
}
|
||||
|
||||
// Build raw HTTP request matching SoftEther's HttpClientSend format exactly.
|
||||
// Go's http.Request.Write adds User-Agent and may reorder headers.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "POST %s HTTP/1.1\r\n", httpVPNTarget)
|
||||
fmt.Fprintf(&buf, "Host: %s\r\n", conn.RemoteAddr())
|
||||
fmt.Fprintf(&buf, "Keep-Alive: timeout=15; max=19\r\n")
|
||||
fmt.Fprintf(&buf, "Connection: Keep-Alive\r\n")
|
||||
fmt.Fprintf(&buf, "Content-Type: %s\r\n", httpContentType2)
|
||||
fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(body))
|
||||
fmt.Fprintf(&buf, "\r\n")
|
||||
buf.Write(body)
|
||||
|
||||
if _, err := conn.Write(buf.Bytes()); err != nil {
|
||||
return fmt.Errorf("write pack: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecvPack reads an HTTP response and deserializes the body as a Pack.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.c#L1202 (HttpClientRecv)
|
||||
func RecvPack(conn *Conn) (*Pack, error) {
|
||||
resp, err := http.ReadResponse(conn.reader, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read http response: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read entire body into buffer so we can parse the Pack from it
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response body: %w", err)
|
||||
}
|
||||
|
||||
return ReadPack(bytes.NewReader(body))
|
||||
}
|
||||
366
pkg/protocol/pack.go
Normal file
366
pkg/protocol/pack.go
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
// Package protocol implements the SoftEther VPN wire protocol.
|
||||
//
|
||||
// SoftEther uses a custom binary serialization format called "Pack" for all
|
||||
// control-plane messages (hello, auth, welcome). A Pack contains Elements,
|
||||
// each with a name, type, and one or more Values.
|
||||
//
|
||||
// Wire format (all big-endian):
|
||||
//
|
||||
// Pack: uint32(numElements) + Element...
|
||||
// Element: BufStr(name) + uint32(type) + uint32(numValues) + Value...
|
||||
// BufStr: uint32(len) + bytes (null-terminated)
|
||||
// Value: type-dependent (see below)
|
||||
//
|
||||
// Reference: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Value types matching SoftEther's VALUE_* constants.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.h#L20-L28
|
||||
const (
|
||||
ValueInt uint32 = 0 // 32-bit unsigned integer
|
||||
ValueData uint32 = 1 // Arbitrary binary data (prefixed with uint32 length)
|
||||
ValueStr uint32 = 2 // ANSI string (prefixed with uint32 length)
|
||||
ValueUniStr uint32 = 3 // Unicode string as UTF-8 (prefixed with uint32 length, null-terminated)
|
||||
ValueInt64 uint32 = 4 // 64-bit unsigned integer
|
||||
)
|
||||
|
||||
// Value holds a single typed value within an Element.
|
||||
type Value struct {
|
||||
IntValue uint32
|
||||
Int64Value uint64
|
||||
Data []byte
|
||||
Str string
|
||||
}
|
||||
|
||||
// Element is a named, typed collection of values within a Pack.
|
||||
type Element struct {
|
||||
Name string
|
||||
Type uint32
|
||||
Values []Value
|
||||
}
|
||||
|
||||
// Pack is a collection of named Elements, used for all SoftEther control messages.
|
||||
type Pack struct {
|
||||
Elements []*Element
|
||||
}
|
||||
|
||||
// --- Reading ---
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c#L831 (PackRead)
|
||||
|
||||
// ReadPack deserializes a Pack from a binary reader.
|
||||
func ReadPack(r io.Reader) (*Pack, error) {
|
||||
var num uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &num); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if num > 262144 {
|
||||
return nil, fmt.Errorf("too many elements: %d", num)
|
||||
}
|
||||
|
||||
p := &Pack{}
|
||||
for i := uint32(0); i < num; i++ {
|
||||
e, err := readElement(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("element %d: %w", i, err)
|
||||
}
|
||||
p.Elements = append(p.Elements, e)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func readElement(r io.Reader) (*Element, error) {
|
||||
name, err := readBufStr(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var typ, numValues uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &typ); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Read(r, binary.BigEndian, &numValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := &Element{Name: name, Type: typ}
|
||||
for i := uint32(0); i < numValues; i++ {
|
||||
v, err := readValue(r, typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Values = append(e.Values, v)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func readValue(r io.Reader, typ uint32) (v Value, err error) {
|
||||
switch typ {
|
||||
case ValueInt:
|
||||
err = binary.Read(r, binary.BigEndian, &v.IntValue)
|
||||
case ValueInt64:
|
||||
err = binary.Read(r, binary.BigEndian, &v.Int64Value)
|
||||
case ValueData:
|
||||
var size uint32
|
||||
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
|
||||
return
|
||||
}
|
||||
v.Data = make([]byte, size)
|
||||
_, err = io.ReadFull(r, v.Data)
|
||||
case ValueStr:
|
||||
var size uint32
|
||||
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(r, buf); err != nil {
|
||||
return
|
||||
}
|
||||
v.Str = string(buf)
|
||||
case ValueUniStr:
|
||||
var size uint32
|
||||
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(r, buf); err != nil {
|
||||
return
|
||||
}
|
||||
// SoftEther sends UTF-8 for UniStr on the wire
|
||||
v.Str = strings.TrimRight(string(buf), "\x00")
|
||||
default:
|
||||
err = fmt.Errorf("unknown value type: %d", typ)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// readBufStr reads a string written by SoftEther's WriteBufStr.
|
||||
// Wire format: uint32(strlen+1) + strlen bytes (no null terminator on wire).
|
||||
// The size field includes the null terminator in its count, but the null
|
||||
// is NOT actually written to the wire.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Memory.c#L2903
|
||||
func readBufStr(r io.Reader) (string, error) {
|
||||
var size uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &size); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if size == 0 {
|
||||
return "", nil
|
||||
}
|
||||
// size includes the null terminator count, but only (size-1) bytes are on the wire
|
||||
buf := make([]byte, size-1)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// --- Writing ---
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c#L873 (PackWrite)
|
||||
|
||||
// Bytes serializes the Pack to binary format.
|
||||
func (p *Pack) Bytes() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := binary.Write(&buf, binary.BigEndian, uint32(len(p.Elements))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range p.Elements {
|
||||
if err := writeElement(&buf, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeElement(w io.Writer, e *Element) error {
|
||||
if err := writeBufStr(w, e.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, e.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(e.Values))); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range e.Values {
|
||||
if err := writeValue(w, &v, e.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeValue(w io.Writer, v *Value, typ uint32) error {
|
||||
switch typ {
|
||||
case ValueInt:
|
||||
return binary.Write(w, binary.BigEndian, v.IntValue)
|
||||
case ValueInt64:
|
||||
return binary.Write(w, binary.BigEndian, v.Int64Value)
|
||||
case ValueData:
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(v.Data))); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(v.Data)
|
||||
return err
|
||||
case ValueStr:
|
||||
b := []byte(v.Str)
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(b))); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(b)
|
||||
return err
|
||||
case ValueUniStr:
|
||||
b := append([]byte(v.Str), 0) // UTF-8 with null terminator
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(b))); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(b)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unknown value type: %d", typ)
|
||||
}
|
||||
}
|
||||
|
||||
// writeBufStr writes a string in SoftEther's WriteBufStr format.
|
||||
// Wire format: uint32(strlen+1) + strlen bytes (no null terminator on wire).
|
||||
func writeBufStr(w io.Writer, s string) error {
|
||||
b := []byte(s)
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(b)+1)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Accessors ---
|
||||
|
||||
// GetElement finds an element by name (case-insensitive).
|
||||
func (p *Pack) GetElement(name string) *Element {
|
||||
upper := strings.ToUpper(name)
|
||||
for _, e := range p.Elements {
|
||||
if strings.ToUpper(e.Name) == upper {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInt returns the first uint32 value of the named element.
|
||||
func (p *Pack) GetInt(name string) uint32 {
|
||||
if e := p.GetElement(name); e != nil && e.Type == ValueInt && len(e.Values) > 0 {
|
||||
return e.Values[0].IntValue
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetInt64 returns the first uint64 value of the named element.
|
||||
func (p *Pack) GetInt64(name string) uint64 {
|
||||
if e := p.GetElement(name); e != nil && e.Type == ValueInt64 && len(e.Values) > 0 {
|
||||
return e.Values[0].Int64Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetStr returns the first string value of the named element (STR or UNISTR).
|
||||
func (p *Pack) GetStr(name string) string {
|
||||
if e := p.GetElement(name); e != nil && len(e.Values) > 0 {
|
||||
if e.Type == ValueStr || e.Type == ValueUniStr {
|
||||
return e.Values[0].Str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetData returns the first data value of the named element.
|
||||
func (p *Pack) GetData(name string) []byte {
|
||||
if e := p.GetElement(name); e != nil && e.Type == ValueData && len(e.Values) > 0 {
|
||||
return e.Values[0].Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBool returns the first integer value as a boolean (non-zero = true).
|
||||
func (p *Pack) GetBool(name string) bool {
|
||||
return p.GetInt(name) != 0
|
||||
}
|
||||
|
||||
// GetError returns the "error" field, which holds SoftEther error codes.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Cedar.h#L590
|
||||
func (p *Pack) GetError() uint32 {
|
||||
return p.GetInt("error")
|
||||
}
|
||||
|
||||
// --- Mutators ---
|
||||
|
||||
// AddInt adds a uint32 element.
|
||||
func (p *Pack) AddInt(name string, val uint32) {
|
||||
p.Elements = append(p.Elements, &Element{
|
||||
Name: name,
|
||||
Type: ValueInt,
|
||||
Values: []Value{{IntValue: val}},
|
||||
})
|
||||
}
|
||||
|
||||
// AddInt64 adds a uint64 element.
|
||||
func (p *Pack) AddInt64(name string, val uint64) {
|
||||
p.Elements = append(p.Elements, &Element{
|
||||
Name: name,
|
||||
Type: ValueInt64,
|
||||
Values: []Value{{Int64Value: val}},
|
||||
})
|
||||
}
|
||||
|
||||
// AddStr adds a string element.
|
||||
func (p *Pack) AddStr(name string, val string) {
|
||||
p.Elements = append(p.Elements, &Element{
|
||||
Name: name,
|
||||
Type: ValueStr,
|
||||
Values: []Value{{Str: val}},
|
||||
})
|
||||
}
|
||||
|
||||
// AddData adds a binary data element.
|
||||
func (p *Pack) AddData(name string, val []byte) {
|
||||
p.Elements = append(p.Elements, &Element{
|
||||
Name: name,
|
||||
Type: ValueData,
|
||||
Values: []Value{{Data: val}},
|
||||
})
|
||||
}
|
||||
|
||||
// AddBool adds a boolean element (stored as uint32: 0 or 1).
|
||||
func (p *Pack) AddBool(name string, val bool) {
|
||||
v := uint32(0)
|
||||
if val {
|
||||
v = 1
|
||||
}
|
||||
p.AddInt(name, v)
|
||||
}
|
||||
|
||||
// AddDummyValue adds the "pencore" random padding element.
|
||||
// SoftEther servers expect this padding in Pack messages sent via HTTP.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.c#L1165
|
||||
func (p *Pack) AddDummyValue() {
|
||||
size := rand.Intn(1000)
|
||||
buf := make([]byte, size)
|
||||
rand.Read(buf)
|
||||
p.AddData("pencore", buf)
|
||||
}
|
||||
|
||||
// AddIP4 adds an IPv4 address with the required companion @ipv6_* elements.
|
||||
// SoftEther always expects these three companion elements for any IP field.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c#L575 (PackAddIp32)
|
||||
func (p *Pack) AddIP4(name string, ip uint32) {
|
||||
p.AddBool(name+"@ipv6_bool", false)
|
||||
p.AddData(name+"@ipv6_array", make([]byte, 16))
|
||||
p.AddInt(name+"@ipv6_scope_id", 0)
|
||||
p.AddInt(name, ip)
|
||||
}
|
||||
139
pkg/tap/tap.go
Normal file
139
pkg/tap/tap.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Package tap provides Linux TAP (Layer 2) device management.
|
||||
//
|
||||
// A TAP device is a virtual Ethernet interface that allows userspace programs
|
||||
// to read and write raw Ethernet frames. SoftEther VPN operates at Layer 2,
|
||||
// so it bridges Ethernet frames between the TAP device and the VPN tunnel.
|
||||
//
|
||||
// This uses the Linux kernel's TUN/TAP driver via /dev/net/tun with the
|
||||
// IFF_TAP | IFF_NO_PI flags (TAP mode, no packet info header).
|
||||
//
|
||||
// See: https://www.kernel.org/doc/html/latest/networking/tuntap.html
|
||||
package tap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
tunDevice = "/dev/net/tun"
|
||||
ifnameSize = 16
|
||||
)
|
||||
|
||||
// Device represents an open TAP network interface.
|
||||
type Device struct {
|
||||
file *os.File
|
||||
Name string // Kernel-assigned interface name (e.g. "tap0")
|
||||
}
|
||||
|
||||
// Open creates a new TAP device with the given name.
|
||||
// If name is empty, the kernel assigns one automatically (tap0, tap1, ...).
|
||||
// Requires CAP_NET_ADMIN or root privileges.
|
||||
func Open(name string) (*Device, error) {
|
||||
fd, err := unix.Open(tunDevice, unix.O_RDWR|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %s: %w", tunDevice, err)
|
||||
}
|
||||
|
||||
var ifr [unix.IFNAMSIZ + 64]byte
|
||||
// IFF_TAP: Layer 2 (Ethernet frames), IFF_NO_PI: no 4-byte packet info header
|
||||
flags := uint16(unix.IFF_TAP | unix.IFF_NO_PI)
|
||||
ifr[ifnameSize] = byte(flags)
|
||||
ifr[ifnameSize+1] = byte(flags >> 8)
|
||||
|
||||
if name != "" {
|
||||
copy(ifr[:ifnameSize], name)
|
||||
}
|
||||
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.TUNSETIFF, uintptr(unsafe.Pointer(&ifr[0])))
|
||||
if errno != 0 {
|
||||
unix.Close(fd)
|
||||
return nil, fmt.Errorf("ioctl TUNSETIFF: %w", errno)
|
||||
}
|
||||
|
||||
// Read back the kernel-assigned name
|
||||
assignedName := string(ifr[:ifnameSize])
|
||||
for i, b := range assignedName {
|
||||
if b == 0 {
|
||||
assignedName = assignedName[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &Device{
|
||||
file: os.NewFile(uintptr(fd), tunDevice),
|
||||
Name: assignedName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read reads a single Ethernet frame from the TAP device.
|
||||
func (d *Device) Read(buf []byte) (int, error) {
|
||||
return d.file.Read(buf)
|
||||
}
|
||||
|
||||
// Write writes a single Ethernet frame to the TAP device.
|
||||
func (d *Device) Write(buf []byte) (int, error) {
|
||||
return d.file.Write(buf)
|
||||
}
|
||||
|
||||
// Close closes the TAP device file descriptor, which also removes the interface.
|
||||
func (d *Device) Close() error {
|
||||
return d.file.Close()
|
||||
}
|
||||
|
||||
// MAC returns the hardware (MAC) address of the TAP interface.
|
||||
func (d *Device) MAC() (net.HardwareAddr, error) {
|
||||
sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("socket: %w", err)
|
||||
}
|
||||
defer unix.Close(sock)
|
||||
|
||||
var ifr [40]byte
|
||||
copy(ifr[:ifnameSize], d.Name)
|
||||
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCGIFHWADDR, uintptr(unsafe.Pointer(&ifr[0])))
|
||||
if errno != 0 {
|
||||
return nil, fmt.Errorf("SIOCGIFHWADDR: %w", errno)
|
||||
}
|
||||
// MAC starts at offset ifnameSize+2 (sa_family is 2 bytes)
|
||||
mac := make(net.HardwareAddr, 6)
|
||||
copy(mac, ifr[ifnameSize+2:ifnameSize+8])
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
// SetUp brings the TAP interface up (equivalent to `ip link set <name> up`).
|
||||
func (d *Device) SetUp() error {
|
||||
sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("socket: %w", err)
|
||||
}
|
||||
defer unix.Close(sock)
|
||||
|
||||
var ifr [40]byte
|
||||
copy(ifr[:ifnameSize], d.Name)
|
||||
|
||||
// SIOCGIFFLAGS: get current interface flags
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCGIFFLAGS, uintptr(unsafe.Pointer(&ifr[0])))
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("SIOCGIFFLAGS: %w", errno)
|
||||
}
|
||||
|
||||
// Set IFF_UP flag
|
||||
flags := uint16(ifr[ifnameSize]) | uint16(ifr[ifnameSize+1])<<8
|
||||
flags |= unix.IFF_UP
|
||||
ifr[ifnameSize] = byte(flags)
|
||||
ifr[ifnameSize+1] = byte(flags >> 8)
|
||||
|
||||
// SIOCSIFFLAGS: apply updated flags
|
||||
_, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCSIFFLAGS, uintptr(unsafe.Pointer(&ifr[0])))
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("SIOCSIFFLAGS: %w", errno)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue