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
129
pkg/client/tunnel.go
Normal file
129
pkg/client/tunnel.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue