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:
commit
829ca73b1b
340 changed files with 199140 additions and 0 deletions
137
docs/architecture.md
Normal file
137
docs/architecture.md
Normal 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.
|
||||
54
docs/building.md
Normal file
54
docs/building.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Building
|
||||
|
||||
## Go
|
||||
|
||||
Requires Go 1.24 or later.
|
||||
|
||||
```bash
|
||||
go build -o softether-go ./cmd/softether-go/
|
||||
```
|
||||
|
||||
### Static binary
|
||||
|
||||
For deployment in minimal containers (Alpine, scratch) or systems without glibc:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 go build -o softether-go ./cmd/softether-go/
|
||||
```
|
||||
|
||||
### Cross-compilation
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o softether-go ./cmd/softether-go/
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o softether-go ./cmd/softether-go/
|
||||
```
|
||||
|
||||
Only Linux targets are supported (the TAP device interface uses Linux-specific ioctls).
|
||||
|
||||
## Nix
|
||||
|
||||
Build with the flake:
|
||||
|
||||
```bash
|
||||
nix build
|
||||
```
|
||||
|
||||
The result is in `./result/bin/softether-go`.
|
||||
|
||||
Enter a development shell with Go tooling:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
Build within the dev shell:
|
||||
|
||||
```bash
|
||||
nix develop -c bash -c 'go build -o softether-go ./cmd/softether-go/'
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
The only Go dependency is `golang.org/x/sys` for Linux syscall constants (TUN/TAP ioctls). Dependencies are vendored in `vendor/`.
|
||||
|
||||
At runtime, the only external dependency is `ip` (iproute2) for route and address management.
|
||||
27
docs/main.md
Normal file
27
docs/main.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# softether-go documentation
|
||||
|
||||
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.
|
||||
|
||||
## 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)
|
||||
- DNS configuration from DHCP lease (backup/restore of `/etc/resolv.conf`)
|
||||
- Hashed password (SHA-0) and plaintext password (RADIUS/external) authentication
|
||||
- Single static binary, Linux only
|
||||
|
||||
## Contents
|
||||
|
||||
- [Usage & CLI reference](usage.md) — flags, examples, Docker usage
|
||||
- [Architecture](architecture.md) — connection flow, DHCP, reconnection, routing
|
||||
- [Building](building.md) — Go, Nix, static builds
|
||||
- [Project structure](structure.md) — source layout and package descriptions
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux (uses `/dev/net/tun` for TAP devices)
|
||||
- `CAP_NET_ADMIN` or root (TAP device creation, route management)
|
||||
- `ip` command (iproute2) on `$PATH`
|
||||
57
docs/structure.md
Normal file
57
docs/structure.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Project structure
|
||||
|
||||
```
|
||||
softether-go/
|
||||
├── cmd/softether-go/
|
||||
│ └── main.go CLI entry point
|
||||
├── pkg/
|
||||
│ ├── client/
|
||||
│ │ ├── client.go SoftEther handshake and session
|
||||
│ │ ├── tunnel.go TCP block framing and keepalive
|
||||
│ │ └── crypto.go SHA-0 and password hashing
|
||||
│ ├── protocol/
|
||||
│ │ ├── http.go HTTP transport layer
|
||||
│ │ └── pack.go Pack binary serialization
|
||||
│ ├── dhcp/
|
||||
│ │ └── dhcp.go DHCP client (raw Ethernet frames)
|
||||
│ └── tap/
|
||||
│ └── tap.go Linux TAP device management
|
||||
├── docs/ Documentation
|
||||
├── vendor/ Vendored Go dependencies
|
||||
├── flake.nix Nix build definition
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
## Package details
|
||||
|
||||
### `cmd/softether-go`
|
||||
|
||||
CLI entry point. Handles flag parsing, signal handling, and the reconnection loop. On each session:
|
||||
- Connects to the server
|
||||
- Runs DHCP through the tunnel
|
||||
- Configures the TAP interface (IP, routes, DNS)
|
||||
- Bridges Ethernet frames between the TAP device and the VPN tunnel
|
||||
- Cleans up on disconnect and retries
|
||||
|
||||
### `pkg/client`
|
||||
|
||||
**`client.go`** — implements the SoftEther handshake: TLS connect, signature upload, hello/auth/welcome pack exchange. Exports `Connect(Config) (*Session, error)` and the `Config`/`Session` types.
|
||||
|
||||
**`tunnel.go`** — TCP block framing after the HTTP handshake. `ReadFrames()` reads batches of Ethernet frames from the server. `WriteFrames()` sends batches. `StartKeepalive()` sends periodic keepalive packets (every 3s) to prevent server timeout.
|
||||
|
||||
**`crypto.go`** — SHA-0 implementation (differs from SHA-1 only in the message schedule — no left-rotate). `HashPassword()` produces `SHA0(password)`. `SecurePassword()` produces `SHA0(hashed + serverRandom)`.
|
||||
|
||||
### `pkg/protocol`
|
||||
|
||||
**`http.go`** — HTTP transport layer. `DialTLS()` establishes the TLS connection. `UploadSignature()` sends the protocol signature. `SendPack()` and `RecvPack()` exchange binary Packs as HTTP POST request/response bodies.
|
||||
|
||||
**`pack.go`** — SoftEther Pack binary serialization. A Pack is a list of named Elements, each containing typed Values (int, string, data, ip4). Handles the BufStr wire format (`uint32(strlen+1)` then `strlen` bytes) and the `pencore` random padding element.
|
||||
|
||||
### `pkg/dhcp`
|
||||
|
||||
**`dhcp.go`** — DHCP client that constructs complete Ethernet/IP/UDP/DHCP frames. The full DHCP exchange (DISCOVER → OFFER → REQUEST → ACK) runs through the VPN tunnel's frame transport. Parses lease information including classless static routes (option 121/249, RFC 3442).
|
||||
|
||||
### `pkg/tap`
|
||||
|
||||
**`tap.go`** — Linux TAP (Layer 2) device management via `/dev/net/tun`. Opens TAP devices with `IFF_TAP | IFF_NO_PI`, reads/writes raw Ethernet frames. Provides `MAC()` to get the hardware address and `SetUp()` to bring the interface up.
|
||||
118
docs/usage.md
Normal file
118
docs/usage.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# 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) |
|
||||
| `-plain-password` | `false` | Send password as plaintext (AuthType 2, for RADIUS/external auth) |
|
||||
| `-insecure` | `false` | Skip TLS certificate verification |
|
||||
| `-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 |
|
||||
| `-reconnect-delay` | `5s` | Delay between reconnection attempts |
|
||||
|
||||
## Authentication
|
||||
|
||||
Two authentication modes are supported:
|
||||
|
||||
**Hashed password (AuthType 1)** — the default. Password is hashed with SHA-0 and combined with the server's random challenge to produce a `SecurePassword`. Used for local user accounts on the SoftEther server.
|
||||
|
||||
```bash
|
||||
softether-go -host vpn.example.com -user admin -pass secret
|
||||
```
|
||||
|
||||
**Plaintext password (AuthType 2)** — enabled with `-plain-password`. Password is sent as-is over TLS. Used when the server delegates authentication to an external system like RADIUS.
|
||||
|
||||
```bash
|
||||
softether-go -host vpn.example.com -user admin -pass secret -plain-password
|
||||
```
|
||||
|
||||
## Network configuration flags
|
||||
|
||||
These flags control what the client does with the DHCP lease it receives from the VPN server.
|
||||
|
||||
### `-accept-default-gateway`
|
||||
|
||||
Adds a default route via the DHCP-provided gateway on the TAP interface with metric 50. Before doing this, the client adds a `/32` host route to the VPN server via the current default gateway so the tunnel itself is not routed through the VPN.
|
||||
|
||||
Without this flag, only the subnet route (implicit from the assigned IP/mask) is added.
|
||||
|
||||
### `-accept-static-routes`
|
||||
|
||||
Installs classless static routes from DHCP option 121 (RFC 3442) or option 249 (Microsoft variant). These are non-default routes pushed by the DHCP server, such as routes to specific subnets via the VPN gateway.
|
||||
|
||||
If a static route entry has destination `0.0.0.0/0` (default route), it is only installed when `-accept-default-gateway` is also set. Per RFC 3442, when option 121 is present it takes precedence over option 3 (Router).
|
||||
|
||||
### `-accept-dns`
|
||||
|
||||
Overwrites `/etc/resolv.conf` with the DNS servers from the DHCP lease. The original file is backed up in memory and restored when the session ends (disconnect, reconnect, or shutdown).
|
||||
|
||||
## Examples
|
||||
|
||||
Minimal connection:
|
||||
```bash
|
||||
softether-go -host vpn.example.com -user admin -pass secret
|
||||
```
|
||||
|
||||
Full setup with routing and DNS:
|
||||
```bash
|
||||
softether-go \
|
||||
-host vpn.example.com \
|
||||
-port 992 \
|
||||
-hub DEFAULT \
|
||||
-user admin \
|
||||
-pass secret \
|
||||
-plain-password \
|
||||
-tap vpn0 \
|
||||
-insecure \
|
||||
-accept-default-gateway \
|
||||
-accept-static-routes \
|
||||
-accept-dns
|
||||
```
|
||||
|
||||
Named TAP interface with custom reconnect delay:
|
||||
```bash
|
||||
softether-go \
|
||||
-host vpn.example.com \
|
||||
-user admin \
|
||||
-pass secret \
|
||||
-tap myvpn \
|
||||
-reconnect-delay 10s
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
The client works in containers with `NET_ADMIN` capability and the TUN device:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
The container needs `iproute2` installed (`apk add iproute2` on Alpine) for the `ip` command.
|
||||
|
||||
## Signals
|
||||
|
||||
- **SIGINT / SIGTERM** — clean shutdown: closes tunnel, flushes TAP addresses, restores DNS, removes server host route
|
||||
- During reconnect delay, a signal triggers immediate shutdown instead of waiting
|
||||
Loading…
Add table
Add a link
Reference in a new issue