New random transaction ID for each DHCP exchange (initial and renewal) to avoid matching stale responses from previous transactions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| cmd/softether-go | ||
| pkg | ||
| vendor | ||
| .gitignore | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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)
- 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/tunfor TAP devices) CAP_NET_ADMINor root (TAP device creation, route management)ipcommand (iproute2) on$PATH
Building
Requires Go 1.24 or later.
go build -o softether-go ./cmd/softether-go/
Static binary (for Alpine, scratch containers):
CGO_ENABLED=0 go build -o softether-go ./cmd/softether-go/
Cross-compilation:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o softether-go ./cmd/softether-go/
Nix:
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) |
-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.
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.
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 <VPN_IP> table N and ip route replace default via <VPN_GW> dev <TAP> table N. Needed when the VPN server forwards ports to the client.
Examples
Minimal:
softether-go -host vpn.example.com -user admin -pass secret
Full setup:
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
No DHCP (manual config):
softether-go -host vpn.example.com -user admin -pass secret -no-dhcp -tap vpn0
Docker
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
│ │
- TLS connect — standard TLS to the server port (typically 443 or 992)
- Upload signature —
VPNCONNECTvia HTTP POST to/vpnsvc/connect.cgi - Download Hello — server sends version info and 20-byte random challenge
- Upload Auth — credentials, client version, connection options, node info as a Pack
- Download Welcome — session info on success, error code on failure
- Tunnel mode — raw TCP block framing:
uint32(numBlocks) + [uint32(size) + data].... Keepalive:uint32(0xFFFFFFFF) + uint32(randSize) + randData
Session lifecycle
- Start bridge — bidirectional frame forwarding begins immediately
- DHCP exchange — DHCP frames sent through tunnel while bridge runs. DHCP client intercepts server frames via
FeedFramecallback - Configure TAP — IP, routes, DNS, policy routing applied from lease
- Wait — blocks until bridge errors or signal arrives
- Cleanup — flush addresses, restore DNS, remove policy routes
- 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 <VPN_IP> table N ensures reply packets for VPN port forwards go back through the tunnel, not the default route.
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