Compare commits
No commits in common. "9e050b140b9fba25d26491e6e4a8d9f6e01ab202" and "1d919aafc5a14bd9c70481e59449acfd11b1b5c8" have entirely different histories.
9e050b140b
...
1d919aafc5
5 changed files with 22 additions and 62 deletions
|
|
@ -57,7 +57,7 @@ pkg/tap/
|
||||||
## CLI flags
|
## CLI flags
|
||||||
|
|
||||||
Required: `-host`, `-user`
|
Required: `-host`, `-user`
|
||||||
Optional: `-pass`, `-port` (443), `-hub` (DEFAULT), `-tap` (auto), `-mac`, `-plain-password`, `-insecure`, `-no-dhcp`, `-accept-default-gateway`, `-accept-static-routes`, `-accept-dns`, `-policy-route-table` (0=disabled), `-connmark`, `-reconnect-delay-secs` (5), `-keepalive-timeout-secs` (0=disabled)
|
Optional: `-pass`, `-port` (443), `-hub` (DEFAULT), `-tap` (auto), `-mac`, `-plain-password`, `-insecure`, `-no-dhcp`, `-accept-default-gateway`, `-accept-static-routes`, `-accept-dns`, `-policy-route-table` (0=disabled), `-connmark`, `-reconnect-delay` (5s)
|
||||||
|
|
||||||
## SoftEther protocol pitfalls
|
## SoftEther protocol pitfalls
|
||||||
|
|
||||||
|
|
@ -99,9 +99,6 @@ Before connecting, resolves server hostname and adds `/32` route via current def
|
||||||
### CONNMARK for DNAT reply routing
|
### CONNMARK for DNAT reply routing
|
||||||
`-connmark` (requires `-policy-route-table`) adds iptables CONNMARK rules so DNAT'd connections (port forwards to namespaces/VMs) have replies routed back through the tunnel. Without this, replies from DNAT targets use the default route because their source IP doesn't match the `from <VPN_IP>` policy rule. CONNMARK marks incoming VPN connections and restores the mark on reply packets.
|
`-connmark` (requires `-policy-route-table`) adds iptables CONNMARK rules so DNAT'd connections (port forwards to namespaces/VMs) have replies routed back through the tunnel. Without this, replies from DNAT targets use the default route because their source IP doesn't match the `from <VPN_IP>` policy rule. CONNMARK marks incoming VPN connections and restores the mark on reply packets.
|
||||||
|
|
||||||
### Keepalive timeout watchdog
|
|
||||||
`-keepalive-timeout-secs N` enables a receive-side watchdog. The server sends keepalives every ~3s. The watchdog checks at `N/3` intervals whether any data (frames or keepalives) was received within the last N seconds. If not, it closes the connection, triggering the reconnect loop. Without this, a half-open TCP connection can go undetected for minutes (depends on kernel `tcp_retries2`). `Close()` is guarded by `sync.Once` so the watchdog, Bridge error path, and `defer tunnel.Close()` can all call it safely.
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
- **RAM**: 4.6 MB RSS idle, flat under 97 Mbit/s load (vs SoftEther C client: ~23 MB across 4 processes)
|
- **RAM**: 4.6 MB RSS idle, flat under 97 Mbit/s load (vs SoftEther C client: ~23 MB across 4 processes)
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,7 @@ softether-go [flags]
|
||||||
| `-accept-dns` | `false` | Set `/etc/resolv.conf` from DHCP-provided DNS servers |
|
| `-accept-dns` | `false` | Set `/etc/resolv.conf` from DHCP-provided DNS servers |
|
||||||
| `-policy-route-table` | `0` | Policy routing table number (0 = disabled) |
|
| `-policy-route-table` | `0` | Policy routing table number (0 = disabled) |
|
||||||
| `-connmark` | `false` | Use CONNMARK to route DNAT reply traffic back through VPN |
|
| `-connmark` | `false` | Use CONNMARK to route DNAT reply traffic back through VPN |
|
||||||
| `-keepalive-timeout-secs` | `0` | Close connection if no server data within N seconds (0 = disabled) |
|
| `-reconnect-delay` | `5s` | Delay between reconnection attempts |
|
||||||
| `-reconnect-delay-secs` | `5` | Delay between reconnection attempts in seconds |
|
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
|
|
@ -113,8 +112,6 @@ softether-go -host vpn.example.com -user admin -pass secret -plain-password
|
||||||
|
|
||||||
**`-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.
|
**`-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.
|
||||||
|
|
||||||
**`-keepalive-timeout-secs N`** — enables a receive-side keepalive watchdog. The server sends keepalives every ~3 seconds. If no data (frames or keepalives) is received within N seconds, the connection is closed and the reconnect loop kicks in. Detects half-open TCP connections that would otherwise stall for minutes. Recommended value: `15`.
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
Minimal:
|
Minimal:
|
||||||
|
|
@ -241,7 +238,7 @@ SoftEther uses **SHA-0** (not SHA-1) — no left-rotate in message schedule. `Ha
|
||||||
|
|
||||||
### Keepalive
|
### Keepalive
|
||||||
|
|
||||||
Sent every 3 seconds: `uint32(0xFFFFFFFF) + uint32(randSize) + randData`. Incoming keepalives update a receive timestamp. With `-keepalive-timeout-secs N`, a watchdog checks this timestamp at `N/3` intervals and closes the connection if no data arrived within N seconds, triggering reconnection.
|
Sent every 3 seconds: `uint32(0xFFFFFFFF) + uint32(randSize) + randData`. Silently consumed, never forwarded to TAP.
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,12 @@ func main() {
|
||||||
macAddr := flag.String("mac", "", "TAP interface MAC address (e.g. 5E:3B:6F:63:A8:3E)")
|
macAddr := flag.String("mac", "", "TAP interface MAC address (e.g. 5E:3B:6F:63:A8:3E)")
|
||||||
insecure := flag.Bool("insecure", false, "Skip TLS certificate verification")
|
insecure := flag.Bool("insecure", false, "Skip TLS certificate verification")
|
||||||
noDHCP := flag.Bool("no-dhcp", false, "Disable built-in DHCP client")
|
noDHCP := flag.Bool("no-dhcp", false, "Disable built-in DHCP client")
|
||||||
reconnectDelay := flag.Int("reconnect-delay-secs", 5, "Delay between reconnection attempts in seconds")
|
reconnectDelay := flag.Duration("reconnect-delay", 5*time.Second, "Delay between reconnection attempts")
|
||||||
acceptDefaultGW := flag.Bool("accept-default-gateway", false, "Install DHCP-provided gateway as default route")
|
acceptDefaultGW := flag.Bool("accept-default-gateway", false, "Install DHCP-provided gateway as default route")
|
||||||
acceptStaticRoutes := flag.Bool("accept-static-routes", false, "Install DHCP classless static routes (option 121/249)")
|
acceptStaticRoutes := flag.Bool("accept-static-routes", false, "Install DHCP classless static routes (option 121/249)")
|
||||||
acceptDNS := flag.Bool("accept-dns", false, "Set /etc/resolv.conf from DHCP-provided DNS servers")
|
acceptDNS := flag.Bool("accept-dns", false, "Set /etc/resolv.conf from DHCP-provided DNS servers")
|
||||||
policyRouteTable := flag.Int("policy-route-table", 0, "Policy routing table: route replies from VPN IP back through VPN gateway")
|
policyRouteTable := flag.Int("policy-route-table", 0, "Policy routing table: route replies from VPN IP back through VPN gateway")
|
||||||
connMark := flag.Bool("connmark", false, "Use CONNMARK to route DNAT reply traffic back through VPN (for port forwards to namespaces/VMs)")
|
connMark := flag.Bool("connmark", false, "Use CONNMARK to route DNAT reply traffic back through VPN (for port forwards to namespaces/VMs)")
|
||||||
keepaliveTimeout := flag.Int("keepalive-timeout-secs", 0, "Close connection if no data from server within N seconds (0 = disabled)")
|
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|
@ -100,20 +99,19 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
err := runSession(cfg, dev, mac, netOpts, time.Duration(*keepaliveTimeout)*time.Second, sig)
|
err := runSession(cfg, dev, mac, netOpts, sig)
|
||||||
if err == errShutdown {
|
if err == errShutdown {
|
||||||
log.Println("shutting down")
|
log.Println("shutting down")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("session ended: %v", err)
|
log.Printf("session ended: %v", err)
|
||||||
delay := time.Duration(*reconnectDelay) * time.Second
|
log.Printf("reconnecting in %v...", *reconnectDelay)
|
||||||
log.Printf("reconnecting in %v...", delay)
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sig:
|
case <-sig:
|
||||||
log.Println("shutting down")
|
log.Println("shutting down")
|
||||||
return
|
return
|
||||||
case <-time.After(delay):
|
case <-time.After(*reconnectDelay):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
var errShutdown = fmt.Errorf("shutdown requested")
|
var errShutdown = fmt.Errorf("shutdown requested")
|
||||||
|
|
||||||
func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts netcfg.Options, keepaliveTimeout time.Duration, sig chan os.Signal) error {
|
func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts netcfg.Options, sig chan os.Signal) error {
|
||||||
log.Printf("connecting to %s:%d hub=%s user=%s", cfg.Host, cfg.Port, cfg.Hub, cfg.Username)
|
log.Printf("connecting to %s:%d hub=%s user=%s", cfg.Host, cfg.Port, cfg.Hub, cfg.Username)
|
||||||
|
|
||||||
sess, err := client.Connect(cfg)
|
sess, err := client.Connect(cfg)
|
||||||
|
|
@ -24,7 +24,7 @@ func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts n
|
||||||
}
|
}
|
||||||
log.Printf("connected: session=%s connection=%s", sess.SessionName, sess.ConnectionName)
|
log.Printf("connected: session=%s connection=%s", sess.SessionName, sess.ConnectionName)
|
||||||
|
|
||||||
tunnel := client.NewTunnel(sess, keepaliveTimeout)
|
tunnel := client.NewTunnel(sess)
|
||||||
tunnel.StartKeepalive()
|
tunnel.StartKeepalive()
|
||||||
defer tunnel.Close()
|
defer tunnel.Close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
@ -35,21 +34,16 @@ type Tunnel struct {
|
||||||
writeErr atomic.Value // stores error from writeLoop
|
writeErr atomic.Value // stores error from writeLoop
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
stopped sync.Once
|
stopped sync.Once
|
||||||
keepaliveTimeout time.Duration // 0 = disabled
|
|
||||||
lastRecv atomic.Int64 // unix nano of last data received from server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTunnel creates a tunnel from an established session.
|
// NewTunnel creates a tunnel from an established session.
|
||||||
// keepaliveTimeout controls how long to wait for server data before closing the
|
// Call StartKeepalive() before reading/writing frames.
|
||||||
// connection (0 = disabled). Call StartKeepalive() before reading/writing frames.
|
func NewTunnel(sess *Session) *Tunnel {
|
||||||
func NewTunnel(sess *Session, keepaliveTimeout time.Duration) *Tunnel {
|
|
||||||
t := &Tunnel{
|
t := &Tunnel{
|
||||||
sess: sess,
|
sess: sess,
|
||||||
writeCh: make(chan []byte, writeChanSize),
|
writeCh: make(chan []byte, writeChanSize),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
keepaliveTimeout: keepaliveTimeout,
|
|
||||||
}
|
}
|
||||||
t.lastRecv.Store(time.Now().UnixNano())
|
|
||||||
go t.writeLoop()
|
go t.writeLoop()
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
@ -71,14 +65,10 @@ func (t *Tunnel) writeLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close stops the keepalive goroutine and closes the underlying connection.
|
// Close stops the keepalive goroutine and closes the underlying connection.
|
||||||
// Safe for concurrent/repeated calls — only the first call actually closes.
|
// The writeCh is not closed here — writeLoop exits when the connection write fails.
|
||||||
func (t *Tunnel) Close() error {
|
func (t *Tunnel) Close() error {
|
||||||
var err error
|
t.stopped.Do(func() { close(t.stopCh) })
|
||||||
t.stopped.Do(func() {
|
return t.sess.Conn.Close()
|
||||||
close(t.stopCh)
|
|
||||||
err = t.sess.Conn.Close()
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrames reads a batch of Ethernet frames from the server.
|
// ReadFrames reads a batch of Ethernet frames from the server.
|
||||||
|
|
@ -88,7 +78,6 @@ func (t *Tunnel) ReadFrames() ([][]byte, error) {
|
||||||
if err := binary.Read(t.sess.Conn, binary.BigEndian, &numBlocks); err != nil {
|
if err := binary.Read(t.sess.Conn, binary.BigEndian, &numBlocks); err != nil {
|
||||||
return nil, fmt.Errorf("read num blocks: %w", err)
|
return nil, fmt.Errorf("read num blocks: %w", err)
|
||||||
}
|
}
|
||||||
t.lastRecv.Store(time.Now().UnixNano())
|
|
||||||
|
|
||||||
// Keepalive: server sends 0xFFFFFFFF + uint32(size) + random data
|
// Keepalive: server sends 0xFFFFFFFF + uint32(size) + random data
|
||||||
if numBlocks == keepAliveMagic {
|
if numBlocks == keepAliveMagic {
|
||||||
|
|
@ -236,25 +225,4 @@ func (t *Tunnel) StartKeepalive() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Watchdog: close connection if no data received within timeout
|
|
||||||
if t.keepaliveTimeout > 0 {
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(t.keepaliveTimeout / 3)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-t.stopCh:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
last := time.Unix(0, t.lastRecv.Load())
|
|
||||||
if time.Since(last) > t.keepaliveTimeout {
|
|
||||||
log.Printf("keepalive timeout: no data from server for %v, closing connection", t.keepaliveTimeout)
|
|
||||||
t.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue