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:
Git Sagar 2026-06-06 16:13:51 +05:30
commit 829ca73b1b
340 changed files with 199140 additions and 0 deletions

129
pkg/client/tunnel.go Normal file
View file

@ -0,0 +1,129 @@
package client
import (
"encoding/binary"
"fmt"
"io"
"math/rand"
"sync"
"time"
)
// TCP block framing constants.
// After the HTTP handshake completes, SoftEther switches to raw TCP framing:
//
// Send: uint32(numBlocks) + [uint32(blockSize) + blockData]...
// Recv: same format
// Keepalive: uint32(0xFFFFFFFF) + uint32(randSize) + randData
//
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1654 (TcpSockRecv)
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1761 (TcpSockSend)
const (
keepAliveMagic uint32 = 0xFFFFFFFF // Magic value indicating a keepalive packet
maxKeepaliveSize uint32 = 512 // Max random data in keepalive
keepAliveInterval = 3 * time.Second
)
// Tunnel handles bidirectional TCP block framing for Ethernet frames over a
// SoftEther VPN session. Each "block" is one Ethernet frame.
type Tunnel struct {
sess *Session
stopCh chan struct{}
stopped sync.Once
}
// NewTunnel creates a tunnel from an established session.
// Call StartKeepalive() before reading/writing frames.
func NewTunnel(sess *Session) *Tunnel {
return &Tunnel{
sess: sess,
stopCh: make(chan struct{}),
}
}
// Close stops the keepalive goroutine and closes the underlying connection.
func (t *Tunnel) Close() error {
t.stopped.Do(func() { close(t.stopCh) })
return t.sess.Conn.Close()
}
// ReadFrames reads a batch of Ethernet frames from the server.
// Returns nil (no error) for keepalive packets. Blocks until data arrives.
func (t *Tunnel) ReadFrames() ([][]byte, error) {
var numBlocks uint32
if err := binary.Read(t.sess.Conn, binary.BigEndian, &numBlocks); err != nil {
return nil, fmt.Errorf("read num blocks: %w", err)
}
// Keepalive: server sends 0xFFFFFFFF + uint32(size) + random data
if numBlocks == keepAliveMagic {
var size uint32
if err := binary.Read(t.sess.Conn, binary.BigEndian, &size); err != nil {
return nil, fmt.Errorf("read keepalive size: %w", err)
}
if _, err := io.CopyN(io.Discard, t.sess.Conn, int64(size)); err != nil {
return nil, fmt.Errorf("discard keepalive: %w", err)
}
return nil, nil
}
frames := make([][]byte, 0, numBlocks)
for i := uint32(0); i < numBlocks; i++ {
var size uint32
if err := binary.Read(t.sess.Conn, binary.BigEndian, &size); err != nil {
return nil, fmt.Errorf("read block size: %w", err)
}
buf := make([]byte, size)
if _, err := io.ReadFull(t.sess.Conn, buf); err != nil {
return nil, fmt.Errorf("read block data: %w", err)
}
frames = append(frames, buf)
}
return frames, nil
}
// WriteFrames sends a batch of Ethernet frames to the server.
func (t *Tunnel) WriteFrames(frames [][]byte) error {
if err := binary.Write(t.sess.Conn, binary.BigEndian, uint32(len(frames))); err != nil {
return fmt.Errorf("write num blocks: %w", err)
}
for _, frame := range frames {
if err := binary.Write(t.sess.Conn, binary.BigEndian, uint32(len(frame))); err != nil {
return fmt.Errorf("write block size: %w", err)
}
if _, err := t.sess.Conn.Write(frame); err != nil {
return fmt.Errorf("write block data: %w", err)
}
}
return nil
}
// StartKeepalive sends periodic keepalive packets to prevent the server from
// timing out the connection. Must be called after the session enters tunnel mode.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1779
func (t *Tunnel) StartKeepalive() {
go func() {
ticker := time.NewTicker(keepAliveInterval)
defer ticker.Stop()
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
select {
case <-t.stopCh:
return
case <-ticker.C:
size := uint32(rng.Intn(int(maxKeepaliveSize))) + 1
if err := binary.Write(t.sess.Conn, binary.BigEndian, keepAliveMagic); err != nil {
return
}
if err := binary.Write(t.sess.Conn, binary.BigEndian, size); err != nil {
return
}
randData := make([]byte, size)
rng.Read(randData)
if _, err := t.sess.Conn.Write(randData); err != nil {
return
}
}
}
}()
}