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>
This commit is contained in:
Git Sagar 2026-06-06 16:13:51 +05:30
commit 829ca73b1b
340 changed files with 199140 additions and 0 deletions

137
docs/architecture.md Normal file
View file

@ -0,0 +1,137 @@
# 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.