softether-go/README.md
Git Sagar 61237283f5 consolidate docs into README.md, remove docs/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 17:54:38 +05:30

263 lines
10 KiB
Markdown

# softether-go
Standalone SoftEther VPN client written in Go. Connects to SoftEther VPN servers using the native protocol over TLS, with built-in DHCP, automatic reconnection, and route management. Single static binary, Linux only, zero runtime dependencies beyond `ip` (iproute2).
## Features
- Native SoftEther protocol (TLS + HTTP handshake + TCP block framing)
- Built-in DHCP client (raw Ethernet frame construction through the VPN tunnel)
- Automatic reconnection with fresh DHCP on each reconnect
- Host route to VPN server via existing default gateway (prevents routing loops)
- Classless static routes (DHCP option 121/249, RFC 3442)
- Policy routing for asymmetric return paths (VPN port forwards)
- DNS configuration from DHCP lease (backup/restore of `/etc/resolv.conf`)
- Deterministic MAC address support for stable DHCP assignments
- Hashed password (SHA-0) and plaintext password (RADIUS/external) authentication
- ~4.6 MB RAM under load, single process
## Requirements
- Linux (uses `/dev/net/tun` for TAP devices)
- `CAP_NET_ADMIN` or root (TAP device creation, route management)
- `ip` command (iproute2) on `$PATH`
## Building
Requires Go 1.24 or later.
```bash
go build -o softether-go ./cmd/softether-go/
```
Static binary (for Alpine, scratch containers):
```bash
CGO_ENABLED=0 go build -o softether-go ./cmd/softether-go/
```
Cross-compilation:
```bash
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o softether-go ./cmd/softether-go/
```
Nix:
```bash
nix build # result in ./result/bin/softether-go
nix develop # dev shell with Go tooling
```
The only Go dependency is `golang.org/x/sys` (vendored in `vendor/`).
## Usage
```
softether-go [flags]
```
### Required flags
| Flag | Description |
|---|---|
| `-host` | SoftEther server hostname or IP |
| `-user` | Authentication username |
### Optional flags
| Flag | Default | Description |
|---|---|---|
| `-pass` | `""` | Authentication password |
| `-port` | `443` | Server port |
| `-hub` | `DEFAULT` | Virtual hub name |
| `-tap` | *(auto)* | TAP interface name (kernel-assigned if empty) |
| `-mac` | *(auto)* | TAP interface MAC address (e.g. `5E:3B:6F:63:A8:3E`) |
| `-plain-password` | `false` | Send password as plaintext (AuthType 2, for RADIUS/external auth) |
| `-insecure` | `false` | Skip TLS certificate verification |
| `-no-dhcp` | `false` | Disable built-in DHCP client |
| `-accept-default-gateway` | `false` | Install DHCP-provided gateway as default route |
| `-accept-static-routes` | `false` | Install DHCP classless static routes (option 121/249) |
| `-accept-dns` | `false` | Set `/etc/resolv.conf` from DHCP-provided DNS servers |
| `-policy-route-table` | `0` | Policy routing table number (0 = disabled) |
| `-reconnect-delay` | `5s` | Delay between reconnection attempts |
### Authentication
**Hashed password (AuthType 1)** — the default. Password is hashed with SHA-0 and combined with the server's random challenge. Used for local user accounts.
```bash
softether-go -host vpn.example.com -user admin -pass secret
```
**Plaintext password (AuthType 2)** — enabled with `-plain-password`. Password sent as-is over TLS. Used for RADIUS/external auth.
```bash
softether-go -host vpn.example.com -user admin -pass secret -plain-password
```
### Network configuration
**`-mac`** — sets a specific MAC for deterministic DHCP assignments across reconnects.
**`-accept-default-gateway`** — installs DHCP gateway as default route (metric 50). A `/32` host route to the VPN server via the original gateway prevents routing loops.
**`-accept-static-routes`** — installs classless static routes from DHCP option 121/249. A `0.0.0.0/0` entry is only installed if `-accept-default-gateway` is also set.
**`-accept-dns`** — overwrites `/etc/resolv.conf` with DHCP DNS servers. Original is backed up and restored on disconnect.
**`-policy-route-table N`** — policy routing for asymmetric return paths. Adds `ip rule from <VPN_IP> table N` and `ip route replace default via <VPN_GW> dev <TAP> table N`. Needed when the VPN server forwards ports to the client.
### Examples
Minimal:
```bash
softether-go -host vpn.example.com -user admin -pass secret
```
Full setup:
```bash
softether-go \
-host vpn.example.com \
-port 992 \
-user admin \
-pass secret \
-plain-password \
-tap vpn0 \
-mac 5E:3B:6F:63:A8:3E \
-insecure \
-accept-default-gateway \
-accept-static-routes \
-accept-dns \
-policy-route-table 200
```
No DHCP (manual config):
```bash
softether-go -host vpn.example.com -user admin -pass secret -no-dhcp -tap vpn0
```
### Docker
```bash
docker run --rm -it \
--cap-add NET_ADMIN \
--device /dev/net/tun \
-v ./softether-go:/usr/bin/softether-go \
alpine \
softether-go -host vpn.example.com -user admin -pass secret \
-plain-password -insecure -tap vpn0 \
-accept-default-gateway -accept-dns
```
Container needs `iproute2` (`apk add iproute2` on Alpine).
### Signals
- **SIGINT / SIGTERM** — clean shutdown: closes tunnel, flushes TAP, restores DNS, removes routes
- During reconnect delay, signal triggers immediate shutdown
## Architecture
### Connection flow
```
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
│ │
```
1. **TLS connect** — standard TLS to the server port (typically 443 or 992)
2. **Upload signature**`VPNCONNECT` via HTTP POST to `/vpnsvc/connect.cgi`
3. **Download Hello** — server sends version info and 20-byte random challenge
4. **Upload Auth** — credentials, client version, connection options, node info as a Pack
5. **Download Welcome** — session info on success, error code on failure
6. **Tunnel mode** — raw TCP block framing: `uint32(numBlocks) + [uint32(size) + data]...`. Keepalive: `uint32(0xFFFFFFFF) + uint32(randSize) + randData`
### Session lifecycle
1. **Start bridge** — bidirectional frame forwarding begins immediately
2. **DHCP exchange** — DHCP frames sent through tunnel while bridge runs. DHCP client intercepts server frames via `FeedFrame` callback
3. **Configure TAP** — IP, routes, DNS, policy routing applied from lease
4. **Wait** — blocks until bridge errors or signal arrives
5. **Cleanup** — flush addresses, restore DNS, remove policy routes
6. **Reconnect** — wait, then repeat with fresh handshake and DHCP
### DHCP through the tunnel
SoftEther operates at Layer 2. The built-in DHCP client constructs complete Ethernet/IP/UDP/DHCP frames sent through the tunnel:
```
DHCP Client VPN Tunnel DHCP Server (on VPN)
│ │ │
├── DISCOVER ──► WriteFrames ├────────────────────────────►│
│ │ │
│◄── FeedFrame ◄─ ReadFrames ◄──────── OFFER ─────────────┤
│ │ │
├── REQUEST ───► WriteFrames ├────────────────────────────►│
│ │ │
│◄── FeedFrame ◄─ ReadFrames ◄──────── ACK ───────────────┤
│ │ │
```
DHCP options requested: subnet mask (1), router (3), DNS (6), lease time (51), classless static routes (121/249).
**Why fresh DHCP on reconnect**: SoftEther's `DHCPForce` policy drops packets from IPs not in its `DhcpAllocated` table. On disconnect, the server clears all entries for the session.
### Routing
**Server host route**`/32` route to VPN server via current default gateway, added before first connection. Prevents routing loop when VPN becomes default route.
**Default gateway** — DHCP gateway installed with metric 50, lower than existing default routes.
**Static routes** — classless routes from option 121/249. Default route entries (`0.0.0.0/0`) only installed with `-accept-default-gateway`.
**Policy routing**`ip rule from <VPN_IP> table N` ensures reply packets for VPN port forwards go back through the tunnel, not the default route.
### Password hashing
SoftEther uses **SHA-0** (not SHA-1) — no left-rotate in message schedule. `HashedPassword = SHA0(password)`, `SecurePassword = SHA0(HashedPassword + ServerRandom)`. Plaintext auth (AuthType 2) sends password as-is over TLS.
### Keepalive
Sent every 3 seconds: `uint32(0xFFFFFFFF) + uint32(randSize) + randData`. Silently consumed, never forwarded to TAP.
## Project structure
```
cmd/softether-go/
main.go Flag parsing, TAP setup, reconnect loop
session.go Session lifecycle, DHCP orchestration
pkg/client/
client.go SoftEther handshake and session
tunnel.go TCP block framing, keepalive, frame bridging
crypto.go SHA-0 and password hashing
pkg/protocol/
http.go TLS connection, HTTP transport layer
pack.go Pack binary serialization
pkg/dhcp/
dhcp.go DHCP client (raw Ethernet frames)
pkg/netcfg/
netcfg.go TAP configuration, routing, DNS management
pkg/tap/
tap.go Linux TAP device management
```
## License
MIT