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>
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
-
TLS connect — standard TLS to the server port (typically 443 or 992). The connection looks like normal HTTPS traffic to network observers.
-
Upload signature — the client sends
VPNCONNECTas the body of an HTTP POST to/vpnsvc/connect.cgi. The server validates this to confirm the client speaks the SoftEther protocol. -
Download Hello — the server responds with a binary Pack containing its version info and a 20-byte random challenge used for password hashing.
-
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. -
Download Welcome — the server responds with session info (session name, connection name, session key, policies) on success, or an error code on failure.
-
Tunnel mode — the connection switches from HTTP to raw TCP block framing. Each message is
uint32(numBlocks) + [uint32(size) + data].... Keepalive packets use0xFFFFFFFFas 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) AddElementrejects duplicate names — the second add silently fails- String values use BufStr encoding:
uint32(strlen+1)followed bystrlenbytes (no null terminator on wire) - HTTP-transported Packs include a
pencoredummy 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:
- Flushes IP addresses from the TAP interface
- Restores
/etc/resolv.confif DNS was modified - Waits
-reconnect-delay(default 5s) - Establishes a new TLS connection and repeats the full handshake
- Runs a fresh DHCP exchange through the new tunnel
- 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.