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>
137 lines
7.3 KiB
Markdown
137 lines
7.3 KiB
Markdown
# 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.
|