No description
Find a file
Git Sagar 51824b830e netcfg: add -connmark flag for DNAT reply routing
When VPN traffic is DNAT'd to local namespaces/VMs, reply packets have
a different source IP (namespace veth) so the policy route's
"from <VPN_IP>" rule doesn't match. CONNMARK marks all connections
arriving on the VPN interface and restores the mark on reply packets,
routing them back through the tunnel via fwmark rule.

New flag: -connmark (requires -policy-route-table)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 01:06:17 +05:30
cmd/softether-go netcfg: add -connmark flag for DNAT reply routing 2026-06-07 01:06:17 +05:30
pkg netcfg: add -connmark flag for DNAT reply routing 2026-06-07 01:06:17 +05:30
vendor initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
.gitignore add .gitignore for build artifacts 2026-06-06 16:15:08 +05:30
CLAUDE.md netcfg: add -connmark flag for DNAT reply routing 2026-06-07 01:06:17 +05:30
flake.lock initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
flake.nix initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
go.mod initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
go.sum initial commit: standalone SoftEther VPN client in Go 2026-06-06 16:13:51 +05:30
README.md netcfg: add -connmark flag for DNAT reply routing 2026-06-07 01:06:17 +05:30

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)
  • CONNMARK-based DNAT reply routing (for port forwards to namespaces/VMs)
  • 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
  • iptables on $PATH (only if using -connmark)

Building

Requires Go 1.24 or later.

go build -o softether-go ./cmd/softether-go/

Static binary (for Alpine, scratch containers):

CGO_ENABLED=0 go build -o softether-go ./cmd/softether-go/

Cross-compilation:

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o softether-go ./cmd/softether-go/

Nix:

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)
-connmark false Use CONNMARK to route DNAT reply traffic back through VPN
-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.

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.

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.

-connmark — requires -policy-route-table. Uses iptables CONNMARK to route DNAT reply traffic back through the VPN tunnel. Without this, traffic forwarded to local namespaces/VMs (via DNAT) gets replies routed via the default gateway instead of the tunnel, breaking the connection. Adds CONNMARK --set-mark on incoming VPN packets and CONNMARK --restore-mark on reply packets from other interfaces.

Examples

Minimal:

softether-go -host vpn.example.com -user admin -pass secret

Full setup:

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 \
  -connmark

No DHCP (manual config):

softether-go -host vpn.example.com -user admin -pass secret -no-dhcp -tap vpn0

Docker

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 signatureVPNCONNECT 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 routingip rule from <VPN_IP> table N ensures reply packets for VPN port forwards go back through the tunnel, not the default route.

CONNMARK (-connmark) — solves a subtler routing problem: when VPN traffic is DNAT'd to a local namespace or VM, the reply packets have a different source IP (the namespace veth) so the from <VPN_IP> rule doesn't match. CONNMARK marks all connections arriving on the VPN interface, then restores the mark on reply packets from any interface, routing them back through the tunnel via fwmark rule.

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