# softether-go Standalone SoftEther VPN client written in Go. Connects to SoftEther VPN servers using the native protocol over TLS, with built-in DHCP, automatic reconnection, and route management. Single static binary, Linux only, zero runtime dependencies beyond `ip` (iproute2). ## Features - Native SoftEther protocol (TLS + HTTP handshake + TCP block framing) - Built-in DHCP client (raw Ethernet frame construction through the VPN tunnel) - Automatic reconnection with fresh DHCP on each reconnect - Host route to VPN server via existing default gateway (prevents routing loops) - Classless static routes (DHCP option 121/249, RFC 3442) - Policy routing for asymmetric return paths (VPN port forwards) - CONNMARK-based DNAT reply routing (for port forwards to namespaces/VMs) - DNS configuration from DHCP lease (backup/restore of `/etc/resolv.conf`) - Deterministic MAC address support for stable DHCP assignments - Hashed password (SHA-0) and plaintext password (RADIUS/external) authentication - ~4.6 MB RAM under load, single process ## Requirements - Linux (uses `/dev/net/tun` for TAP devices) - `CAP_NET_ADMIN` or root (TAP device creation, route management) - `ip` command (iproute2) on `$PATH` - `iptables` on `$PATH` (only if using `-connmark`) ## Building Requires Go 1.24 or later. ```bash go build -o softether-go ./cmd/softether-go/ ``` Static binary (for Alpine, scratch containers): ```bash CGO_ENABLED=0 go build -o softether-go ./cmd/softether-go/ ``` Cross-compilation: ```bash CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o softether-go ./cmd/softether-go/ ``` Nix: ```bash nix build # result in ./result/bin/softether-go nix develop # dev shell with Go tooling ``` The only Go dependency is `golang.org/x/sys` (vendored in `vendor/`). ## Usage ``` softether-go [flags] ``` ### Required flags | Flag | Description | |---|---| | `-host` | SoftEther server hostname or IP | | `-user` | Authentication username | ### Optional flags | Flag | Default | Description | |---|---|---| | `-pass` | `""` | Authentication password | | `-port` | `443` | Server port | | `-hub` | `DEFAULT` | Virtual hub name | | `-tap` | *(auto)* | TAP interface name (kernel-assigned if empty) | | `-mac` | *(auto)* | TAP interface MAC address (e.g. `5E:3B:6F:63:A8:3E`) | | `-plain-password` | `false` | Send password as plaintext (AuthType 2, for RADIUS/external auth) | | `-insecure` | `false` | Skip TLS certificate verification | | `-no-dhcp` | `false` | Disable built-in DHCP client | | `-accept-default-gateway` | `false` | Install DHCP-provided gateway as default route | | `-accept-static-routes` | `false` | Install DHCP classless static routes (option 121/249) | | `-accept-dns` | `false` | Set `/etc/resolv.conf` from DHCP-provided DNS servers | | `-policy-route-table` | `0` | Policy routing table number (0 = disabled) | | `-connmark` | `false` | Use CONNMARK to route DNAT reply traffic back through VPN | | `-reconnect-delay` | `5s` | Delay between reconnection attempts | ### Authentication **Hashed password (AuthType 1)** — the default. Password is hashed with SHA-0 and combined with the server's random challenge. Used for local user accounts. ```bash softether-go -host vpn.example.com -user admin -pass secret ``` **Plaintext password (AuthType 2)** — enabled with `-plain-password`. Password sent as-is over TLS. Used for RADIUS/external auth. ```bash softether-go -host vpn.example.com -user admin -pass secret -plain-password ``` ### Network configuration **`-mac`** — sets a specific MAC for deterministic DHCP assignments across reconnects. **`-accept-default-gateway`** — installs DHCP gateway as default route (metric 50). A `/32` host route to the VPN server via the original gateway prevents routing loops. **`-accept-static-routes`** — installs classless static routes from DHCP option 121/249. A `0.0.0.0/0` entry is only installed if `-accept-default-gateway` is also set. **`-accept-dns`** — overwrites `/etc/resolv.conf` with DHCP DNS servers. Original is backed up and restored on disconnect. **`-policy-route-table N`** — policy routing for asymmetric return paths. Adds `ip rule from table N` and `ip route replace default via dev table N`. Needed when the VPN server forwards ports to the client. **`-connmark`** — requires `-policy-route-table`. Uses iptables CONNMARK to route DNAT reply traffic back through the VPN tunnel. Without this, traffic forwarded to local namespaces/VMs (via DNAT) gets replies routed via the default gateway instead of the tunnel, breaking the connection. Adds `CONNMARK --set-mark` on incoming VPN packets and `CONNMARK --restore-mark` on reply packets from other interfaces. ### Examples Minimal: ```bash softether-go -host vpn.example.com -user admin -pass secret ``` Full setup: ```bash softether-go \ -host vpn.example.com \ -port 992 \ -user admin \ -pass secret \ -plain-password \ -tap vpn0 \ -mac 5E:3B:6F:63:A8:3E \ -insecure \ -accept-default-gateway \ -accept-static-routes \ -accept-dns \ -policy-route-table 200 \ -connmark ``` No DHCP (manual config): ```bash softether-go -host vpn.example.com -user admin -pass secret -no-dhcp -tap vpn0 ``` ### Docker ```bash docker run --rm -it \ --cap-add NET_ADMIN \ --device /dev/net/tun \ -v ./softether-go:/usr/bin/softether-go \ alpine \ softether-go -host vpn.example.com -user admin -pass secret \ -plain-password -insecure -tap vpn0 \ -accept-default-gateway -accept-dns ``` Container needs `iproute2` (`apk add iproute2` on Alpine). ### Signals - **SIGINT / SIGTERM** — clean shutdown: closes tunnel, flushes TAP, restores DNS, removes routes - During reconnect delay, signal triggers immediate shutdown ## Architecture ### Connection flow ``` Client Server │ │ ├──── TLS handshake ───────────────────►│ Standard TLS, looks like HTTPS │ │ ├──── POST /vpnsvc/connect.cgi ────────►│ Upload signature ("VPNCONNECT") │ │ │◄──── HTTP 200 (Pack: hello) ──────────┤ Server version + random challenge │ │ ├──── POST /vpnsvc/vpn.cgi ───────────►│ Upload auth (credentials + metadata) │ │ │◄──── HTTP 200 (Pack: welcome) ────────┤ Session info (or error code) │ │ │◄────── TCP block framing ────────────►│ Ethernet frames + keepalive │ │ ``` 1. **TLS connect** — standard TLS to the server port (typically 443 or 992) 2. **Upload signature** — `VPNCONNECT` via HTTP POST to `/vpnsvc/connect.cgi` 3. **Download Hello** — server sends version info and 20-byte random challenge 4. **Upload Auth** — credentials, client version, connection options, node info as a Pack 5. **Download Welcome** — session info on success, error code on failure 6. **Tunnel mode** — raw TCP block framing: `uint32(numBlocks) + [uint32(size) + data]...`. Keepalive: `uint32(0xFFFFFFFF) + uint32(randSize) + randData` ### Session lifecycle 1. **Start bridge** — bidirectional frame forwarding begins immediately 2. **DHCP exchange** — DHCP frames sent through tunnel while bridge runs. DHCP client intercepts server frames via `FeedFrame` callback 3. **Configure TAP** — IP, routes, DNS, policy routing applied from lease 4. **Wait** — blocks until bridge errors or signal arrives 5. **Cleanup** — flush addresses, restore DNS, remove policy routes 6. **Reconnect** — wait, then repeat with fresh handshake and DHCP ### DHCP through the tunnel SoftEther operates at Layer 2. The built-in DHCP client constructs complete Ethernet/IP/UDP/DHCP frames sent through the tunnel: ``` DHCP Client VPN Tunnel DHCP Server (on VPN) │ │ │ ├── DISCOVER ──► WriteFrames ├────────────────────────────►│ │ │ │ │◄── FeedFrame ◄─ ReadFrames ◄──────── OFFER ─────────────┤ │ │ │ ├── REQUEST ───► WriteFrames ├────────────────────────────►│ │ │ │ │◄── FeedFrame ◄─ ReadFrames ◄──────── ACK ───────────────┤ │ │ │ ``` DHCP options requested: subnet mask (1), router (3), DNS (6), lease time (51), classless static routes (121/249). **Why fresh DHCP on reconnect**: SoftEther's `DHCPForce` policy drops packets from IPs not in its `DhcpAllocated` table. On disconnect, the server clears all entries for the session. ### Routing **Server host route** — `/32` route to VPN server via current default gateway, added before first connection. Prevents routing loop when VPN becomes default route. **Default gateway** — DHCP gateway installed with metric 50, lower than existing default routes. **Static routes** — classless routes from option 121/249. Default route entries (`0.0.0.0/0`) only installed with `-accept-default-gateway`. **Policy routing** — `ip rule from table N` ensures reply packets for VPN port forwards go back through the tunnel, not the default route. **CONNMARK (`-connmark`)** — solves a subtler routing problem: when VPN traffic is DNAT'd to a local namespace or VM, the reply packets have a different source IP (the namespace veth) so the `from ` rule doesn't match. CONNMARK marks all connections arriving on the VPN interface, then restores the mark on reply packets from any interface, routing them back through the tunnel via `fwmark` rule. ### Password hashing SoftEther uses **SHA-0** (not SHA-1) — no left-rotate in message schedule. `HashedPassword = SHA0(password)`, `SecurePassword = SHA0(HashedPassword + ServerRandom)`. Plaintext auth (AuthType 2) sends password as-is over TLS. ### Keepalive Sent every 3 seconds: `uint32(0xFFFFFFFF) + uint32(randSize) + randData`. Silently consumed, never forwarded to TAP. ## Project structure ``` cmd/softether-go/ main.go Flag parsing, TAP setup, reconnect loop session.go Session lifecycle, DHCP orchestration pkg/client/ client.go SoftEther handshake and session tunnel.go TCP block framing, keepalive, frame bridging crypto.go SHA-0 and password hashing pkg/protocol/ http.go TLS connection, HTTP transport layer pack.go Pack binary serialization pkg/dhcp/ dhcp.go DHCP client (raw Ethernet frames) pkg/netcfg/ netcfg.go TAP configuration, routing, DNS management pkg/tap/ tap.go Linux TAP device management ``` ## License MIT