softether-go/docs/architecture.md
Git Sagar 17c1063e1f refactor: extract session/netcfg/tunnel, add mac/dhcp/policy-route flags
- Split cmd/softether-go into main.go (flags, reconnect loop) and
  session.go (session lifecycle, DHCP orchestration)
- Extract network config to pkg/netcfg (TAP config, routing, DNS, policy routes)
- Move frame bridging to pkg/client/tunnel.go as Bridge() method
- Add -mac, -dhcp, -policy-route-table CLI flags
- Add SetMAC() to pkg/tap for deterministic DHCP assignments
- Update all docs to reflect new structure and flags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 16:43:12 +05:30

149 lines
8.2 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
## Session lifecycle
After connecting, a session proceeds as:
1. **Start bridge** — bidirectional frame forwarding between the tunnel and TAP device begins immediately in the background
2. **DHCP exchange** — if enabled, DHCP frames are sent through the tunnel while the bridge is already running. The DHCP client intercepts server frames via a callback (`FeedFrame`) before they reach the TAP device
3. **Configure TAP** — IP address, routes, DNS, and policy routing are applied from the DHCP lease
4. **Wait** — the session blocks until the bridge errors (server disconnect, network failure) or a signal arrives
5. **Cleanup** — TAP addresses are flushed, DNS is restored, policy routes are removed
On disconnect, the reconnect loop waits and starts a new session with a fresh handshake and DHCP exchange.
## 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 frame bridge runs concurrently with the DHCP exchange. All server frames pass through the `FeedFrame` callback, which identifies DHCP responses by transaction ID. Non-DHCP frames are written to the TAP device as normal (though the TAP has no IP yet, so the OS drops them).
### 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.
## 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.
### Policy routing
With `-policy-route-table N`, the client sets up policy routing for asymmetric return paths:
```
ip route replace default via <VPN_GW> dev <TAP> table N
ip rule add from <VPN_IP> table N
```
This is needed when the VPN server has port forwards to the client. Without policy routing, inbound traffic arrives via the VPN tunnel but reply packets use the default route (home router) instead of going back through the tunnel. The remote host sees replies from a different IP and drops them.
The policy route is cleaned up on disconnect and re-applied with each new DHCP lease (since the VPN IP may change).
## 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.