No description
Find a file
Git Sagar 47e06b525c tunnel: replace write mutex with channel-based single writer
All writes (frames, keepalive, DHCP renewal) are queued to a buffered
channel and drained by a single writer goroutine. Eliminates mutex
contention on the data path entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 23:16:27 +05:30
cmd/softether-go add DHCP lease renewal at T/2 2026-06-06 22:05:34 +05:30
pkg tunnel: replace write mutex with channel-based single writer 2026-06-06 23:16:27 +05:30
vendor initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
.gitignore add .gitignore for build artifacts 2026-06-06 16:15:08 +05:30
CLAUDE.md rename -dhcp flag to -no-dhcp for cleaner UX 2026-06-06 17:53:01 +05:30
flake.lock initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
flake.nix initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
go.mod initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
go.sum initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
README.md consolidate docs into README.md, remove docs/ 2026-06-06 17:54:38 +05:30

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/tun for TAP devices)
  • CAP_NET_ADMIN or root (TAP device creation, route management)
  • ip command (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
  │                                       │
  1. TLS connect — standard TLS to the server port (typically 443 or 992)
  2. Upload signatureVPNCONNECT 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 routingip 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