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
236
pkg/client/client.go
Normal file
236
pkg/client/client.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
// Package client implements the SoftEther VPN client handshake and session management.
|
||||
//
|
||||
// The connection flow follows the SoftEther protocol:
|
||||
// 1. TLS connect (standard TLS, looks like HTTPS)
|
||||
// 2. Upload signature ("VPNCONNECT" string via HTTP POST)
|
||||
// 3. Download Hello (server sends version info + random challenge)
|
||||
// 4. Upload Auth (client sends credentials + node info via HTTP POST)
|
||||
// 5. Download Welcome (server sends session info if auth succeeds)
|
||||
// 6. Enter tunneling mode (raw TCP block framing, no more HTTP)
|
||||
//
|
||||
// Reference: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7145 (ClientConnectToServer flow)
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"git.sagar.ch/sagar/softether-go/pkg/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
clientStr = "SoftEther VPN Client"
|
||||
clientVer = 502
|
||||
clientBuild = 5187
|
||||
|
||||
// Auth types matching SoftEther's CLIENT_AUTHTYPE_* constants.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Cedar.h#L554
|
||||
authTypePassword uint32 = 1
|
||||
authTypePlainPassword uint32 = 2
|
||||
)
|
||||
|
||||
// Config holds the connection parameters for a SoftEther VPN session.
|
||||
type Config struct {
|
||||
Host string // Server hostname or IP
|
||||
Port int // Server port (default: 443)
|
||||
Hub string // Virtual hub name
|
||||
Username string // Authentication username
|
||||
Password string // Password (hashed or sent plain depending on PlainPassword)
|
||||
PlainPassword bool // If true, send password as plaintext (AuthType 2)
|
||||
InsecureSkipVerify bool // Skip TLS certificate verification
|
||||
}
|
||||
|
||||
// Session represents an established SoftEther VPN session.
|
||||
// After Connect() returns, the underlying Conn is ready for TCP block framing.
|
||||
type Session struct {
|
||||
Conn *protocol.Conn
|
||||
SessionKey [sha1Size]byte // 20-byte session key from server
|
||||
SessionKey32 uint32 // 32-bit session key
|
||||
SessionName string // Server-assigned session name (e.g. "SID-ADMIN-1")
|
||||
ConnectionName string // Server-assigned connection name
|
||||
}
|
||||
|
||||
// Connect performs the full SoftEther handshake and returns an established session.
|
||||
// The returned Session's Conn is ready for TCP block framing (tunnel mode).
|
||||
func Connect(cfg Config) (*Session, error) {
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 443
|
||||
}
|
||||
|
||||
// Step 1: TLS connection
|
||||
conn, err := protocol.DialTLS(cfg.Host, cfg.Port, cfg.InsecureSkipVerify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Upload signature
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7208
|
||||
if err := protocol.UploadSignature(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("signature: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Download Hello — server sends version info and a random challenge
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7250 (ClientDownloadHello)
|
||||
helloPack, err := protocol.RecvPack(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("hello recv: %w", err)
|
||||
}
|
||||
if e := helloPack.GetError(); e != 0 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("hello error: %d", e)
|
||||
}
|
||||
|
||||
serverRandom := helloPack.GetData("random")
|
||||
if len(serverRandom) != sha1Size {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("invalid server random size: %d", len(serverRandom))
|
||||
}
|
||||
var random [sha1Size]byte
|
||||
copy(random[:], serverRandom)
|
||||
|
||||
// Step 4: Upload Auth — send credentials and client metadata
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7289 (ClientUploadAuth)
|
||||
authPack := buildAuthPack(cfg, random)
|
||||
|
||||
if err := protocol.SendPack(conn, authPack); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("auth send: %w", err)
|
||||
}
|
||||
|
||||
// Step 5: Download Welcome — server sends session info if auth succeeded
|
||||
welcomePack, err := protocol.RecvPack(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("welcome recv: %w", err)
|
||||
}
|
||||
|
||||
if e := welcomePack.GetError(); e != 0 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("auth failed, error code: %d", e)
|
||||
}
|
||||
|
||||
sess := &Session{
|
||||
Conn: conn,
|
||||
SessionName: welcomePack.GetStr("session_name"),
|
||||
ConnectionName: welcomePack.GetStr("connection_name"),
|
||||
}
|
||||
|
||||
sessionKeyData := welcomePack.GetData("session_key")
|
||||
if len(sessionKeyData) == sha1Size {
|
||||
copy(sess.SessionKey[:], sessionKeyData)
|
||||
}
|
||||
sess.SessionKey32 = welcomePack.GetInt("session_key_32")
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
// buildAuthPack constructs the authentication Pack sent to the server.
|
||||
// This includes credentials, client version, connection options, node info, and OS info.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7289
|
||||
func buildAuthPack(cfg Config, random [sha1Size]byte) *protocol.Pack {
|
||||
p := &protocol.Pack{}
|
||||
|
||||
// Login method and credentials
|
||||
p.AddStr("method", "login")
|
||||
p.AddStr("hubname", cfg.Hub)
|
||||
p.AddStr("username", cfg.Username)
|
||||
|
||||
if cfg.PlainPassword {
|
||||
// Plain password auth (AuthType 2): password sent as-is over TLS
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L5548
|
||||
p.AddInt("authtype", authTypePlainPassword)
|
||||
p.AddStr("plain_password", cfg.Password)
|
||||
} else {
|
||||
// Hashed password auth (AuthType 1): SecurePassword = SHA0(SHA0(password) + server_random)
|
||||
hashedPw := HashPassword(cfg.Password)
|
||||
securePw := SecurePassword(hashedPw, random)
|
||||
p.AddInt("authtype", authTypePassword)
|
||||
p.AddData("secure_password", securePw[:])
|
||||
}
|
||||
|
||||
// Client version info
|
||||
p.AddStr("client_str", clientStr)
|
||||
p.AddInt("client_ver", clientVer)
|
||||
p.AddInt("client_build", clientBuild)
|
||||
|
||||
// Protocol: 0 = TCP (CONNECTION_TCP), 1 = UDP
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Cedar.h#L522
|
||||
p.AddInt("protocol", 0)
|
||||
|
||||
// Hello exchange fields
|
||||
p.AddStr("hello", clientStr)
|
||||
p.AddInt("version", clientVer)
|
||||
p.AddInt("build", clientBuild)
|
||||
p.AddInt("client_id", 0)
|
||||
|
||||
// Connection options
|
||||
p.AddInt("max_connection", 1)
|
||||
p.AddBool("use_encrypt", true)
|
||||
p.AddBool("use_compress", false)
|
||||
p.AddBool("half_connection", false)
|
||||
p.AddBool("require_bridge_routing_mode", false)
|
||||
p.AddBool("require_monitor_mode", false)
|
||||
p.AddBool("qos", true)
|
||||
p.AddBool("support_bulk_on_rudp", true)
|
||||
p.AddBool("support_hmac_on_bulk_of_rudp", true)
|
||||
p.AddBool("support_udp_recovery", true)
|
||||
p.AddInt("rudp_bulk_max_version", 2)
|
||||
|
||||
// Machine unique ID (random for this session)
|
||||
var uniqueID [sha1Size]byte
|
||||
randBytes := make([]byte, 64)
|
||||
rand.Read(randBytes)
|
||||
uniqueID = sha0(randBytes)
|
||||
p.AddData("unique_id", uniqueID[:])
|
||||
|
||||
// Brand string for connection limit
|
||||
p.AddStr("branded_ctos", "Branded_VPN")
|
||||
|
||||
// Node info — describes the client machine to the server.
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L8034 (OutRpcNodeInfo)
|
||||
hostname, _ := os.Hostname()
|
||||
p.AddStr("ClientProductName", clientStr)
|
||||
p.AddStr("ServerProductName", "")
|
||||
p.AddStr("ClientOsName", runtime.GOOS)
|
||||
p.AddStr("ClientOsVer", runtime.GOARCH)
|
||||
p.AddStr("ClientOsProductId", "")
|
||||
p.AddStr("ClientHostname", hostname)
|
||||
p.AddStr("ServerHostname", "")
|
||||
p.AddStr("ProxyHostname", "")
|
||||
// Note: HubName is NOT added here because "hubname" (lowercase) is already
|
||||
// present from the auth fields. SoftEther's AddElement rejects duplicate names
|
||||
// (case-insensitive), and PackRead returns NULL on AddElement failure.
|
||||
// p.AddStr("HubName", cfg.Hub)
|
||||
p.AddData("UniqueId", uniqueID[:16])
|
||||
p.AddInt("ClientProductVer", clientVer)
|
||||
p.AddInt("ClientProductBuild", clientBuild)
|
||||
p.AddInt("ServerProductVer", 0)
|
||||
p.AddInt("ServerProductBuild", 0)
|
||||
p.AddIP4("ClientIpAddress", 0)
|
||||
p.AddData("ClientIpAddress6", make([]byte, 16))
|
||||
p.AddInt("ClientPort", 0)
|
||||
p.AddIP4("ServerIpAddress", 0)
|
||||
p.AddData("ServerIpAddress6", make([]byte, 16))
|
||||
p.AddInt("ServerPort2", 0)
|
||||
p.AddIP4("ProxyIpAddress", 0)
|
||||
p.AddData("ProxyIpAddress6", make([]byte, 16))
|
||||
p.AddInt("ProxyPort", 0)
|
||||
|
||||
// OS version info (non-Windows)
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L8079 (OutRpcWinVer)
|
||||
p.AddBool("V_IsWindows", false)
|
||||
p.AddBool("V_IsNT", false)
|
||||
p.AddBool("V_IsServer", false)
|
||||
p.AddBool("V_IsBeta", false)
|
||||
p.AddInt("V_VerMajor", 0)
|
||||
p.AddInt("V_VerMinor", 0)
|
||||
p.AddInt("V_Build", 0)
|
||||
p.AddInt("V_ServicePack", 0)
|
||||
p.AddStr("V_Title", "Linux")
|
||||
|
||||
return p
|
||||
}
|
||||
137
pkg/client/crypto.go
Normal file
137
pkg/client/crypto.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// SHA-0 implementation for SoftEther VPN password authentication.
|
||||
//
|
||||
// SoftEther uses SHA-0 (the original, withdrawn FIPS-180 hash — NOT SHA-1) for
|
||||
// password hashing and the SecurePassword challenge-response. SHA-0 differs from
|
||||
// SHA-1 only in that the message schedule expansion does NOT include a left-rotate
|
||||
// by 1 bit (see transform() below).
|
||||
//
|
||||
// Auth flow:
|
||||
// 1. Client hashes the plaintext password: HashedPassword = SHA0(password)
|
||||
// 2. Server sends a 20-byte random challenge in the Hello pack
|
||||
// 3. Client computes: SecurePassword = SHA0(HashedPassword + ServerRandom)
|
||||
// 4. Client sends SecurePassword in the auth pack
|
||||
//
|
||||
// Reference C implementation:
|
||||
// https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Encrypt.c#L1088 (Sha0)
|
||||
// https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Sam.c#L105 (SecurePassword)
|
||||
package client
|
||||
|
||||
const sha1Size = 20
|
||||
|
||||
func rol(bits int, value uint32) uint32 {
|
||||
return (value << bits) | (value >> (32 - bits))
|
||||
}
|
||||
|
||||
type sha0ctx struct {
|
||||
count uint64
|
||||
buf [64]byte
|
||||
state [8]uint32
|
||||
}
|
||||
|
||||
func (c *sha0ctx) init() {
|
||||
c.state[0] = 0x67452301
|
||||
c.state[1] = 0xEFCDAB89
|
||||
c.state[2] = 0x98BADCFE
|
||||
c.state[3] = 0x10325476
|
||||
c.state[4] = 0xC3D2E1F0
|
||||
c.count = 0
|
||||
}
|
||||
|
||||
func (c *sha0ctx) transform() {
|
||||
var W [80]uint32
|
||||
|
||||
p := 0
|
||||
t := 0
|
||||
for ; t < 16; t++ {
|
||||
W[t] = uint32(c.buf[p])<<24 | uint32(c.buf[p+1])<<16 | uint32(c.buf[p+2])<<8 | uint32(c.buf[p+3])
|
||||
p += 4
|
||||
}
|
||||
// SHA-0: W[t] = W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16] (NO rotate)
|
||||
// SHA-1 would be: W[t] = rol(1, W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16])
|
||||
for ; t < 80; t++ {
|
||||
W[t] = W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]
|
||||
}
|
||||
|
||||
A, B, C, D, E := c.state[0], c.state[1], c.state[2], c.state[3], c.state[4]
|
||||
for t = 0; t < 80; t++ {
|
||||
tmp := rol(5, A) + E + W[t]
|
||||
if t < 20 {
|
||||
tmp += (D ^ (B & (C ^ D))) + 0x5A827999
|
||||
} else if t < 40 {
|
||||
tmp += (B ^ C ^ D) + 0x6ED9EBA1
|
||||
} else if t < 60 {
|
||||
tmp += ((B & C) | (D & (B | C))) + 0x8F1BBCDC
|
||||
} else {
|
||||
tmp += (B ^ C ^ D) + 0xCA62C1D6
|
||||
}
|
||||
E = D
|
||||
D = C
|
||||
C = rol(30, B)
|
||||
B = A
|
||||
A = tmp
|
||||
}
|
||||
|
||||
c.state[0] += A
|
||||
c.state[1] += B
|
||||
c.state[2] += C
|
||||
c.state[3] += D
|
||||
c.state[4] += E
|
||||
}
|
||||
|
||||
func (c *sha0ctx) update(data []byte) {
|
||||
i := int(c.count & 63)
|
||||
c.count += uint64(len(data))
|
||||
for _, d := range data {
|
||||
c.buf[i] = d
|
||||
i++
|
||||
if i == 64 {
|
||||
c.transform()
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sha0ctx) final() {
|
||||
cnt := c.count * 8
|
||||
c.update([]byte{0x80})
|
||||
for c.count&63 != 56 {
|
||||
c.update([]byte{0x0})
|
||||
}
|
||||
for i := 0; i < 8; i++ {
|
||||
c.update([]byte{byte(cnt >> ((7 - i) * 8))})
|
||||
}
|
||||
p := 0
|
||||
for i := 0; i < 5; i++ {
|
||||
c.buf[p] = byte(c.state[i] >> 24)
|
||||
c.buf[p+1] = byte(c.state[i] >> 16)
|
||||
c.buf[p+2] = byte(c.state[i] >> 8)
|
||||
c.buf[p+3] = byte(c.state[i])
|
||||
p += 4
|
||||
}
|
||||
}
|
||||
|
||||
func sha0(data []byte) [sha1Size]byte {
|
||||
var c sha0ctx
|
||||
c.init()
|
||||
c.update(data)
|
||||
c.final()
|
||||
var out [sha1Size]byte
|
||||
copy(out[:], c.buf[:sha1Size])
|
||||
return out
|
||||
}
|
||||
|
||||
// SecurePassword computes the challenge-response: SHA0(HashedPassword + ServerRandom).
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Sam.c#L105
|
||||
func SecurePassword(hashedPassword, random [sha1Size]byte) [sha1Size]byte {
|
||||
buf := make([]byte, sha1Size*2)
|
||||
copy(buf, hashedPassword[:])
|
||||
copy(buf[sha1Size:], random[:])
|
||||
return sha0(buf)
|
||||
}
|
||||
|
||||
// HashPassword computes SHA0(password) — the first step of SoftEther password auth.
|
||||
// The result is what SoftEther stores server-side as "HashedPassword".
|
||||
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Sam.c#L91 (HashPassword)
|
||||
func HashPassword(password string) [sha1Size]byte {
|
||||
return sha0([]byte(password))
|
||||
}
|
||||
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