// 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 Go 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 } // swapLE32 byte-swaps a uint32 to encode it as little-endian within a big-endian Pack int. // SoftEther's OutRpcNodeInfo wraps node info fields with LittleEndian32() before PackAddInt, // and InRpcNodeInfo reads them with LittleEndian32(PackGetInt(...)). Our Pack writes big-endian, // so we pre-swap to match the C client's wire format. func swapLE32(v uint32) uint32 { return (v>>24)&0xFF | (v>>8)&0xFF00 | (v<<8)&0xFF0000 | (v<<24)&0xFF000000 } // 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]) // Node info ints use LittleEndian32 encoding (see Admin.c:OutRpcNodeInfo). // The server reads them back with LittleEndian32(PackGetInt(...)). p.AddInt("ClientProductVer", swapLE32(clientVer)) p.AddInt("ClientProductBuild", swapLE32(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 }