add CLAUDE.md with project context and protocol notes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bbeeb8c41b
commit
8a3ea06c9e
1 changed files with 137 additions and 0 deletions
137
CLAUDE.md
Normal file
137
CLAUDE.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# CLAUDE.md
|
||||
|
||||
## What this is
|
||||
|
||||
Standalone SoftEther VPN client written in Go. Single binary, Linux only, ~2100 LOC. Connects to SoftEther VPN servers using the native protocol over TLS with built-in DHCP, automatic reconnection, and route management. Zero runtime deps beyond `ip` (iproute2).
|
||||
|
||||
**Repo**: `/storage/gitrepos/softether-go/`
|
||||
**Remote**: `ssh://forgejo@git.sagar.ch:2255/sagar/softether-go.git`
|
||||
|
||||
## Build & test
|
||||
|
||||
```bash
|
||||
# Build (no Go on this host, use nix-shell)
|
||||
nix-shell -p go --run "cd /storage/gitrepos/softether-go && CGO_ENABLED=0 go build -o softether-go ./cmd/softether-go/"
|
||||
|
||||
# Nix build
|
||||
nix build
|
||||
|
||||
# Docker test (staging server)
|
||||
docker run --rm --cap-add NET_ADMIN --device /dev/net/tun \
|
||||
-v /storage/gitrepos/softether-go/softether-go:/usr/bin/softether-go \
|
||||
alpine:latest sh -c '
|
||||
apk add --no-cache iproute2 curl > /dev/null 2>&1
|
||||
softether-go -host 50-117-55-1.ip.fresh.ipb.cloud -port 992 \
|
||||
-user b83dsu -pass xrXRYmG9 -plain-password -insecure -tap vpn0 \
|
||||
-accept-default-gateway -accept-dns 2>&1 &
|
||||
sleep 12 && curl -s https://ifconfig.me && echo ""
|
||||
kill %1; wait'
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
cmd/softether-go/
|
||||
main.go Flag parsing, TAP setup, reconnect loop
|
||||
session.go Session lifecycle (connect → bridge → DHCP → configure), runDHCP
|
||||
|
||||
pkg/client/
|
||||
client.go SoftEther handshake: TLS → signature → hello → auth → welcome
|
||||
tunnel.go TCP block framing, keepalive, Bridge() for bidirectional frame forwarding
|
||||
crypto.go SHA-0 (not SHA-1) for password hashing
|
||||
|
||||
pkg/protocol/
|
||||
http.go TLS connection, HTTP POST for Pack exchange
|
||||
pack.go Pack binary serialization (SoftEther's wire format)
|
||||
|
||||
pkg/dhcp/
|
||||
dhcp.go Raw Ethernet/IP/UDP DHCP client through VPN tunnel
|
||||
|
||||
pkg/netcfg/
|
||||
netcfg.go TAP config, routing (default/static/policy), DNS, server host route
|
||||
|
||||
pkg/tap/
|
||||
tap.go Linux TAP device via /dev/net/tun (IFF_TAP | IFF_NO_PI)
|
||||
```
|
||||
|
||||
## CLI flags
|
||||
|
||||
Required: `-host`, `-user`
|
||||
Optional: `-pass`, `-port` (443), `-hub` (DEFAULT), `-tap` (auto), `-mac`, `-plain-password`, `-insecure`, `-dhcp` (true), `-accept-default-gateway`, `-accept-static-routes`, `-accept-dns`, `-policy-route-table` (0=disabled), `-reconnect-delay` (5s)
|
||||
|
||||
## SoftEther protocol pitfalls
|
||||
|
||||
### Pack element name collision
|
||||
SoftEther's `AddElement` rejects case-insensitive duplicate names, and `PackRead` returns NULL on failure — **aborting the entire Pack parse**. The auth pack has `hubname` (lowercase, auth field). Do NOT also add `HubName` (node info field) — they collide. The `HubName` line is commented out in `client.go`.
|
||||
|
||||
### Node info LittleEndian32
|
||||
`OutRpcNodeInfo` wraps int fields (`ClientProductVer`, `ClientProductBuild`, `ClientPort`, `ServerPort2`, `ProxyPort`) with `LittleEndian32()` before `PackAddInt`. Our Pack writes big-endian ints, so we pre-swap these with `swapLE32()`. Without this, server logs show garbled version numbers.
|
||||
|
||||
### BufStr wire format
|
||||
`WriteBufStr`: writes `uint32(strlen+1)` then `strlen` bytes — NO null terminator on wire. The +1 in the size field accounts for a logical null, but it's not transmitted.
|
||||
|
||||
### SHA-0 password hashing
|
||||
SoftEther uses SHA-0 (withdrawn FIPS-180), NOT SHA-1. Only difference: no left-rotate in the message schedule expansion (`W[t] = W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]`, no `rol(1, ...)`).
|
||||
|
||||
### DHCPForce policy
|
||||
Server drops packets from IPs not in its DhcpAllocated table. On disconnect, server clears ALL MAC/IP entries for the session. Fresh DHCP is required on every reconnect — reusing old IP gets silently dropped.
|
||||
|
||||
### pencore padding
|
||||
HTTP-transported Packs must include a `pencore` element with random data. Without it, server may reject the Pack.
|
||||
|
||||
## Architecture decisions
|
||||
|
||||
### Bridge starts before DHCP
|
||||
The frame bridge runs concurrently with DHCP. The DHCP client intercepts server frames via `FeedFrame` callback. Non-DHCP frames go to TAP (OS drops them since TAP has no IP yet). This avoids missing DHCP responses.
|
||||
|
||||
### TAP goroutine leak on disconnect
|
||||
The TAP→Server goroutine in `Bridge()` blocks on `tapRead` (TAP fd doesn't support `SetReadDeadline`). On disconnect, it survives until the next TAP frame arrives, then `WriteFrames` fails on the closed connection and it exits. Holds 1.6 KB buffer. Not worth adding `poll(2)` complexity.
|
||||
|
||||
### Single TCP connection
|
||||
`max_connection` is set to 1. SoftEther supports multiple TCP connections for bandwidth aggregation, but this adds complexity for minimal gain on modern connections.
|
||||
|
||||
### Server host route
|
||||
Before connecting, resolves server hostname and adds `/32` route via current default gateway. Prevents routing loop when `-accept-default-gateway` installs VPN as default route.
|
||||
|
||||
### Policy routing
|
||||
`-policy-route-table N` adds `ip rule from <VPN_IP> table N` + `ip route replace default via <VPN_GW> dev <TAP> table N`. Needed when VPN server port-forwards to client — ensures reply packets go back through VPN tunnel, not default route.
|
||||
|
||||
## Performance
|
||||
|
||||
- **RAM**: 4.6 MB RSS idle, flat under 97 Mbit/s load (vs SoftEther C client: ~23 MB across 4 processes)
|
||||
- **Speed**: 97 Mbit/s down, 95 Mbit/s up, 140ms ping (single TCP, Docker, staging server)
|
||||
- **Binary**: ~7.5 MB static (CGO_ENABLED=0)
|
||||
|
||||
## Test servers
|
||||
|
||||
| Server | Host | Port | User | Pass | Auth |
|
||||
|---|---|---|---|---|---|
|
||||
| Staging (US) | 50-117-55-1.ip.fresh.ipb.cloud | 992 | b83dsu | xrXRYmG9 | plain (RADIUS) |
|
||||
| Production (India) | 65-20-68-5.ip.fresh.ipb.cloud | 992 | T0Eq5yf97gXy8Q | (empty) | plain (RADIUS) |
|
||||
|
||||
The staging user/pass was split from the original username `b83dsuxrXRYmG9` — the SoftEther server patches in softether-5 concatenate username+password when len==14.
|
||||
|
||||
## NixOS integration (pending)
|
||||
|
||||
junto host currently uses the SoftEther C client (`modules/apps/softether-client.nix`). Plan is to replace with softether-go. The C client module config for junto:
|
||||
- Instance name: `india-pub-ip`
|
||||
- MAC: `5E-3B-6F-63-A8-3E` (deterministic from sha256 of instance name)
|
||||
- Policy route table: 200
|
||||
- No default gateway (`nogateway`), no DNS override
|
||||
- dhcpcd with custom script for policy routing
|
||||
|
||||
Equivalent softether-go command:
|
||||
```
|
||||
softether-go -host 65-20-68-5.ip.fresh.ipb.cloud -port 992 \
|
||||
-user T0Eq5yf97gXy8Q -plain-password -insecure \
|
||||
-tap india-pub-ip -mac 5E:3B:6F:63:A8:3E \
|
||||
-policy-route-table 200
|
||||
```
|
||||
|
||||
To add as flake input: `softether-go.url = "git+https://git.sagar.ch/sagar/softether-go.git";`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Go 1.24+, single dep: `golang.org/x/sys` (vendored)
|
||||
- Runtime: `ip` (iproute2)
|
||||
- `vendorHash = null` in flake.nix (deps are vendored)
|
||||
Loading…
Add table
Add a link
Reference in a new issue