softether-go/docs/architecture.md
Git Sagar 829ca73b1b 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>
2026-06-06 16:13:51 +05:30

7.3 KiB

Architecture

Connection flow

The SoftEther protocol layers HTTP over TLS, then switches to raw TCP framing for the data tunnel.

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
  │                                       │

Step details

  1. TLS connect — standard TLS to the server port (typically 443 or 992). The connection looks like normal HTTPS traffic to network observers.

  2. Upload signature — the client sends VPNCONNECT as the body of an HTTP POST to /vpnsvc/connect.cgi. The server validates this to confirm the client speaks the SoftEther protocol.

  3. Download Hello — the server responds with a binary Pack containing its version info and a 20-byte random challenge used for password hashing.

  4. Upload Auth — the client sends credentials (hashed or plaintext password), client version, connection options, and node info as a Pack via HTTP POST to /vpnsvc/vpn.cgi.

  5. Download Welcome — the server responds with session info (session name, connection name, session key, policies) on success, or an error code on failure.

  6. Tunnel mode — the connection switches from HTTP to raw TCP block framing. Each message is uint32(numBlocks) + [uint32(size) + data].... Keepalive packets use 0xFFFFFFFF as the block count, followed by random padding.

Pack format

SoftEther uses a custom binary serialization called "Pack" for all structured data exchange. A Pack contains named Elements, each holding typed Values (int, string, data, ip4).

Key details:

  • Element names are case-insensitive (compared with StrCmpi)
  • AddElement rejects duplicate names — the second add silently fails
  • String values use BufStr encoding: uint32(strlen+1) followed by strlen bytes (no null terminator on wire)
  • HTTP-transported Packs include a pencore dummy element with random padding

DHCP through the tunnel

SoftEther operates at Layer 2 — the tunnel carries raw Ethernet frames. The built-in DHCP client constructs complete Ethernet/IP/UDP/DHCP frames and sends them through the tunnel's frame transport.

DHCP Client                  VPN Tunnel                 DHCP Server (on VPN)
    │                            │                            │
    ├── DISCOVER ──► WriteFrames ├────────────────────────────►│
    │                            │                            │
    │◄── FeedFrame ◄─ ReadFrames ◄──────── OFFER ─────────────┤
    │                            │                            │
    ├── REQUEST ───► WriteFrames ├────────────────────────────►│
    │                            │                            │
    │◄── FeedFrame ◄─ ReadFrames ◄──────── ACK ───────────────┤
    │                            │                            │

The DHCP client starts reading tunnel frames before sending DISCOVER, so responses are not missed. Non-DHCP frames received during the exchange are dropped (the TAP bridge is not yet active).

Why raw frames?

The VPN tunnel transports Ethernet frames, and the DHCP exchange must happen inside the tunnel before the TAP interface has an IP address. Constructing frames directly avoids any external dependency (dhcpcd, udhcpc) and keeps the client self-contained.

DHCP options requested

  • Option 1: Subnet Mask
  • Option 3: Router (gateway)
  • Option 6: DNS Servers
  • Option 51: Lease Time
  • Option 121: Classless Static Routes (RFC 3442)
  • Option 249: Microsoft Classless Static Routes

Why DHCP is required on reconnect

SoftEther servers with DHCPForce policy discard any packet whose source IP is not in the server's IP table with DhcpAllocated=true. When a session disconnects, the server calls HubPaFree which deletes all MAC and IP table entries for that session. The new session has no entries, so all traffic is dropped until a fresh DHCP exchange creates a new DhcpAllocated=true entry.

Reconnection

On disconnect (TCP error, server timeout, etc.), the client:

  1. Flushes IP addresses from the TAP interface
  2. Restores /etc/resolv.conf if DNS was modified
  3. Waits -reconnect-delay (default 5s)
  4. Establishes a new TLS connection and repeats the full handshake
  5. Runs a fresh DHCP exchange through the new tunnel
  6. Reconfigures the TAP interface with the new lease

The TAP device itself persists across reconnections — only its IP configuration is reset. The host route to the VPN server also persists.

Routing

Server host route

Before the first connection, the client resolves the server hostname and adds a /32 host route via the current default gateway:

50.117.55.1/32 via 172.17.0.1 dev eth0

This ensures the VPN tunnel traffic itself always uses the original path, even after -accept-default-gateway installs a new default route via the VPN.

Default gateway

With -accept-default-gateway, the DHCP-provided gateway is installed as a default route with metric 50:

default via 10.100.8.1 dev vpn0 metric 50

The metric ensures it takes precedence over any existing default route with a higher metric, while the server host route (no metric, most specific) keeps the tunnel traffic on the original path.

Static routes

With -accept-static-routes, classless static routes from DHCP option 121 or 249 are installed. A 0.0.0.0/0 entry in static routes is treated as a default gateway and only installed if -accept-default-gateway is also set. Per RFC 3442, option 121 takes precedence over option 3 (Router) when present.

Keepalive

The client sends keepalive packets every 3 seconds. A keepalive is uint32(0xFFFFFFFF) + uint32(randSize) + randData. The server sends keepalives in the same format. Keepalive packets are silently consumed and never forwarded to the TAP device.

Password hashing

SoftEther uses SHA-0 (not SHA-1) for password hashing. SHA-0 differs from SHA-1 only in the message schedule — there is no left-rotate of the W[t] values.

For hashed password auth (AuthType 1):

HashedPassword = SHA0(password)
SecurePassword = SHA0(HashedPassword + ServerRandom)

For plaintext auth (AuthType 2), the password is sent as-is over the TLS connection.