Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes

This commit is contained in:
Shtorm
2026-06-04 01:47:50 +03:00
parent 9b3da79c32
commit 195a33379d
164 changed files with 16665 additions and 1332 deletions

View File

@@ -1,82 +0,0 @@
package masque
import (
"sync"
"github.com/sagernet/wireguard-go/tun"
"github.com/songgao/water"
)
type NetstackAdapter struct {
dev tun.Device
tunnelBufPool sync.Pool
tunnelSizesPool sync.Pool
}
func (n *NetstackAdapter) ReadPacket(buf []byte) (int, error) {
packetBufsPtr := n.tunnelBufPool.Get().(*[][]byte)
sizesPtr := n.tunnelSizesPool.Get().(*[]int)
defer func() {
(*packetBufsPtr)[0] = nil
n.tunnelBufPool.Put(packetBufsPtr)
n.tunnelSizesPool.Put(sizesPtr)
}()
(*packetBufsPtr)[0] = buf
(*sizesPtr)[0] = 0
_, err := n.dev.Read(*packetBufsPtr, *sizesPtr, 0)
if err != nil {
return 0, err
}
return (*sizesPtr)[0], nil
}
func (n *NetstackAdapter) WritePacket(pkt []byte) error {
// Write expects a slice of packet buffers.
_, err := n.dev.Write([][]byte{pkt}, 0)
return err
}
// NewNetstackAdapter creates a new NetstackAdapter.
func NewNetstackAdapter(dev tun.Device) TunnelDevice {
return &NetstackAdapter{
dev: dev,
tunnelBufPool: sync.Pool{
New: func() interface{} {
buf := make([][]byte, 1)
return &buf
},
},
tunnelSizesPool: sync.Pool{
New: func() interface{} {
sizes := make([]int, 1)
return &sizes
},
},
}
}
type WaterAdapter struct {
iface *water.Interface
}
func (w *WaterAdapter) ReadPacket(buf []byte) (int, error) {
n, err := w.iface.Read(buf)
if err != nil {
return 0, err
}
return n, nil
}
func (w *WaterAdapter) WritePacket(pkt []byte) error {
_, err := w.iface.Write(pkt)
return err
}
func NewWaterAdapter(iface *water.Interface) TunnelDevice {
return &WaterAdapter{iface: iface}
}

View File

@@ -16,17 +16,11 @@ import (
M "github.com/sagernet/sing/common/metadata"
)
type TunnelDevice interface {
ReadPacket(buf []byte) (int, error)
WritePacket(pkt []byte) error
}
type Tunnel struct {
ctx context.Context
logger logger.ContextLogger
options TunnelOptions
tunDevice Device
tunnelDevice TunnelDevice
ctx context.Context
logger logger.ContextLogger
options TunnelOptions
device Device
udpConn net.PacketConn
tr *http3.Transport
@@ -49,17 +43,16 @@ func NewTunnel(ctx context.Context, logger logger.ContextLogger, options TunnelO
return nil, E.Cause(err, "create MASQUE device")
}
return &Tunnel{
ctx: ctx,
logger: logger,
options: options,
tunDevice: tunDevice,
tunnelDevice: NewNetstackAdapter(tunDevice),
ctx: ctx,
logger: logger,
options: options,
device: tunDevice,
}, nil
}
func (e *Tunnel) Start(resolve bool) error {
if resolve {
err := e.tunDevice.Start()
err := e.device.Start()
if err != nil {
return err
}
@@ -72,14 +65,14 @@ func (e *Tunnel) DialContext(ctx context.Context, network string, destination M.
if !destination.Addr.IsValid() {
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
}
return e.tunDevice.DialContext(ctx, network, destination)
return e.device.DialContext(ctx, network, destination)
}
func (e *Tunnel) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if !destination.Addr.IsValid() {
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
}
return e.tunDevice.ListenPacket(ctx, destination)
return e.device.ListenPacket(ctx, destination)
}
func (e *Tunnel) Close() error {
@@ -95,14 +88,16 @@ func (e *Tunnel) Close() error {
}
e.ipConn = nil
}
return e.tunDevice.Close()
return e.device.Close()
}
func (e *Tunnel) maintainTunnel() {
go func() {
buf := make([]byte, 1280)
bufs := make([][]byte, 1)
bufs[0] = make([]byte, 1280)
sizes := make([]int, 1)
for e.ctx.Err() == nil {
n, err := e.tunnelDevice.ReadPacket(buf)
_, err := e.device.Read(bufs, sizes, 0)
if err != nil {
e.logger.ErrorContext(e.ctx, fmt.Errorf("failed to read from TUN device: %v", err))
continue
@@ -111,7 +106,7 @@ func (e *Tunnel) maintainTunnel() {
if err != nil {
return
}
icmp, err := ipConn.WritePacket(buf[:n])
icmp, err := ipConn.WritePacket(bufs[0][:sizes[0]])
if err != nil {
if errors.As(err, new(*connectip.CloseError)) {
if ok := e.closeIpConn(ipConn); ok {
@@ -123,7 +118,7 @@ func (e *Tunnel) maintainTunnel() {
continue
}
if len(icmp) > 0 {
if err := e.tunnelDevice.WritePacket(icmp); err != nil {
if _, err := e.device.Write([][]byte{icmp}, 0); err != nil {
if errors.As(err, new(*connectip.CloseError)) {
e.logger.ErrorContext(e.ctx, fmt.Errorf("connection closed while writing ICMP to TUN device: %v", err))
continue
@@ -151,7 +146,7 @@ func (e *Tunnel) maintainTunnel() {
e.logger.ErrorContext(e.ctx, fmt.Errorf("Error reading from IP connection: %v, continuine...", err))
continue
}
if err := e.tunnelDevice.WritePacket(buf[:n]); err != nil {
if _, err := e.device.Write([][]byte{buf[:n]}, 0); err != nil {
continue
}
}

227
transport/openvpn/cipher.go Normal file
View File

@@ -0,0 +1,227 @@
package openvpn
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"hash"
"golang.org/x/crypto/chacha20poly1305"
)
const (
AESGCMTagSize = 16
AESGCMIVSize = 12
CBCIVSize = aes.BlockSize
)
type DataCipher interface {
Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error)
Decrypt(packet []byte, headerSize int) ([]byte, error)
}
type AEADDataCipher struct {
send cipher.AEAD
recv cipher.AEAD
sendImplicitIV [AESGCMIVSize]byte
recvImplicitIV [AESGCMIVSize]byte
}
func NewAEADCipher(keys *KeyMaterial, cipherName string) (*AEADDataCipher, error) {
var send, recv cipher.AEAD
var err error
if cipherName == CipherCHACHA20POLY {
send, err = chacha20poly1305.New(keys.SendCipherKey)
if err != nil {
return nil, err
}
recv, err = chacha20poly1305.New(keys.RecvCipherKey)
if err != nil {
return nil, err
}
} else {
sendBlock, err := aes.NewCipher(keys.SendCipherKey)
if err != nil {
return nil, err
}
recvBlock, err := aes.NewCipher(keys.RecvCipherKey)
if err != nil {
return nil, err
}
send, err = cipher.NewGCMWithTagSize(sendBlock, AESGCMTagSize)
if err != nil {
return nil, err
}
recv, err = cipher.NewGCMWithTagSize(recvBlock, AESGCMTagSize)
if err != nil {
return nil, err
}
}
if len(keys.SendHMACKey) < AESGCMIVSize-4 || len(keys.RecvHMACKey) < AESGCMIVSize-4 {
return nil, errors.New("openvpn implicit IV keys are too short")
}
g := &AEADDataCipher{send: send, recv: recv}
copy(g.sendImplicitIV[4:], keys.SendHMACKey[:AESGCMIVSize-4])
copy(g.recvImplicitIV[4:], keys.RecvHMACKey[:AESGCMIVSize-4])
return g, nil
}
func (g *AEADDataCipher) Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error) {
var pidBytes [4]byte
binary.BigEndian.PutUint32(pidBytes[:], packetID)
nonce := g.nonce(packetID, g.sendImplicitIV)
ad := append(header, pidBytes[:]...)
sealed := g.send.Seal(nil, nonce[:], payload, ad)
out := make([]byte, 0, len(header)+4+len(sealed))
out = append(out, header...)
out = append(out, pidBytes[:]...)
out = append(out, sealed[len(sealed)-AESGCMTagSize:]...)
out = append(out, sealed[:len(sealed)-AESGCMTagSize]...)
return out, nil
}
func (g *AEADDataCipher) Decrypt(packet []byte, headerSize int) ([]byte, error) {
if len(packet) < headerSize+4+AESGCMTagSize+1 {
return nil, errors.New("openvpn gcm data packet too short")
}
header := packet[:headerSize]
pidBytes := packet[headerSize : headerSize+4]
tag := packet[headerSize+4 : headerSize+4+AESGCMTagSize]
ciphertext := packet[headerSize+4+AESGCMTagSize:]
combined := append(ciphertext, tag...)
ad := append(header, pidBytes...)
nonce := g.nonce(binary.BigEndian.Uint32(pidBytes), g.recvImplicitIV)
return g.recv.Open(nil, nonce[:], combined, ad)
}
func (g *AEADDataCipher) nonce(packetID uint32, implicit [AESGCMIVSize]byte) [AESGCMIVSize]byte {
nonce := implicit
binary.BigEndian.PutUint32(nonce[:4], binary.BigEndian.Uint32(nonce[:4])^packetID)
return nonce
}
type CBCDataCipher struct {
sendBlock cipher.Block
recvBlock cipher.Block
sendHMAC []byte
recvHMAC []byte
newHash func() hash.Hash
hmacSize int
}
func NewCBCCipher(keys *KeyMaterial, auth string) (*CBCDataCipher, error) {
sendBlock, err := aes.NewCipher(keys.SendCipherKey)
if err != nil {
return nil, err
}
recvBlock, err := aes.NewCipher(keys.RecvCipherKey)
if err != nil {
return nil, err
}
var newHash func() hash.Hash
var hmacSize int
switch auth {
case AuthSHA256:
newHash = sha256.New
hmacSize = sha256.Size
case AuthSHA384:
newHash = sha512.New384
hmacSize = 48
case AuthSHA512:
newHash = sha512.New
hmacSize = sha512.Size
default:
newHash = sha1.New
hmacSize = sha1.Size
}
return &CBCDataCipher{
sendBlock: sendBlock,
recvBlock: recvBlock,
sendHMAC: cloneBytes(keys.SendHMACKey[:hmacSize]),
recvHMAC: cloneBytes(keys.RecvHMACKey[:hmacSize]),
newHash: newHash,
hmacSize: hmacSize,
}, nil
}
func (c *CBCDataCipher) Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error) {
var pidBytes [4]byte
binary.BigEndian.PutUint32(pidBytes[:], packetID)
plain := append(pidBytes[:], payload...)
padLen := aes.BlockSize - (len(plain) % aes.BlockSize)
for i := 0; i < padLen; i++ {
plain = append(plain, byte(padLen))
}
iv := make([]byte, CBCIVSize)
if _, err := rand.Read(iv); err != nil {
return nil, err
}
ct := make([]byte, len(plain))
cipher.NewCBCEncrypter(c.sendBlock, iv).CryptBlocks(ct, plain)
mac := hmac.New(c.newHash, c.sendHMAC)
mac.Write(iv)
mac.Write(ct)
tag := mac.Sum(nil)
out := make([]byte, 0, len(header)+c.hmacSize+CBCIVSize+len(ct))
out = append(out, header...)
out = append(out, tag...)
out = append(out, iv...)
out = append(out, ct...)
return out, nil
}
func (c *CBCDataCipher) Decrypt(packet []byte, headerSize int) ([]byte, error) {
minSize := headerSize + c.hmacSize + CBCIVSize + aes.BlockSize
if len(packet) < minSize {
return nil, errors.New("openvpn cbc data packet too short")
}
tag := packet[headerSize : headerSize+c.hmacSize]
iv := packet[headerSize+c.hmacSize : headerSize+c.hmacSize+CBCIVSize]
ct := packet[headerSize+c.hmacSize+CBCIVSize:]
if len(ct)%aes.BlockSize != 0 {
return nil, errors.New("openvpn cbc ciphertext not block-aligned")
}
mac := hmac.New(c.newHash, c.recvHMAC)
mac.Write(iv)
mac.Write(ct)
if !hmac.Equal(tag, mac.Sum(nil)) {
return nil, errors.New("openvpn cbc hmac verification failed")
}
plain := make([]byte, len(ct))
cipher.NewCBCDecrypter(c.recvBlock, iv).CryptBlocks(plain, ct)
padLen := int(plain[len(plain)-1])
if padLen < 1 || padLen > aes.BlockSize {
return nil, errors.New("openvpn cbc invalid padding")
}
plain = plain[:len(plain)-padLen]
if len(plain) < 4 {
return nil, errors.New("openvpn cbc payload too short")
}
return plain[4:], nil
}
func CipherKeyLength(cipher string) int {
switch cipher {
case CipherAES128GCM, CipherAES128CBC:
return 16
case CipherAES192GCM, CipherAES192CBC:
return 24
default:
return 32
}
}
func IsAEAD(cipher string) bool {
switch cipher {
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM, CipherCHACHA20POLY:
return true
default:
return false
}
}

280
transport/openvpn/client.go Normal file
View File

@@ -0,0 +1,280 @@
package openvpn
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/sagernet/sing/common/tls"
)
const defaultHandshakeTimeout = 30 * time.Second
type Client struct {
config *ClientConfig
tlsConfig tls.Config
mux *PacketMux
control *ControlChannel
tlsConn tls.Conn
data *DataChannel
push *PushReply
cancel context.CancelFunc
}
func NewClient(config *ClientConfig, io PacketIO, tlsConfig tls.Config) (*Client, error) {
if config == nil {
return nil, errors.New("nil openvpn client config")
}
if io == nil {
return nil, errors.New("nil openvpn packet io")
}
if tlsConfig == nil {
return nil, errors.New("nil openvpn tls config")
}
var crypt ControlCrypt
var err error
if config.TLSAuthKey != nil {
crypt, err = NewTLSAuth(config.TLSAuthKey, config.KeyDirection, config.Auth)
if err != nil {
return nil, err
}
} else if config.TLSCryptKey != nil {
crypt, err = NewTLSCrypt(config.TLSCryptKey, true)
if err != nil {
return nil, err
}
}
local, err := NewSessionID()
if err != nil {
return nil, err
}
runCtx, cancel := context.WithCancel(context.Background())
mux := NewPacketMux(io)
go mux.Run(runCtx)
return &Client{
config: config,
tlsConfig: tlsConfig,
mux: mux,
control: NewControlChannel(mux, crypt, local),
cancel: cancel,
}, nil
}
func (c *Client) Handshake(ctx context.Context) (*PushReply, error) {
if c == nil {
return nil, errors.New("nil openvpn client")
}
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, defaultHandshakeTimeout)
defer cancel()
}
if c.config.TLSCryptV2WKc != nil {
if err := c.sendResetV3(ctx); err != nil {
return nil, fmt.Errorf("send hard reset v3: %w", err)
}
} else {
if err := c.control.SendReset(ctx); err != nil {
return nil, fmt.Errorf("send hard reset: %w", err)
}
}
if err := c.waitServerReset(ctx); err != nil {
return nil, err
}
controlConn := NewControlConn(c.control)
tlsConn, err := c.tlsConfig.Client(controlConn)
if err != nil {
return nil, fmt.Errorf("openvpn tls client: %w", err)
}
c.tlsConn = tlsConn
if err := c.tlsConn.HandshakeContext(ctx); err != nil {
return nil, fmt.Errorf("openvpn tls handshake: %w", err)
}
clientRecord, err := NewClientKeyMethod2Record(
InstallScriptOptionsString(c.config.Proto, c.config.Cipher, c.config.Auth),
InstallScriptPeerInfo(c.config.Cipher),
strings.TrimSpace(c.config.Username),
c.config.Password,
)
if err != nil {
return nil, err
}
clientBytes, err := clientRecord.MarshalClient()
if err != nil {
return nil, err
}
if _, err := c.tlsConn.Write(clientBytes); err != nil {
return nil, fmt.Errorf("write key method 2 client record: %w", err)
}
serverRecord, err := c.readServerKeyMethod(ctx)
if err != nil {
return nil, err
}
sources := clientRecord.Sources
sources.Server = serverRecord.Sources.Server
keys, err := DeriveClientKeyMaterial(sources, c.control.LocalSessionID(), c.control.RemoteSessionID(), 32)
if err != nil {
return nil, fmt.Errorf("derive data channel keys: %w", err)
}
if _, err := c.tlsConn.Write([]byte(PushRequest + "\x00")); err != nil {
return nil, fmt.Errorf("write push request: %w", err)
}
push, err := c.readPushReply(ctx)
if err != nil {
return nil, err
}
c.push = push
dataCipher := c.config.Cipher
if push.Cipher != "" {
dataCipher = push.Cipher
}
if dataCipher == "" {
return nil, errors.New("openvpn server did not negotiate a cipher and no cipher configured")
}
keyLen := CipherKeyLength(dataCipher)
keys.SendCipherKey = keys.SendCipherKey[:keyLen]
keys.RecvCipherKey = keys.RecvCipherKey[:keyLen]
var cipher DataCipher
if IsAEAD(dataCipher) {
cipher, err = NewAEADCipher(keys, dataCipher)
} else {
cipher, err = NewCBCCipher(keys, c.config.Auth)
}
if err != nil {
return nil, err
}
c.data = NewDataChannel(cipher, push.PeerID, push.CompLZO)
return push, nil
}
func (c *Client) WriteIPPacket(ctx context.Context, packet []byte) error {
if c.data == nil {
return errors.New("openvpn data channel is not ready")
}
encrypted, err := c.data.Encrypt(packet)
if err != nil {
return err
}
return c.mux.WritePacket(ctx, encrypted)
}
func (c *Client) ReadIPPacket(ctx context.Context) ([]byte, error) {
if c.data == nil {
return nil, errors.New("openvpn data channel is not ready")
}
for {
packet, err := c.mux.ReadDataPacket(ctx)
if err != nil {
return nil, err
}
plain, err := c.data.Decrypt(packet)
if err != nil {
continue
}
return plain, nil
}
}
func (c *Client) Close() error {
if c.cancel != nil {
c.cancel()
}
if c.tlsConn != nil {
_ = c.tlsConn.Close()
}
if c.mux != nil {
return c.mux.Close()
}
return nil
}
func (c *Client) waitServerReset(ctx context.Context) error {
for {
packet, err := c.control.Read(ctx)
if err != nil {
return fmt.Errorf("read hard reset response: %w", err)
}
switch packet.Opcode {
case PControlHardResetServerV2:
return c.control.SendAck(ctx)
case PControlHardResetServerV1:
return fmt.Errorf("openvpn server replied with unsupported key method 1 reset")
}
}
}
func (c *Client) readServerKeyMethod(ctx context.Context) (*KeyMethod2Record, error) {
var buf []byte
tmp := make([]byte, 4096)
for {
n, err := c.tlsConn.Read(tmp)
if err != nil {
return nil, fmt.Errorf("read key method 2 server record: %w", err)
}
buf = append(buf, tmp[:n]...)
record, err := ParseServerKeyMethod2Record(buf)
if err == nil {
return record, nil
}
if !strings.Contains(err.Error(), "truncated") && !errors.Is(err, ioStringEOF) {
return nil, err
}
}
}
func (c *Client) readPushReply(ctx context.Context) (*PushReply, error) {
var buf []byte
tmp := make([]byte, 4096)
for {
n, err := c.tlsConn.Read(tmp)
if err != nil {
if errors.Is(err, io.EOF) && len(buf) > 0 {
break
}
return nil, fmt.Errorf("read push reply: %w", err)
}
buf = append(buf, tmp[:n]...)
if bytes.Contains(buf, []byte("\x00")) || strings.Contains(string(buf), "PUSH_REPLY") {
msg := string(buf)
if idx := strings.IndexByte(msg, 0); idx >= 0 {
msg = msg[:idx]
}
if reply, err := ParsePushReply(msg); err == nil {
return reply, nil
}
}
}
return nil, ctx.Err()
}
func (c *Client) sendResetV3(ctx context.Context) error {
c.control.mu.Lock()
messageID := c.control.sendMessage
c.control.sendMessage++
packet := &ControlPacket{
Opcode: PControlHardResetClientV3,
KeyID: c.control.keyID,
LocalSession: c.control.local,
MessageID: messageID,
}
c.control.pending[messageID] = packet
c.control.mu.Unlock()
encoded, err := c.control.encodeAndWrap(ctx, packet)
if err != nil {
return err
}
encoded = append(encoded, c.config.TLSCryptV2WKc...)
return c.control.io.WritePacket(ctx, encoded)
}
var _ net.Conn = (*ControlConn)(nil)

175
transport/openvpn/config.go Normal file
View File

@@ -0,0 +1,175 @@
package openvpn
import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
)
const (
ProtoUDP = "udp"
ProtoTCP = "tcp"
CipherAES128GCM = "AES-128-GCM"
CipherAES192GCM = "AES-192-GCM"
CipherAES256GCM = "AES-256-GCM"
CipherAES128CBC = "AES-128-CBC"
CipherAES192CBC = "AES-192-CBC"
CipherAES256CBC = "AES-256-CBC"
CipherCHACHA20POLY = "CHACHA20-POLY1305"
AuthSHA1 = "SHA1"
AuthSHA256 = "SHA256"
AuthSHA384 = "SHA384"
AuthSHA512 = "SHA512"
)
type ClientConfig struct {
Proto string
Cipher string
Auth string
Username string
Password string
KeyDirection int
TLSCrypt []byte
TLSCryptV2 bool
TLSCryptKey []byte
TLSCryptV2WKc []byte
TLSAuthKey []byte
}
func (c *ClientConfig) Prepare() error {
if c == nil {
return errors.New("nil openvpn client config")
}
c.Proto = normalizeProto(c.Proto)
c.Cipher = strings.ToUpper(strings.TrimSpace(c.Cipher))
if c.Auth == "" {
c.Auth = AuthSHA1
}
c.Auth = strings.ToUpper(strings.TrimSpace(c.Auth))
if c.Proto != ProtoUDP && c.Proto != ProtoTCP {
return fmt.Errorf("unsupported openvpn proto %q: only udp and tcp are supported", c.Proto)
}
if c.Cipher != "" && !isValidCipher(c.Cipher) {
return fmt.Errorf("unsupported openvpn cipher %q", c.Cipher)
}
if !isValidAuth(c.Auth) {
return fmt.Errorf("unsupported openvpn auth %q", c.Auth)
}
if c.TLSCryptV2 {
kc, wkc, err := decodeTLSCryptV2Key(c.TLSCrypt)
if err != nil {
return fmt.Errorf("parse tls-crypt-v2 key: %w", err)
}
c.TLSCryptKey = kc
c.TLSCryptV2WKc = wkc
return nil
}
if len(strings.TrimSpace(string(c.TLSCrypt))) == 0 {
return nil
}
key, err := decodeStaticKey(c.TLSCrypt)
if err != nil {
return fmt.Errorf("parse tls key: %w", err)
}
if c.KeyDirection >= 0 {
c.TLSAuthKey = key
} else {
c.TLSCryptKey = key
}
return nil
}
func normalizeProto(proto string) string {
switch strings.ToLower(strings.TrimSpace(proto)) {
case "", "udp", "udp4":
return ProtoUDP
case "tcp", "tcp-client", "tcp4", "tcp4-client":
return ProtoTCP
default:
return strings.ToLower(strings.TrimSpace(proto))
}
}
func isValidCipher(cipher string) bool {
switch cipher {
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM,
CipherAES128CBC, CipherAES192CBC, CipherAES256CBC,
CipherCHACHA20POLY:
return true
}
return false
}
func isValidAuth(auth string) bool {
switch auth {
case AuthSHA1, AuthSHA256, AuthSHA384, AuthSHA512:
return true
}
return false
}
func decodeStaticKey(block []byte) ([]byte, error) {
var hexLines []string
for _, raw := range strings.Split(string(block), "\n") {
line := strings.TrimSpace(raw)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "-----BEGIN OpenVPN Static key") || strings.HasPrefix(line, "-----END OpenVPN Static key") {
continue
}
hexLines = append(hexLines, line)
}
encoded := strings.Join(hexLines, "")
key, err := hex.DecodeString(encoded)
if err != nil {
return nil, err
}
if len(key) != 256 {
return nil, fmt.Errorf("invalid static key length %d, expected 256 bytes", len(key))
}
return key, nil
}
func decodeTLSCryptV2Key(block []byte) (kc []byte, wkc []byte, err error) {
data, err := decodePEM(block, "OpenVPN tls-crypt-v2 client key")
if err != nil {
return nil, nil, err
}
if len(data) < 256 {
return nil, nil, fmt.Errorf("tls-crypt-v2 key too short: %d bytes", len(data))
}
return data[:256], data[256:], nil
}
func decodePEM(block []byte, expectedHeader string) ([]byte, error) {
lines := strings.Split(string(block), "\n")
var b64 strings.Builder
inBlock := false
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, "BEGIN") && strings.Contains(line, expectedHeader) {
inBlock = true
continue
}
if strings.Contains(line, "END") && strings.Contains(line, expectedHeader) {
break
}
if inBlock {
b64.WriteString(line)
}
}
if b64.Len() == 0 {
return nil, fmt.Errorf("no %s block found", expectedHeader)
}
return base64Decode(b64.String())
}
func base64Decode(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}

View File

@@ -0,0 +1,475 @@
package openvpn
import (
"context"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
)
type PacketIO interface {
ReadPacket(ctx context.Context) ([]byte, error)
WritePacket(ctx context.Context, packet []byte) error
Close() error
LocalAddr() net.Addr
RemoteAddr() net.Addr
}
type ControlChannel struct {
io PacketIO
encode func(*ControlPacket, uint32, uint32) ([]byte, error)
decode func([]byte) (*ControlPacket, uint32, uint32, error)
clock func() time.Time
keyID uint8
local SessionID
remote SessionID
mu sync.Mutex
sendPacketID uint32
sendMessage uint32
ackPending []uint32
pending map[uint32]*ControlPacket
readDeadline time.Time
writeDeadline time.Time
}
func NewControlChannel(io PacketIO, crypt ControlCrypt, local SessionID) *ControlChannel {
ch := &ControlChannel{
io: io,
clock: time.Now,
local: local,
pending: make(map[uint32]*ControlPacket),
}
if crypt != nil {
ch.encode = func(p *ControlPacket, pid uint32, t uint32) ([]byte, error) {
return EncodeControlPacketCrypt(*p, crypt, pid, t)
}
ch.decode = func(pkt []byte) (*ControlPacket, uint32, uint32, error) {
return DecodeControlPacketCrypt(crypt, pkt)
}
} else {
ch.encode = func(p *ControlPacket, _ uint32, _ uint32) ([]byte, error) {
return EncodeControlPacket(*p)
}
ch.decode = func(pkt []byte) (*ControlPacket, uint32, uint32, error) {
cp, err := DecodeControlPacket(pkt)
return cp, 0, 0, err
}
}
return ch
}
func (c *ControlChannel) LocalSessionID() SessionID {
return c.local
}
func (c *ControlChannel) RemoteSessionID() SessionID {
c.mu.Lock()
defer c.mu.Unlock()
return c.remote
}
func (c *ControlChannel) SetRemoteSessionID(id SessionID) {
c.mu.Lock()
c.remote = id
c.mu.Unlock()
}
func (c *ControlChannel) SendReset(ctx context.Context) error {
_, err := c.Send(ctx, PControlHardResetClientV2, nil)
return err
}
func (c *ControlChannel) Send(ctx context.Context, opcode Opcode, payload []byte) (uint32, error) {
if !opcode.HasMessageID() {
return 0, fmt.Errorf("opcode %s cannot carry a reliable message", opcode)
}
c.mu.Lock()
messageID := c.sendMessage
c.sendMessage++
packet := &ControlPacket{
Opcode: opcode,
KeyID: c.keyID,
LocalSession: c.local,
AckIDs: append([]uint32(nil), c.ackPending...),
AckRemoteSession: c.remote,
MessageID: messageID,
Payload: cloneBytes(payload),
}
c.ackPending = nil
c.pending[messageID] = packet
c.mu.Unlock()
if err := c.writeControlPacket(ctx, packet); err != nil {
return 0, err
}
return messageID, nil
}
func (c *ControlChannel) SendAck(ctx context.Context) error {
c.mu.Lock()
if len(c.ackPending) == 0 {
c.mu.Unlock()
return nil
}
packet := &ControlPacket{
Opcode: PAckV1,
KeyID: c.keyID,
LocalSession: c.local,
AckIDs: append([]uint32(nil), c.ackPending...),
AckRemoteSession: c.remote,
}
c.ackPending = nil
c.mu.Unlock()
return c.writeControlPacket(ctx, packet)
}
func (c *ControlChannel) Read(ctx context.Context) (*ControlPacket, error) {
for {
packet, err := c.readControlPacket(ctx)
if err != nil {
return nil, err
}
c.mu.Lock()
if c.remote == (SessionID{}) && packet.LocalSession != c.local {
c.remote = packet.LocalSession
}
for _, ackID := range packet.AckIDs {
delete(c.pending, ackID)
}
if packet.Opcode.HasMessageID() {
c.ackPending = appendAck(c.ackPending, packet.MessageID)
}
c.mu.Unlock()
if packet.Opcode == PAckV1 {
continue
}
return packet, nil
}
}
func (c *ControlChannel) PendingMessages() int {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.pending)
}
func (c *ControlChannel) RetransmitPending(ctx context.Context) error {
c.mu.Lock()
packets := make([]*ControlPacket, 0, len(c.pending))
for _, packet := range c.pending {
cp := *packet
cp.AckIDs = append([]uint32(nil), c.ackPending...)
cp.AckRemoteSession = c.remote
packets = append(packets, &cp)
}
c.ackPending = nil
c.mu.Unlock()
for _, packet := range packets {
if err := c.writeControlPacket(ctx, packet); err != nil {
return err
}
}
return nil
}
func (c *ControlChannel) writeControlPacket(ctx context.Context, packet *ControlPacket) error {
c.mu.Lock()
c.sendPacketID++
packetID := c.sendPacketID
unixTime := uint32(c.clock().Unix())
deadline := c.writeDeadline
c.mu.Unlock()
if !deadline.IsZero() {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, deadline)
defer cancel()
}
encoded, err := c.encode(packet, packetID, unixTime)
if err != nil {
return err
}
return c.io.WritePacket(ctx, encoded)
}
func (c *ControlChannel) encodeAndWrap(ctx context.Context, packet *ControlPacket) ([]byte, error) {
c.mu.Lock()
c.sendPacketID++
packetID := c.sendPacketID
unixTime := uint32(c.clock().Unix())
c.mu.Unlock()
return c.encode(packet, packetID, unixTime)
}
func (c *ControlChannel) readControlPacket(ctx context.Context) (*ControlPacket, error) {
c.mu.Lock()
deadline := c.readDeadline
c.mu.Unlock()
if !deadline.IsZero() {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, deadline)
defer cancel()
}
raw, err := c.io.ReadPacket(ctx)
if err != nil {
return nil, err
}
packet, _, _, err := c.decode(raw)
return packet, err
}
func (c *ControlChannel) SetDeadline(t time.Time) error {
c.mu.Lock()
c.readDeadline = t
c.writeDeadline = t
c.mu.Unlock()
return nil
}
func (c *ControlChannel) SetReadDeadline(t time.Time) error {
c.mu.Lock()
c.readDeadline = t
c.mu.Unlock()
return nil
}
func (c *ControlChannel) SetWriteDeadline(t time.Time) error {
c.mu.Lock()
c.writeDeadline = t
c.mu.Unlock()
return nil
}
func appendAck(acks []uint32, ack uint32) []uint32 {
for _, existing := range acks {
if existing == ack {
return acks
}
}
return append(acks, ack)
}
type ControlConn struct {
channel *ControlChannel
readBuf []byte
closed bool
mu sync.Mutex
}
func NewControlConn(channel *ControlChannel) *ControlConn {
return &ControlConn{channel: channel}
}
func (c *ControlConn) Read(b []byte) (int, error) {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return 0, net.ErrClosed
}
if len(c.readBuf) > 0 {
n := copy(b, c.readBuf)
c.readBuf = c.readBuf[n:]
c.mu.Unlock()
return n, nil
}
c.mu.Unlock()
for {
packet, err := c.channel.Read(context.Background())
if err != nil {
return 0, err
}
if packet.Opcode != PControlV1 {
if err := c.channel.SendAck(context.Background()); err != nil {
return 0, err
}
continue
}
if err := c.channel.SendAck(context.Background()); err != nil {
return 0, err
}
if len(packet.Payload) == 0 {
continue
}
n := copy(b, packet.Payload)
if n < len(packet.Payload) {
c.mu.Lock()
c.readBuf = append(c.readBuf, packet.Payload[n:]...)
c.mu.Unlock()
}
return n, nil
}
}
func (c *ControlConn) Write(b []byte) (int, error) {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return 0, net.ErrClosed
}
c.mu.Unlock()
if _, err := c.channel.Send(context.Background(), PControlV1, b); err != nil {
return 0, err
}
return len(b), nil
}
func (c *ControlConn) Close() error {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
c.closed = true
c.mu.Unlock()
return c.channel.io.Close()
}
func (c *ControlConn) LocalAddr() net.Addr {
return c.channel.io.LocalAddr()
}
func (c *ControlConn) RemoteAddr() net.Addr {
return c.channel.io.RemoteAddr()
}
func (c *ControlConn) SetDeadline(t time.Time) error {
return c.channel.SetDeadline(t)
}
func (c *ControlConn) SetReadDeadline(t time.Time) error {
return c.channel.SetReadDeadline(t)
}
func (c *ControlConn) SetWriteDeadline(t time.Time) error {
return c.channel.SetWriteDeadline(t)
}
type streamPacketIO struct {
conn net.Conn
}
type datagramPacketIO struct {
conn net.Conn
}
func NewDatagramPacketIO(conn net.Conn) PacketIO {
return &datagramPacketIO{conn: conn}
}
func (d *datagramPacketIO) ReadPacket(ctx context.Context) ([]byte, error) {
done := make(chan struct{})
var (
packet []byte
err error
)
go func() {
defer close(done)
buf := make([]byte, 64*1024)
var n int
n, err = d.conn.Read(buf)
if err == nil {
packet = cloneBytes(buf[:n])
}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
return packet, err
}
}
func (d *datagramPacketIO) WritePacket(ctx context.Context, packet []byte) error {
done := make(chan error, 1)
go func() {
_, err := d.conn.Write(packet)
done <- err
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
func (d *datagramPacketIO) Close() error {
return d.conn.Close()
}
func (d *datagramPacketIO) LocalAddr() net.Addr {
return d.conn.LocalAddr()
}
func (d *datagramPacketIO) RemoteAddr() net.Addr {
return d.conn.RemoteAddr()
}
func NewTCPPacketIO(conn net.Conn) PacketIO {
return &streamPacketIO{conn: conn}
}
func (s *streamPacketIO) ReadPacket(ctx context.Context) ([]byte, error) {
done := make(chan struct{})
var (
packet []byte
err error
)
go func() {
defer close(done)
var lenBuf [2]byte
if _, err = io.ReadFull(s.conn, lenBuf[:]); err != nil {
return
}
size := int(lenBuf[0])<<8 | int(lenBuf[1])
if size == 0 {
err = errors.New("empty openvpn tcp packet")
return
}
packet = make([]byte, size)
_, err = io.ReadFull(s.conn, packet)
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
return packet, err
}
}
func (s *streamPacketIO) WritePacket(ctx context.Context, packet []byte) error {
if len(packet) > 0xffff {
return fmt.Errorf("openvpn tcp packet too large: %d", len(packet))
}
done := make(chan error, 1)
go func() {
frame := make([]byte, 2+len(packet))
frame[0] = byte(len(packet) >> 8)
frame[1] = byte(len(packet))
copy(frame[2:], packet)
_, err := s.conn.Write(frame)
done <- err
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
func (s *streamPacketIO) Close() error {
return s.conn.Close()
}
func (s *streamPacketIO) LocalAddr() net.Addr {
return s.conn.LocalAddr()
}
func (s *streamPacketIO) RemoteAddr() net.Addr {
return s.conn.RemoteAddr()
}

91
transport/openvpn/data.go Normal file
View File

@@ -0,0 +1,91 @@
package openvpn
import (
"errors"
"fmt"
"sync"
)
const (
PeerIDUnset uint32 = 0xffffff
)
type DataChannel struct {
cipher DataCipher
keyID uint8
peerID uint32
compLZO bool
mu sync.Mutex
sendPacketID uint32
}
func NewDataChannel(cipher DataCipher, peerID uint32, compLZO bool) *DataChannel {
return &DataChannel{
cipher: cipher,
peerID: peerID,
compLZO: compLZO,
}
}
func (d *DataChannel) Encrypt(packet []byte) ([]byte, error) {
if d.compLZO {
p := make([]byte, 1+len(packet))
p[0] = 0xFA
copy(p[1:], packet)
packet = p
}
d.mu.Lock()
d.sendPacketID++
packetID := d.sendPacketID
d.mu.Unlock()
return d.cipher.Encrypt(d.dataHeader(), packetID, packet)
}
func (d *DataChannel) Decrypt(packet []byte) ([]byte, error) {
if len(packet) < 1 {
return nil, errors.New("empty openvpn data packet")
}
opcode, _ := parseOpcodeKeyID(packet[0])
headerSize := 1
if opcode == PDataV2 {
headerSize = 4
}
plain, err := d.cipher.Decrypt(packet, headerSize)
if err != nil {
return nil, err
}
if d.compLZO {
if len(plain) < 1 {
return nil, errors.New("openvpn comp-lzo packet too short")
}
if plain[0] != 0xFA {
return nil, fmt.Errorf("openvpn compressed packet not supported (byte: 0x%02x)", plain[0])
}
plain = plain[1:]
}
return plain, nil
}
func (d *DataChannel) dataHeader() []byte {
if d.peerID != PeerIDUnset {
return []byte{
opcodeKeyID(PDataV2, d.keyID),
byte(d.peerID >> 16),
byte(d.peerID >> 8),
byte(d.peerID),
}
}
return []byte{opcodeKeyID(PDataV1, d.keyID)}
}
func ParsePeerID(options string) uint32 {
for _, field := range splitPushOptions(options) {
if len(field) > len("peer-id ") && field[:len("peer-id ")] == "peer-id " {
var id uint32
if _, err := fmt.Sscanf(field, "peer-id %d", &id); err == nil && id <= PeerIDUnset {
return id
}
}
}
return PeerIDUnset
}

View File

@@ -0,0 +1,33 @@
package openvpn
import (
"context"
"net/netip"
"time"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
wgTun "github.com/sagernet/wireguard-go/tun"
)
type Device interface {
wgTun.Device
N.Dialer
Start() error
}
type DeviceOptions struct {
Context context.Context
Logger logger.ContextLogger
Handler tun.Handler
UDPTimeout time.Duration
CreateDialer func(interfaceName string) N.Dialer
Name string
MTU uint32
Address []netip.Prefix
}
func NewDevice(options DeviceOptions) (Device, error) {
return newStackDevice(options)
}

View File

@@ -0,0 +1,309 @@
//go:build with_gvisor
package openvpn
import (
"context"
"net"
"net/netip"
"os"
"github.com/sagernet/gvisor/pkg/buffer"
"github.com/sagernet/gvisor/pkg/tcpip"
"github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet"
"github.com/sagernet/gvisor/pkg/tcpip/header"
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv4"
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv6"
"github.com/sagernet/gvisor/pkg/tcpip/stack"
"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/transport/wireguard"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
wgTun "github.com/sagernet/wireguard-go/tun"
)
type stackDevice struct {
ctx context.Context
logger log.ContextLogger
stack *stack.Stack
mtu uint32
events chan wgTun.Event
wgTun.Device
outbound chan *stack.PacketBuffer
packetOutbound chan *buf.Buffer
done chan struct{}
dispatcher stack.NetworkDispatcher
inet4Address netip.Addr
inet6Address netip.Addr
}
func newStackDevice(options DeviceOptions) (*stackDevice, error) {
tunDevice := &stackDevice{
ctx: options.Context,
logger: options.Logger,
mtu: options.MTU,
events: make(chan wgTun.Event, 1),
outbound: make(chan *stack.PacketBuffer, 256),
packetOutbound: make(chan *buf.Buffer, 256),
done: make(chan struct{}),
}
ipStack, err := tun.NewGVisorStackWithOptions((*wireEndpoint)(tunDevice), stack.NICOptions{}, true)
if err != nil {
return nil, err
}
var (
inet4Address netip.Addr
inet6Address netip.Addr
)
for _, prefix := range options.Address {
addr := tun.AddressFromAddr(prefix.Addr())
protoAddr := tcpip.ProtocolAddress{
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: addr,
PrefixLen: prefix.Bits(),
},
}
if prefix.Addr().Is4() {
inet4Address = prefix.Addr()
tunDevice.inet4Address = inet4Address
protoAddr.Protocol = ipv4.ProtocolNumber
} else {
inet6Address = prefix.Addr()
tunDevice.inet6Address = inet6Address
protoAddr.Protocol = ipv6.ProtocolNumber
}
gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
if gErr != nil {
return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String())
}
}
tunDevice.stack = ipStack
if options.Handler != nil {
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
}
return tunDevice, nil
}
func (w *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
addr := tcpip.FullAddress{
NIC: tun.DefaultNIC,
Port: destination.Port,
Addr: tun.AddressFromAddr(destination.Addr),
}
bind := tcpip.FullAddress{
NIC: tun.DefaultNIC,
}
var networkProtocol tcpip.NetworkProtocolNumber
if destination.IsIPv4() {
if !w.inet4Address.IsValid() {
return nil, E.New("missing IPv4 local address")
}
networkProtocol = header.IPv4ProtocolNumber
bind.Addr = tun.AddressFromAddr(w.inet4Address)
} else {
if !w.inet6Address.IsValid() {
return nil, E.New("missing IPv6 local address")
}
networkProtocol = header.IPv6ProtocolNumber
bind.Addr = tun.AddressFromAddr(w.inet6Address)
}
switch N.NetworkName(network) {
case N.NetworkTCP:
tcpConn, err := wireguard.DialTCPWithBind(ctx, w.stack, bind, addr, networkProtocol)
if err != nil {
return nil, err
}
return tcpConn, nil
case N.NetworkUDP:
udpConn, err := gonet.DialUDP(w.stack, &bind, &addr, networkProtocol)
if err != nil {
return nil, err
}
return udpConn, nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}
func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
bind := tcpip.FullAddress{
NIC: tun.DefaultNIC,
}
var networkProtocol tcpip.NetworkProtocolNumber
if destination.IsIPv4() {
networkProtocol = header.IPv4ProtocolNumber
bind.Addr = tun.AddressFromAddr(w.inet4Address)
} else {
networkProtocol = header.IPv6ProtocolNumber
bind.Addr = tun.AddressFromAddr(w.inet6Address)
}
udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol)
if err != nil {
return nil, err
}
return udpConn, nil
}
func (w *stackDevice) Start() error {
w.events <- wgTun.EventUp
return nil
}
func (w *stackDevice) File() *os.File {
return nil
}
func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
select {
case packet, ok := <-w.outbound:
if !ok {
return 0, os.ErrClosed
}
defer packet.DecRef()
var copyN int
/*rangeIterate(packet.Data().AsRange(), func(view *buffer.View) {
copyN += copy(bufs[0][offset+copyN:], view.AsSlice())
})*/
for _, view := range packet.AsSlices() {
copyN += copy(bufs[0][offset+copyN:], view)
}
sizes[0] = copyN
return 1, nil
case packet := <-w.packetOutbound:
defer packet.Release()
sizes[0] = copy(bufs[0][offset:], packet.Bytes())
return 1, nil
case <-w.done:
return 0, os.ErrClosed
}
}
func (w *stackDevice) Write(bufs [][]byte, offset int) (count int, err error) {
for _, b := range bufs {
b = b[offset:]
if len(b) == 0 {
continue
}
var networkProtocol tcpip.NetworkProtocolNumber
switch header.IPVersion(b) {
case header.IPv4Version:
networkProtocol = header.IPv4ProtocolNumber
case header.IPv6Version:
networkProtocol = header.IPv6ProtocolNumber
}
packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(b),
})
w.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer)
packetBuffer.DecRef()
count++
}
return
}
func (w *stackDevice) Flush() error {
return nil
}
func (w *stackDevice) MTU() (int, error) {
return int(w.mtu), nil
}
func (w *stackDevice) Name() (string, error) {
return "sing-box", nil
}
func (w *stackDevice) Events() <-chan wgTun.Event {
return w.events
}
func (w *stackDevice) Close() error {
close(w.done)
close(w.events)
w.stack.Close()
for _, endpoint := range w.stack.CleanupEndpoints() {
endpoint.Abort()
}
w.stack.Wait()
return nil
}
func (w *stackDevice) BatchSize() int {
return 1
}
var _ stack.LinkEndpoint = (*wireEndpoint)(nil)
type wireEndpoint stackDevice
func (ep *wireEndpoint) MTU() uint32 {
return ep.mtu
}
func (ep *wireEndpoint) SetMTU(mtu uint32) {
}
func (ep *wireEndpoint) MaxHeaderLength() uint16 {
return 0
}
func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress {
return ""
}
func (ep *wireEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
}
func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities {
return stack.CapabilityRXChecksumOffload
}
func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
ep.dispatcher = dispatcher
}
func (ep *wireEndpoint) IsAttached() bool {
return ep.dispatcher != nil
}
func (ep *wireEndpoint) Wait() {
}
func (ep *wireEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
func (ep *wireEndpoint) AddHeader(buffer *stack.PacketBuffer) {
}
func (ep *wireEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
return true
}
func (ep *wireEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) {
for _, packetBuffer := range list.AsSlice() {
packetBuffer.IncRef()
select {
case <-ep.done:
return 0, &tcpip.ErrClosedForSend{}
case ep.outbound <- packetBuffer:
}
}
return list.Len(), nil
}
func (ep *wireEndpoint) Close() {
}
func (ep *wireEndpoint) SetOnCloseAction(f func()) {
}

View File

@@ -0,0 +1,13 @@
//go:build !with_gvisor
package openvpn
import "github.com/sagernet/sing-tun"
func newStackDevice(options DeviceOptions) (Device, error) {
return nil, tun.ErrGVisorNotIncluded
}
func newSystemStackDevice(options DeviceOptions) (Device, error) {
return nil, tun.ErrGVisorNotIncluded
}

View File

@@ -0,0 +1,250 @@
package openvpn
import (
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"errors"
"fmt"
"hash"
)
const (
KeyMethod2 = 2
keySourcePreMasterSize = 48
keySourceRandomSize = 32
maxCipherKeyLength = 64
maxHMACKeyLength = 64
keyBlockSize = 2 * (maxCipherKeyLength + maxHMACKeyLength)
keyExpansionID = "OpenVPN"
)
type KeySource struct {
PreMaster [keySourcePreMasterSize]byte
Random1 [keySourceRandomSize]byte
Random2 [keySourceRandomSize]byte
}
type KeySource2 struct {
Client KeySource
Server KeySource
}
type KeyMaterial struct {
SendCipherKey []byte
SendHMACKey []byte
RecvCipherKey []byte
RecvHMACKey []byte
}
type KeyMethod2Record struct {
Sources KeySource2
Options string
Username string
Password string
PeerInfo string
}
func NewClientKeyMethod2Record(options, peerInfo, username, password string) (*KeyMethod2Record, error) {
var record KeyMethod2Record
if _, err := rand.Read(record.Sources.Client.PreMaster[:]); err != nil {
return nil, err
}
if _, err := rand.Read(record.Sources.Client.Random1[:]); err != nil {
return nil, err
}
if _, err := rand.Read(record.Sources.Client.Random2[:]); err != nil {
return nil, err
}
record.Options = options
record.PeerInfo = peerInfo
record.Username = username
record.Password = password
return &record, nil
}
func (r *KeyMethod2Record) MarshalClient() ([]byte, error) {
if r == nil {
return nil, errors.New("nil key method 2 record")
}
out := make([]byte, 0, 4+1+keySourcePreMasterSize+keySourceRandomSize*2+len(r.Options)+16)
out = binary.BigEndian.AppendUint32(out, 0)
out = append(out, KeyMethod2)
out = append(out, r.Sources.Client.PreMaster[:]...)
out = append(out, r.Sources.Client.Random1[:]...)
out = append(out, r.Sources.Client.Random2[:]...)
out = appendOpenVPNString(out, r.Options)
out = appendOpenVPNString(out, r.Username)
out = appendOpenVPNString(out, r.Password)
out = appendOpenVPNString(out, r.PeerInfo)
return out, nil
}
func ParseServerKeyMethod2Record(packet []byte) (*KeyMethod2Record, error) {
if len(packet) < 4+1+keySourceRandomSize*2 {
return nil, errors.New("key method 2 packet too short")
}
if binary.BigEndian.Uint32(packet[:4]) != 0 {
return nil, errors.New("invalid key method 2 prefix")
}
if packet[4]&0x0f != KeyMethod2 {
return nil, fmt.Errorf("unsupported key method %d", packet[4])
}
offset := 5
record := &KeyMethod2Record{}
copy(record.Sources.Server.Random1[:], packet[offset:offset+keySourceRandomSize])
offset += keySourceRandomSize
copy(record.Sources.Server.Random2[:], packet[offset:offset+keySourceRandomSize])
offset += keySourceRandomSize
var err error
record.Options, offset, err = readOpenVPNString(packet, offset)
if err != nil {
return nil, fmt.Errorf("read options: %w", err)
}
record.Username, offset, _ = readOpenVPNString(packet, offset)
record.Password, offset, _ = readOpenVPNString(packet, offset)
record.PeerInfo, _, _ = readOpenVPNString(packet, offset)
return record, nil
}
func DeriveClientKeyMaterial(sources KeySource2, clientSession, serverSession SessionID, cipherKeyLen int) (*KeyMaterial, error) {
if cipherKeyLen != 16 && cipherKeyLen != 32 {
return nil, fmt.Errorf("unsupported data cipher key length %d", cipherKeyLen)
}
var master [48]byte
if err := openvpnPRF(
sources.Client.PreMaster[:],
keyExpansionID+" master secret",
sources.Client.Random1[:],
sources.Server.Random1[:],
nil,
nil,
master[:],
); err != nil {
return nil, err
}
keyBlock := make([]byte, keyBlockSize)
if err := openvpnPRF(
master[:],
keyExpansionID+" key expansion",
sources.Client.Random2[:],
sources.Server.Random2[:],
clientSession[:],
serverSession[:],
keyBlock,
); err != nil {
return nil, err
}
clientToServer := keyBlock[:maxCipherKeyLength+maxHMACKeyLength]
serverToClient := keyBlock[maxCipherKeyLength+maxHMACKeyLength:]
return &KeyMaterial{
SendCipherKey: cloneBytes(clientToServer[:cipherKeyLen]),
SendHMACKey: cloneBytes(clientToServer[maxCipherKeyLength : maxCipherKeyLength+maxHMACKeyLength]),
RecvCipherKey: cloneBytes(serverToClient[:cipherKeyLen]),
RecvHMACKey: cloneBytes(serverToClient[maxCipherKeyLength : maxCipherKeyLength+maxHMACKeyLength]),
}, nil
}
func InstallScriptOptionsString(proto, cipher, auth string) string {
protoName := "UDPv4"
if proto == ProtoTCP {
protoName = "TCPv4_CLIENT"
}
keysize := "128"
switch cipher {
case CipherAES192GCM, CipherAES192CBC:
keysize = "192"
case CipherAES256GCM, CipherAES256CBC, CipherCHACHA20POLY:
keysize = "256"
}
return fmt.Sprintf("V4,dev-type tun,link-mtu 1550,tun-mtu 1500,proto %s,cipher %s,auth %s,keysize %s,key-method 2,tls-client", protoName, cipher, auth, keysize)
}
func InstallScriptPeerInfo(cipher string) string {
if cipher != "" {
return "IV_VER=sing-box-openvpn\nIV_PROTO=6\nIV_CIPHERS=" + cipher + "\n"
}
return "IV_VER=sing-box-openvpn\nIV_PROTO=6\nIV_CIPHERS=AES-256-GCM:AES-192-GCM:AES-128-GCM:AES-256-CBC:AES-192-CBC:AES-128-CBC:CHACHA20-POLY1305\n"
}
func appendOpenVPNString(out []byte, s string) []byte {
if s == "" {
return binary.BigEndian.AppendUint16(out, 0)
}
if len(s)+1 > 0xffff {
s = s[:0xfffe]
}
out = binary.BigEndian.AppendUint16(out, uint16(len(s)+1))
out = append(out, s...)
out = append(out, 0)
return out
}
func readOpenVPNString(packet []byte, offset int) (string, int, error) {
if offset+2 > len(packet) {
return "", offset, ioStringEOF
}
size := int(binary.BigEndian.Uint16(packet[offset : offset+2]))
offset += 2
if size == 0 {
return "", offset, nil
}
if offset+size > len(packet) {
return "", offset, ioStringEOF
}
raw := packet[offset : offset+size]
offset += size
if raw[len(raw)-1] == 0 {
raw = raw[:len(raw)-1]
}
return string(raw), offset, nil
}
var ioStringEOF = errors.New("openvpn string truncated")
func openvpnPRF(secret []byte, label string, clientSeed, serverSeed, clientSession, serverSession []byte, out []byte) error {
seed := make([]byte, 0, len(label)+len(clientSeed)+len(serverSeed)+len(clientSession)+len(serverSession))
seed = append(seed, label...)
seed = append(seed, clientSeed...)
seed = append(seed, serverSeed...)
seed = append(seed, clientSession...)
seed = append(seed, serverSession...)
split := (len(secret) + 1) / 2
s1 := secret[:split]
s2 := secret[len(secret)-split:]
md5Out := pHash(md5.New, s1, seed, len(out))
sha1Out := pHash(sha1.New, s2, seed, len(out))
for i := range out {
out[i] = md5Out[i] ^ sha1Out[i]
}
return nil
}
func pHash(newHash func() hash.Hash, secret, seed []byte, size int) []byte {
out := make([]byte, 0, size)
a := hmacSum(newHash, secret, seed)
for len(out) < size {
chunkInput := make([]byte, 0, len(a)+len(seed))
chunkInput = append(chunkInput, a...)
chunkInput = append(chunkInput, seed...)
out = append(out, hmacSum(newHash, secret, chunkInput)...)
a = hmacSum(newHash, secret, a)
}
return out[:size]
}
func hmacSum(newHash func() hash.Hash, key, data []byte) []byte {
mac := hmac.New(newHash, key)
_, _ = mac.Write(data)
return mac.Sum(nil)
}

92
transport/openvpn/mux.go Normal file
View File

@@ -0,0 +1,92 @@
package openvpn
import (
"context"
"net"
"sync"
)
type PacketMux struct {
io PacketIO
control chan []byte
data chan []byte
done chan struct{}
once sync.Once
}
func NewPacketMux(io PacketIO) *PacketMux {
return &PacketMux{
io: io,
control: make(chan []byte, 64),
data: make(chan []byte, 256),
done: make(chan struct{}),
}
}
func (m *PacketMux) Run(ctx context.Context) {
defer m.Close()
for ctx.Err() == nil {
packet, err := m.io.ReadPacket(ctx)
if err != nil {
return
}
if len(packet) == 0 {
continue
}
opcode, _ := parseOpcodeKeyID(packet[0])
ch := m.data
if opcode.IsControl() {
ch = m.control
}
select {
case ch <- packet:
case <-ctx.Done():
return
case <-m.done:
return
}
}
}
func (m *PacketMux) ReadPacket(ctx context.Context) ([]byte, error) {
select {
case packet := <-m.control:
return packet, nil
case <-ctx.Done():
return nil, ctx.Err()
case <-m.done:
return nil, net.ErrClosed
}
}
func (m *PacketMux) ReadDataPacket(ctx context.Context) ([]byte, error) {
select {
case packet := <-m.data:
return packet, nil
case <-ctx.Done():
return nil, ctx.Err()
case <-m.done:
return nil, net.ErrClosed
}
}
func (m *PacketMux) WritePacket(ctx context.Context, packet []byte) error {
return m.io.WritePacket(ctx, packet)
}
func (m *PacketMux) Close() error {
m.once.Do(func() {
close(m.done)
_ = m.io.Close()
})
return nil
}
func (m *PacketMux) LocalAddr() net.Addr {
return m.io.LocalAddr()
}
func (m *PacketMux) RemoteAddr() net.Addr {
return m.io.RemoteAddr()
}

254
transport/openvpn/packet.go Normal file
View File

@@ -0,0 +1,254 @@
package openvpn
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
)
type ControlCrypt interface {
Wrap(header []byte, packetID uint32, unixTime uint32, plaintext []byte) ([]byte, error)
Unwrap(packet []byte) (header []byte, packetID uint32, unixTime uint32, plaintext []byte, err error)
}
const (
KeyIDMask = 0x07
OpcodeShift = 3
PControlHardResetClientV1 Opcode = 1
PControlHardResetServerV1 Opcode = 2
PControlSoftResetV1 Opcode = 3
PControlV1 Opcode = 4
PAckV1 Opcode = 5
PDataV1 Opcode = 6
PControlHardResetClientV2 Opcode = 7
PControlHardResetServerV2 Opcode = 8
PDataV2 Opcode = 9
PControlHardResetClientV3 Opcode = 10
PControlWKCV1 Opcode = 11
SessionIDSize = 8
)
type Opcode uint8
func (o Opcode) String() string {
switch o {
case PControlHardResetClientV1:
return "P_CONTROL_HARD_RESET_CLIENT_V1"
case PControlHardResetServerV1:
return "P_CONTROL_HARD_RESET_SERVER_V1"
case PControlSoftResetV1:
return "P_CONTROL_SOFT_RESET_V1"
case PControlV1:
return "P_CONTROL_V1"
case PAckV1:
return "P_ACK_V1"
case PDataV1:
return "P_DATA_V1"
case PControlHardResetClientV2:
return "P_CONTROL_HARD_RESET_CLIENT_V2"
case PControlHardResetServerV2:
return "P_CONTROL_HARD_RESET_SERVER_V2"
case PDataV2:
return "P_DATA_V2"
case PControlHardResetClientV3:
return "P_CONTROL_HARD_RESET_CLIENT_V3"
case PControlWKCV1:
return "P_CONTROL_WKC_V1"
default:
return "P_UNKNOWN"
}
}
func (o Opcode) IsControl() bool {
switch o {
case PControlHardResetClientV1, PControlHardResetServerV1, PControlSoftResetV1, PControlV1,
PAckV1, PControlHardResetClientV2, PControlHardResetServerV2, PControlHardResetClientV3, PControlWKCV1:
return true
default:
return false
}
}
func (o Opcode) HasMessageID() bool {
return o.IsControl() && o != PAckV1
}
type SessionID [SessionIDSize]byte
func NewSessionID() (SessionID, error) {
var id SessionID
_, err := rand.Read(id[:])
return id, err
}
type ControlPacket struct {
Opcode Opcode
KeyID uint8
LocalSession SessionID
AckIDs []uint32
AckRemoteSession SessionID
MessageID uint32
Payload []byte
}
func opcodeKeyID(opcode Opcode, keyID uint8) byte {
return byte(opcode)<<OpcodeShift | (keyID & KeyIDMask)
}
func parseOpcodeKeyID(b byte) (Opcode, uint8) {
return Opcode(b >> OpcodeShift), b & KeyIDMask
}
func EncodeControlPlain(p ControlPacket) ([]byte, error) {
if !p.Opcode.IsControl() {
return nil, fmt.Errorf("opcode %s is not a control opcode", p.Opcode)
}
if len(p.AckIDs) > 255 {
return nil, fmt.Errorf("too many ack ids: %d", len(p.AckIDs))
}
size := 1 + len(p.AckIDs)*4
if len(p.AckIDs) > 0 {
size += SessionIDSize
}
if p.Opcode.HasMessageID() {
size += 4 + len(p.Payload)
}
out := make([]byte, 0, size)
out = append(out, byte(len(p.AckIDs)))
for _, id := range p.AckIDs {
var b [4]byte
binary.BigEndian.PutUint32(b[:], id)
out = append(out, b[:]...)
}
if len(p.AckIDs) > 0 {
out = append(out, p.AckRemoteSession[:]...)
}
if p.Opcode.HasMessageID() {
var b [4]byte
binary.BigEndian.PutUint32(b[:], p.MessageID)
out = append(out, b[:]...)
out = append(out, p.Payload...)
}
return out, nil
}
func EncodeControlPacket(p ControlPacket) ([]byte, error) {
plain, err := EncodeControlPlain(p)
if err != nil {
return nil, err
}
header := make([]byte, TLSCryptHeaderSize)
header[0] = opcodeKeyID(p.Opcode, p.KeyID)
copy(header[1:], p.LocalSession[:])
out := make([]byte, 0, len(header)+len(plain))
out = append(out, header...)
out = append(out, plain...)
return out, nil
}
func EncodeControlPacketCrypt(p ControlPacket, crypt ControlCrypt, packetID uint32, unixTime uint32) ([]byte, error) {
plain, err := EncodeControlPlain(p)
if err != nil {
return nil, err
}
header := make([]byte, TLSCryptHeaderSize)
header[0] = opcodeKeyID(p.Opcode, p.KeyID)
copy(header[1:], p.LocalSession[:])
return crypt.Wrap(header, packetID, unixTime, plain)
}
func DecodeControlPacket(packet []byte) (*ControlPacket, error) {
if len(packet) < TLSCryptHeaderSize {
return nil, errors.New("control packet too short")
}
opcode, keyID := parseOpcodeKeyID(packet[0])
if !opcode.IsControl() {
return nil, fmt.Errorf("opcode %s is not a control opcode", opcode)
}
var local SessionID
copy(local[:], packet[1:])
plain := packet[TLSCryptHeaderSize:]
ackIDs, ackRemote, messageID, payload, err := DecodeControlPlain(opcode, plain)
if err != nil {
return nil, err
}
return &ControlPacket{
Opcode: opcode,
KeyID: keyID,
LocalSession: local,
AckIDs: ackIDs,
AckRemoteSession: ackRemote,
MessageID: messageID,
Payload: payload,
}, nil
}
func DecodeControlPacketCrypt(crypt ControlCrypt, packet []byte) (*ControlPacket, uint32, uint32, error) {
header, packetID, unixTime, plain, err := crypt.Unwrap(packet)
if err != nil {
return nil, 0, 0, err
}
if len(header) != TLSCryptHeaderSize {
return nil, 0, 0, fmt.Errorf("invalid control header length %d", len(header))
}
opcode, keyID := parseOpcodeKeyID(header[0])
if !opcode.IsControl() {
return nil, 0, 0, fmt.Errorf("opcode %s is not a control opcode", opcode)
}
var local SessionID
copy(local[:], header[1:])
ackIDs, ackRemote, messageID, payload, err := DecodeControlPlain(opcode, plain)
if err != nil {
return nil, 0, 0, err
}
return &ControlPacket{
Opcode: opcode,
KeyID: keyID,
LocalSession: local,
AckIDs: ackIDs,
AckRemoteSession: ackRemote,
MessageID: messageID,
Payload: payload,
}, packetID, unixTime, nil
}
func DecodeControlPlain(opcode Opcode, plain []byte) (ackIDs []uint32, ackRemote SessionID, messageID uint32, payload []byte, err error) {
if len(plain) < 1 {
return nil, SessionID{}, 0, nil, errors.New("control payload too short")
}
ackLen := int(plain[0])
offset := 1
if len(plain) < offset+ackLen*4 {
return nil, SessionID{}, 0, nil, errors.New("control ack array truncated")
}
ackIDs = make([]uint32, ackLen)
for i := 0; i < ackLen; i++ {
ackIDs[i] = binary.BigEndian.Uint32(plain[offset : offset+4])
offset += 4
}
if ackLen > 0 {
if len(plain) < offset+SessionIDSize {
return nil, SessionID{}, 0, nil, errors.New("control ack remote session truncated")
}
copy(ackRemote[:], plain[offset:offset+SessionIDSize])
offset += SessionIDSize
}
if opcode.HasMessageID() {
if len(plain) < offset+4 {
return nil, SessionID{}, 0, nil, errors.New("control message id truncated")
}
messageID = binary.BigEndian.Uint32(plain[offset : offset+4])
offset += 4
payload = cloneBytes(plain[offset:])
} else if len(plain) != offset {
return nil, SessionID{}, 0, nil, errors.New("ack packet has trailing payload")
}
return ackIDs, ackRemote, messageID, payload, nil
}

139
transport/openvpn/push.go Normal file
View File

@@ -0,0 +1,139 @@
package openvpn
import (
"fmt"
"net/netip"
"strconv"
"strings"
)
const PushRequest = "PUSH_REQUEST"
type PushReply struct {
Raw string
Prefixes []netip.Prefix
DNS []netip.Addr
PeerID uint32
Cipher string
Ping uint32
MTU uint32
CompLZO bool
Redirect bool
BlockIPv6 bool
}
func ParsePushReply(message string) (*PushReply, error) {
message = strings.TrimRight(message, "\x00")
if !strings.HasPrefix(message, "PUSH_REPLY") {
return nil, fmt.Errorf("unexpected openvpn push message %q", message)
}
reply := &PushReply{
Raw: message,
PeerID: PeerIDUnset,
}
for _, option := range splitPushOptions(message) {
fields := strings.Fields(option)
if len(fields) == 0 {
continue
}
switch fields[0] {
case "ifconfig":
if len(fields) >= 3 {
prefix, err := parseIPv4Ifconfig(fields[1], fields[2])
if err != nil {
return nil, err
}
reply.Prefixes = append(reply.Prefixes, prefix)
}
case "ifconfig-ipv6":
if len(fields) >= 2 {
prefix, err := netip.ParsePrefix(fields[1])
if err != nil {
return nil, fmt.Errorf("parse pushed ipv6 address %q: %w", fields[1], err)
}
reply.Prefixes = append(reply.Prefixes, prefix)
}
case "dhcp-option":
if len(fields) >= 3 && fields[1] == "DNS" {
if addr, err := netip.ParseAddr(fields[2]); err == nil {
reply.DNS = append(reply.DNS, addr)
}
}
case "peer-id":
if len(fields) >= 2 {
id, err := strconv.ParseUint(fields[1], 10, 24)
if err != nil {
return nil, fmt.Errorf("parse pushed peer-id %q: %w", fields[1], err)
}
reply.PeerID = uint32(id)
}
case "redirect-gateway":
reply.Redirect = true
case "block-ipv6":
reply.BlockIPv6 = true
case "cipher":
if len(fields) >= 2 {
reply.Cipher = fields[1]
}
case "ping":
if len(fields) >= 2 {
if v, err := strconv.ParseUint(fields[1], 10, 32); err == nil {
reply.Ping = uint32(v)
}
}
case "tun-mtu":
if len(fields) >= 2 {
if v, err := strconv.ParseUint(fields[1], 10, 32); err == nil {
reply.MTU = uint32(v)
}
}
case "comp-lzo":
reply.CompLZO = true
}
}
if len(reply.Prefixes) == 0 {
return nil, fmt.Errorf("openvpn push reply missing ifconfig address")
}
return reply, nil
}
func splitPushOptions(message string) []string {
message = strings.TrimRight(message, "\x00")
parts := strings.Split(message, ",")
if len(parts) > 0 && parts[0] == "PUSH_REPLY" {
parts = parts[1:]
}
out := parts[:0]
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}
func parseIPv4Ifconfig(address, mask string) (netip.Prefix, error) {
addr, err := netip.ParseAddr(address)
if err != nil {
return netip.Prefix{}, fmt.Errorf("parse pushed ipv4 address %q: %w", address, err)
}
maskAddr, err := netip.ParseAddr(mask)
if err != nil {
return netip.Prefix{}, fmt.Errorf("parse pushed ipv4 mask %q: %w", mask, err)
}
if !addr.Is4() || !maskAddr.Is4() {
return netip.Prefix{}, fmt.Errorf("openvpn ifconfig requires ipv4 address and mask")
}
maskBytes := maskAddr.As4()
ones := 0
for _, b := range maskBytes {
for i := 7; i >= 0; i-- {
if b&(1<<i) == 0 {
return netip.PrefixFrom(addr, ones), nil
}
ones++
}
}
return netip.PrefixFrom(addr, ones), nil
}

View File

@@ -0,0 +1,100 @@
package openvpn
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"hash"
)
type TLSAuth struct {
sendHMACKey []byte
recvHMACKey []byte
newHash func() hash.Hash
hmacSize int
}
func NewTLSAuth(staticKey []byte, keyDirection int, auth string) (*TLSAuth, error) {
if len(staticKey) != staticKeySize {
return nil, fmt.Errorf("invalid tls-auth static key length %d, expected %d", len(staticKey), staticKeySize)
}
key0 := staticKey[:keySlotSize]
key1 := staticKey[keySlotSize:]
var sendSlot, recvSlot []byte
if keyDirection == 1 {
sendSlot = key1
recvSlot = key0
} else {
sendSlot = key0
recvSlot = key1
}
var newHash func() hash.Hash
var hmacSize int
switch auth {
case AuthSHA256:
newHash = sha256.New
hmacSize = sha256.Size
case AuthSHA384:
newHash = sha512.New384
hmacSize = 48
case AuthSHA512:
newHash = sha512.New
hmacSize = sha512.Size
default:
newHash = sha1.New
hmacSize = sha1.Size
}
return &TLSAuth{
sendHMACKey: cloneBytes(sendSlot[64 : 64+hmacSize]),
recvHMACKey: cloneBytes(recvSlot[64 : 64+hmacSize]),
newHash: newHash,
hmacSize: hmacSize,
}, nil
}
func (a *TLSAuth) Wrap(header []byte, packetID uint32, unixTime uint32, plaintext []byte) ([]byte, error) {
if len(header) != TLSCryptHeaderSize {
return nil, fmt.Errorf("invalid tls-auth header length %d, expected %d", len(header), TLSCryptHeaderSize)
}
var pid [TLSCryptPIDSize]byte
binary.BigEndian.PutUint32(pid[:4], packetID)
binary.BigEndian.PutUint32(pid[4:], unixTime)
mac := hmac.New(a.newHash, a.sendHMACKey)
mac.Write(pid[:])
mac.Write(header)
mac.Write(plaintext)
tag := mac.Sum(nil)
out := make([]byte, 0, len(header)+a.hmacSize+TLSCryptPIDSize+len(plaintext))
out = append(out, header...)
out = append(out, tag...)
out = append(out, pid[:]...)
out = append(out, plaintext...)
return out, nil
}
func (a *TLSAuth) Unwrap(packet []byte) (header []byte, packetID uint32, unixTime uint32, plaintext []byte, err error) {
minLen := TLSCryptHeaderSize + a.hmacSize + TLSCryptPIDSize
if len(packet) < minLen {
return nil, 0, 0, nil, errors.New("tls-auth packet too short")
}
header = cloneBytes(packet[:TLSCryptHeaderSize])
tag := packet[TLSCryptHeaderSize : TLSCryptHeaderSize+a.hmacSize]
pidStart := TLSCryptHeaderSize + a.hmacSize
pid := packet[pidStart : pidStart+TLSCryptPIDSize]
plaintext = cloneBytes(packet[pidStart+TLSCryptPIDSize:])
mac := hmac.New(a.newHash, a.recvHMACKey)
mac.Write(pid)
mac.Write(header)
mac.Write(plaintext)
tagCheck := mac.Sum(nil)
if !hmac.Equal(tag, tagCheck) {
return nil, 0, 0, nil, errors.New("tls-auth authentication failed")
}
packetID = binary.BigEndian.Uint32(pid[:4])
unixTime = binary.BigEndian.Uint32(pid[4:])
return header, packetID, unixTime, plaintext, nil
}

View File

@@ -0,0 +1,128 @@
package openvpn
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
)
const (
TLSCryptHeaderSize = 1 + 8
TLSCryptPIDSize = 4 + 4
TLSCryptTagSize = sha256.Size
staticKeySize = 256
keySlotSize = 128
cipherKeySize = 32
hmacKeySize = 32
)
type TLSCrypt struct {
encryptCipherKey []byte
encryptHMACKey []byte
decryptCipherKey []byte
decryptHMACKey []byte
}
func NewTLSCrypt(staticKey []byte, client bool) (*TLSCrypt, error) {
if len(staticKey) != staticKeySize {
return nil, fmt.Errorf("invalid tls-crypt static key length %d, expected %d", len(staticKey), staticKeySize)
}
key0 := staticKey[:keySlotSize]
key1 := staticKey[keySlotSize:]
encrypt := key0
decrypt := key1
if client {
encrypt = key1
decrypt = key0
}
return &TLSCrypt{
encryptCipherKey: cloneBytes(encrypt[:cipherKeySize]),
encryptHMACKey: cloneBytes(encrypt[64 : 64+hmacKeySize]),
decryptCipherKey: cloneBytes(decrypt[:cipherKeySize]),
decryptHMACKey: cloneBytes(decrypt[64 : 64+hmacKeySize]),
}, nil
}
func (c *TLSCrypt) Wrap(header []byte, packetID uint32, unixTime uint32, plaintext []byte) ([]byte, error) {
if len(header) != TLSCryptHeaderSize {
return nil, fmt.Errorf("invalid tls-crypt header length %d, expected %d", len(header), TLSCryptHeaderSize)
}
ad := make([]byte, 0, len(header)+TLSCryptPIDSize)
ad = append(ad, header...)
var pid [TLSCryptPIDSize]byte
binary.BigEndian.PutUint32(pid[:4], packetID)
binary.BigEndian.PutUint32(pid[4:], unixTime)
ad = append(ad, pid[:]...)
tag := c.hmac(c.encryptHMACKey, ad, plaintext)
ciphertext, err := aes256ctr(c.encryptCipherKey, tag[:aes.BlockSize], plaintext)
if err != nil {
return nil, err
}
out := make([]byte, 0, len(ad)+len(tag)+len(ciphertext))
out = append(out, ad...)
out = append(out, tag...)
out = append(out, ciphertext...)
return out, nil
}
func (c *TLSCrypt) Unwrap(packet []byte) (header []byte, packetID uint32, unixTime uint32, plaintext []byte, err error) {
if len(packet) < TLSCryptHeaderSize+TLSCryptPIDSize+TLSCryptTagSize {
return nil, 0, 0, nil, errors.New("tls-crypt packet too short")
}
header = cloneBytes(packet[:TLSCryptHeaderSize])
adEnd := TLSCryptHeaderSize + TLSCryptPIDSize
tagEnd := adEnd + TLSCryptTagSize
ad := packet[:adEnd]
tag := packet[adEnd:tagEnd]
ciphertext := packet[tagEnd:]
plaintext, err = aes256ctr(c.decryptCipherKey, tag[:aes.BlockSize], ciphertext)
if err != nil {
return nil, 0, 0, nil, err
}
tagCheck := c.hmac(c.decryptHMACKey, ad, plaintext)
if !hmac.Equal(tag, tagCheck) {
return nil, 0, 0, nil, errors.New("tls-crypt authentication failed")
}
packetID = binary.BigEndian.Uint32(packet[TLSCryptHeaderSize : TLSCryptHeaderSize+4])
unixTime = binary.BigEndian.Uint32(packet[TLSCryptHeaderSize+4 : adEnd])
return header, packetID, unixTime, plaintext, nil
}
func (c *TLSCrypt) hmac(key []byte, parts ...[]byte) []byte {
mac := hmac.New(sha256.New, key)
for _, part := range parts {
_, _ = mac.Write(part)
}
return mac.Sum(nil)
}
func aes256ctr(key, iv, in []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
out := cloneBytes(in)
cipher.NewCTR(block, iv).XORKeyStream(out, out)
return out, nil
}
func cloneBytes(in []byte) []byte {
out := make([]byte, len(in))
copy(out, in)
return out
}

284
transport/openvpn/tunnel.go Normal file
View File

@@ -0,0 +1,284 @@
package openvpn
import (
"bytes"
"context"
"fmt"
"net"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/tls"
)
type TunnelOptions struct {
Dialer N.Dialer
Servers []option.ServerOptions
TLSConfig tls.Config
Config *ClientConfig
UDPTimeout time.Duration
ReconnectDelay time.Duration
PingInterval time.Duration
}
type Tunnel struct {
ctx context.Context
logger logger.ContextLogger
options TunnelOptions
device Device
client *Client
mtu uint32
serverIndex int
await chan struct{}
mu sync.Mutex
}
func NewTunnel(ctx context.Context, logger logger.ContextLogger, options TunnelOptions) (*Tunnel, error) {
if options.ReconnectDelay == 0 {
options.ReconnectDelay = 5 * time.Second
}
return &Tunnel{
ctx: ctx,
logger: logger,
options: options,
await: make(chan struct{}),
}, nil
}
func (t *Tunnel) Start() error {
go func() {
defer close(t.await)
client, err := t.getClient()
if err != nil {
t.logger.Error("OpenVPN connect: ", err)
return
}
t.mtu = 1500
if client.push.MTU > 0 {
t.mtu = client.push.MTU
}
deviceOptions := DeviceOptions{
Context: t.ctx,
Logger: t.logger,
UDPTimeout: t.options.UDPTimeout,
MTU: t.mtu,
Address: client.push.Prefixes,
}
device, err := NewDevice(deviceOptions)
if err != nil {
client.Close()
t.logger.Error("create OpenVPN device: ", err)
return
}
t.device = device
if err := device.Start(); err != nil {
client.Close()
t.logger.Error("start OpenVPN device: ", err)
return
}
t.maintainTunnel()
}()
return nil
}
func (t *Tunnel) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if !destination.Addr.IsValid() {
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
}
return t.device.DialContext(ctx, network, destination)
}
func (t *Tunnel) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if !destination.Addr.IsValid() {
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
}
return t.device.ListenPacket(ctx, destination)
}
func (t *Tunnel) Close() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.client != nil {
t.client.Close()
t.client = nil
}
if t.device != nil {
return t.device.Close()
}
return nil
}
func (t *Tunnel) isTunnelInitialized(ctx context.Context) error {
select {
case <-t.await:
case <-ctx.Done():
return ctx.Err()
}
if t.device == nil {
return E.New("endpoint not initialized")
}
return nil
}
func (t *Tunnel) maintainTunnel() {
go func() {
bufs := make([][]byte, 1)
bufs[0] = make([]byte, t.mtu)
sizes := make([]int, 1)
for t.ctx.Err() == nil {
_, err := t.device.Read(bufs, sizes, 0)
if err != nil {
if t.ctx.Err() != nil {
return
}
continue
}
client, err := t.getClient()
if err != nil {
return
}
if err := client.WriteIPPacket(t.ctx, bufs[0][:sizes[0]]); err != nil {
if t.ctx.Err() != nil {
return
}
}
}
}()
go func() {
for t.ctx.Err() == nil {
client, err := t.getClient()
if err != nil {
return
}
packet, err := client.ReadIPPacket(t.ctx)
if err != nil {
if t.ctx.Err() != nil {
return
}
if ok := t.closeClient(client); ok {
t.logger.ErrorContext(t.ctx, fmt.Errorf("connection lost: %v", err))
}
continue
}
if bytes.Equal(packet, pingPayload) {
continue
}
if _, err := t.device.Write([][]byte{packet}, 0); err != nil {
if t.ctx.Err() != nil {
return
}
}
}
}()
pingInterval := t.options.PingInterval
if pingInterval == 0 && t.client != nil && t.client.push.Ping > 0 {
pingInterval = time.Duration(t.client.push.Ping) * time.Second
}
if pingInterval > 0 {
go func() {
ticker := time.NewTicker(pingInterval)
defer ticker.Stop()
for {
select {
case <-t.ctx.Done():
return
case <-ticker.C:
client, err := t.getClient()
if err != nil {
return
}
client.WriteIPPacket(t.ctx, pingPayload)
}
}
}()
}
<-t.ctx.Done()
}
func (t *Tunnel) getClient() (*Client, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.ctx.Err() != nil {
return nil, t.ctx.Err()
}
if t.client != nil {
return t.client, nil
}
timer := time.NewTimer(0)
defer timer.Stop()
for {
t.logger.InfoContext(t.ctx, "connecting to OpenVPN server")
client, err := t.connect()
if err != nil {
t.logger.ErrorContext(t.ctx, fmt.Errorf("connect failed: %v", err))
timer.Reset(t.options.ReconnectDelay)
select {
case <-t.ctx.Done():
return nil, t.ctx.Err()
case <-timer.C:
}
continue
}
t.client = client
t.logger.InfoContext(t.ctx, "connected to OpenVPN server")
return client, nil
}
}
func (t *Tunnel) closeClient(client *Client) bool {
t.mu.Lock()
defer t.mu.Unlock()
if client == t.client {
t.client.Close()
t.client = nil
return true
}
return false
}
func (t *Tunnel) connect() (*Client, error) {
config := t.options.Config
server := t.options.Servers[t.serverIndex].Build()
t.serverIndex = (t.serverIndex + 1) % len(t.options.Servers)
connectCtx, cancel := context.WithTimeout(t.ctx, t.options.ReconnectDelay)
defer cancel()
var conn net.Conn
var err error
if config.Proto == ProtoTCP {
conn, err = t.options.Dialer.DialContext(connectCtx, N.NetworkTCP, server)
} else {
conn, err = t.options.Dialer.DialContext(connectCtx, N.NetworkUDP, server)
}
if err != nil {
return nil, fmt.Errorf("dial openvpn server: %w", err)
}
var packetIO PacketIO
if config.Proto == ProtoTCP {
packetIO = NewTCPPacketIO(conn)
} else {
packetIO = NewDatagramPacketIO(conn)
}
client, err := NewClient(config, packetIO, t.options.TLSConfig)
if err != nil {
conn.Close()
return nil, err
}
_, err = client.Handshake(connectCtx)
if err != nil {
client.Close()
return nil, fmt.Errorf("openvpn handshake: %w", err)
}
return client, nil
}
var pingPayload = []byte{
0x2a, 0x18, 0x7b, 0xf3, 0x64, 0x1e, 0xb4, 0xcb,
0x07, 0xed, 0x2d, 0x0a, 0x98, 0x1f, 0xc7, 0x48,
}

100
transport/sudoku/address.go Normal file
View File

@@ -0,0 +1,100 @@
package sudoku
import (
"encoding/binary"
"fmt"
"io"
"net"
"strconv"
"strings"
)
func EncodeAddress(rawAddr string) ([]byte, error) {
host, portStr, err := net.SplitHostPort(rawAddr)
if err != nil {
return nil, err
}
portInt, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return nil, err
}
var buf []byte
if i := strings.IndexByte(host, '%'); i >= 0 {
// Zone identifiers are not representable in SOCKS5 IPv6 address encoding.
host = host[:i]
}
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
buf = append(buf, 0x01) // IPv4
buf = append(buf, ip4...)
} else {
buf = append(buf, 0x04) // IPv6
ip16 := ip.To16()
if ip16 == nil {
return nil, fmt.Errorf("invalid ipv6: %q", host)
}
buf = append(buf, ip16...)
}
} else {
if len(host) > 255 {
return nil, fmt.Errorf("domain too long")
}
buf = append(buf, 0x03) // domain
buf = append(buf, byte(len(host)))
buf = append(buf, host...)
}
var portBytes [2]byte
binary.BigEndian.PutUint16(portBytes[:], uint16(portInt))
buf = append(buf, portBytes[:]...)
return buf, nil
}
func DecodeAddress(r io.Reader) (string, error) {
var atyp [1]byte
if _, err := io.ReadFull(r, atyp[:]); err != nil {
return "", err
}
switch atyp[0] {
case 0x01: // IPv4
var ipBuf [net.IPv4len]byte
if _, err := io.ReadFull(r, ipBuf[:]); err != nil {
return "", err
}
var portBuf [2]byte
if _, err := io.ReadFull(r, portBuf[:]); err != nil {
return "", err
}
return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil
case 0x04: // IPv6
var ipBuf [net.IPv6len]byte
if _, err := io.ReadFull(r, ipBuf[:]); err != nil {
return "", err
}
var portBuf [2]byte
if _, err := io.ReadFull(r, portBuf[:]); err != nil {
return "", err
}
return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil
case 0x03: // domain
var lengthBuf [1]byte
if _, err := io.ReadFull(r, lengthBuf[:]); err != nil {
return "", err
}
l := int(lengthBuf[0])
hostBuf := make([]byte, l)
if _, err := io.ReadFull(r, hostBuf); err != nil {
return "", err
}
var portBuf [2]byte
if _, err := io.ReadFull(r, portBuf[:]); err != nil {
return "", err
}
return net.JoinHostPort(string(hostBuf), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil
default:
return "", fmt.Errorf("unknown address type: %d", atyp[0])
}
}

212
transport/sudoku/config.go Normal file
View File

@@ -0,0 +1,212 @@
package sudoku
import (
"fmt"
"strings"
"github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
// ProtocolConfig defines the configuration for the Sudoku protocol stack.
// It is intentionally kept close to the upstream Sudoku project to ensure wire compatibility.
type ProtocolConfig struct {
// Client-only: "host:port".
ServerAddress string
// Pre-shared key (or ED25519 key material) used to derive crypto and tables.
Key string
// "aes-128-gcm", "chacha20-poly1305", or "none".
AEADMethod string
// Table is the single obfuscation table to use when table rotation is disabled.
Table *sudoku.Table
// Tables is an optional candidate set for table rotation.
// If provided (len>0), the client will pick one table per connection and the server will
// probe the handshake to detect which one was used, keeping the handshake format unchanged.
// When Tables is set, Table may be nil.
Tables []*sudoku.Table
// Padding insertion ratio (0-100). Must satisfy PaddingMax >= PaddingMin.
PaddingMin int
PaddingMax int
// EnablePureDownlink enables the pure Sudoku downlink mode.
// When false, the connection uses the bandwidth-optimized packed downlink.
EnablePureDownlink bool
// Client-only: final target "host:port".
TargetAddress string
// Server-side handshake timeout (seconds).
HandshakeTimeoutSeconds int
// DisableHTTPMask disables all HTTP camouflage layers.
DisableHTTPMask bool
// HTTPMaskMode controls how the HTTP layer behaves:
// - "legacy": write a fake HTTP/1.1 header then switch to raw stream (default, not CDN-compatible)
// - "stream": real HTTP tunnel (split-stream), CDN-compatible
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
// - "auto": try stream then fall back to poll
// - "ws": WebSocket tunnel (GET upgrade), CDN-friendly
HTTPMaskMode string
// HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side).
// If false, the tunnel uses HTTP (no port-based inference).
HTTPMaskTLSEnabled bool
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
HTTPMaskHost string
// HTTPMaskPathRoot optionally prefixes all HTTP mask paths with a first-level segment.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
HTTPMaskPathRoot string
// HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled:
// - "off": disable reuse; each Dial establishes its own HTTPMask tunnel
// - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2)
// - "on": enable "single tunnel, multi-target" mux (Sudoku-level multiplex; Dial behaves like "auto" otherwise)
HTTPMaskMultiplex string
}
func (c *ProtocolConfig) Validate() error {
if c.Table == nil && len(c.Tables) == 0 {
return fmt.Errorf("table cannot be nil (or provide tables)")
}
for i, t := range c.Tables {
if t == nil {
return fmt.Errorf("tables[%d] cannot be nil", i)
}
}
if c.Key == "" {
return fmt.Errorf("key cannot be empty")
}
switch c.AEADMethod {
case "aes-128-gcm", "chacha20-poly1305", "none":
default:
return fmt.Errorf("invalid aead-method: %s, must be one of: aes-128-gcm, chacha20-poly1305, none", c.AEADMethod)
}
if c.PaddingMin < 0 || c.PaddingMin > 100 {
return fmt.Errorf("padding-min must be between 0 and 100, got %d", c.PaddingMin)
}
if c.PaddingMax < 0 || c.PaddingMax > 100 {
return fmt.Errorf("padding-max must be between 0 and 100, got %d", c.PaddingMax)
}
if c.PaddingMax < c.PaddingMin {
return fmt.Errorf("padding-max (%d) must be >= padding-min (%d)", c.PaddingMax, c.PaddingMin)
}
if c.HandshakeTimeoutSeconds < 0 {
return fmt.Errorf("handshake-timeout must be >= 0, got %d", c.HandshakeTimeoutSeconds)
}
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) {
case "", "legacy", "stream", "poll", "auto", "ws":
default:
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto, ws", c.HTTPMaskMode)
}
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
v = strings.Trim(v, "/")
if v == "" || strings.Contains(v, "/") {
return fmt.Errorf("invalid http-mask-path-root: must be a single path segment")
}
for i := 0; i < len(v); i++ {
ch := v[i]
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case ch == '_' || ch == '-':
default:
return fmt.Errorf("invalid http-mask-path-root: contains invalid character %q", ch)
}
}
}
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMultiplex)) {
case "", "off", "auto", "on":
default:
return fmt.Errorf("invalid http-mask-multiplex: %s, must be one of: off, auto, on", c.HTTPMaskMultiplex)
}
return nil
}
func (c *ProtocolConfig) ValidateClient() error {
if err := c.Validate(); err != nil {
return err
}
if c.ServerAddress == "" {
return fmt.Errorf("server address cannot be empty")
}
if c.TargetAddress == "" {
return fmt.Errorf("target address cannot be empty")
}
return nil
}
func DefaultConfig() *ProtocolConfig {
return &ProtocolConfig{
AEADMethod: "chacha20-poly1305",
PaddingMin: 10,
PaddingMax: 30,
EnablePureDownlink: true,
HandshakeTimeoutSeconds: 5,
HTTPMaskMode: "legacy",
HTTPMaskMultiplex: "off",
}
}
func DerefInt(v *int, def int) int {
if v == nil {
return def
}
return *v
}
func DerefBool(v *bool, def bool) bool {
if v == nil {
return def
}
return *v
}
// ResolvePadding applies defaults and keeps min/max consistent when only one side is provided.
func ResolvePadding(min, max *int, defMin, defMax int) (int, int) {
paddingMin := DerefInt(min, defMin)
paddingMax := DerefInt(max, defMax)
switch {
case min == nil && max != nil && paddingMax < paddingMin:
paddingMin = paddingMax
case max == nil && min != nil && paddingMax < paddingMin:
paddingMax = paddingMin
}
return paddingMin, paddingMax
}
func NormalizeTableType(tableType string) (string, error) {
normalized, err := sudoku.NormalizeASCIIMode(tableType)
if err != nil {
return "", fmt.Errorf("table-type must be prefer_ascii, prefer_entropy, up_ascii_down_entropy, or up_entropy_down_ascii")
}
return normalized, nil
}
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
if c == nil {
return nil
}
if len(c.Tables) > 0 {
return c.Tables
}
if c.Table != nil {
return []*sudoku.Table{c.Table}
}
return nil
}

View File

@@ -0,0 +1,150 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"golang.org/x/crypto/chacha20poly1305"
)
type AEADConn struct {
net.Conn
aead cipher.AEAD
readBuf bytes.Buffer
nonceSize int
}
func (cc *AEADConn) CloseWrite() error {
if cc == nil || cc.Conn == nil {
return nil
}
if cw, ok := cc.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (cc *AEADConn) CloseRead() error {
if cc == nil || cc.Conn == nil {
return nil
}
if cr, ok := cc.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewAEADConn(c net.Conn, key string, method string) (*AEADConn, error) {
if method == "none" {
return &AEADConn{Conn: c, aead: nil}, nil
}
h := sha256.New()
h.Write([]byte(key))
keyBytes := h.Sum(nil)
var aead cipher.AEAD
var err error
switch method {
case "aes-128-gcm":
block, _ := aes.NewCipher(keyBytes[:16])
aead, err = cipher.NewGCM(block)
case "chacha20-poly1305":
aead, err = chacha20poly1305.New(keyBytes)
default:
return nil, fmt.Errorf("unsupported cipher: %s", method)
}
if err != nil {
return nil, err
}
return &AEADConn{
Conn: c,
aead: aead,
nonceSize: aead.NonceSize(),
}, nil
}
func (cc *AEADConn) Write(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Write(p)
}
maxPayload := 65535 - cc.nonceSize - cc.aead.Overhead()
totalWritten := 0
var frameBuf bytes.Buffer
header := make([]byte, 2)
nonce := make([]byte, cc.nonceSize)
for len(p) > 0 {
chunkSize := len(p)
if chunkSize > maxPayload {
chunkSize = maxPayload
}
chunk := p[:chunkSize]
p = p[chunkSize:]
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return totalWritten, err
}
ciphertext := cc.aead.Seal(nil, nonce, chunk, nil)
frameLen := len(nonce) + len(ciphertext)
binary.BigEndian.PutUint16(header, uint16(frameLen))
frameBuf.Reset()
frameBuf.Write(header)
frameBuf.Write(nonce)
frameBuf.Write(ciphertext)
if _, err := cc.Conn.Write(frameBuf.Bytes()); err != nil {
return totalWritten, err
}
totalWritten += chunkSize
}
return totalWritten, nil
}
func (cc *AEADConn) Read(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Read(p)
}
if cc.readBuf.Len() > 0 {
return cc.readBuf.Read(p)
}
header := make([]byte, 2)
if _, err := io.ReadFull(cc.Conn, header); err != nil {
return 0, err
}
frameLen := int(binary.BigEndian.Uint16(header))
body := make([]byte, frameLen)
if _, err := io.ReadFull(cc.Conn, body); err != nil {
return 0, err
}
if len(body) < cc.nonceSize {
return 0, errors.New("frame too short")
}
nonce := body[:cc.nonceSize]
ciphertext := body[cc.nonceSize:]
plaintext, err := cc.aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return 0, errors.New("decryption failed")
}
cc.readBuf.Write(plaintext)
return cc.readBuf.Read(p)
}

View File

@@ -0,0 +1,116 @@
package crypto
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"filippo.io/edwards25519"
)
// KeyPair holds the scalar private key and point public key
type KeyPair struct {
Private *edwards25519.Scalar
Public *edwards25519.Point
}
// GenerateMasterKey generates a random master private key (scalar) and its public key (point)
func GenerateMasterKey() (*KeyPair, error) {
// 1. Generate random scalar x (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return nil, err
}
x, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return nil, err
}
// 2. Calculate Public Key P = x * G
P := new(edwards25519.Point).ScalarBaseMult(x)
return &KeyPair{Private: x, Public: P}, nil
}
// SplitPrivateKey takes a master private key x and returns a new random split key (r, k)
// such that x = r + k (mod L).
// Returns hex encoded string of r || k (64 bytes)
func SplitPrivateKey(x *edwards25519.Scalar) (string, error) {
// 1. Generate random r (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return "", err
}
r, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return "", err
}
// 2. Calculate k = x - r (mod L)
k := new(edwards25519.Scalar).Subtract(x, r)
// 3. Encode r and k
rBytes := r.Bytes()
kBytes := k.Bytes()
full := make([]byte, 64)
copy(full[:32], rBytes)
copy(full[32:], kBytes)
return hex.EncodeToString(full), nil
}
// RecoverPublicKey takes a split private key (r, k) or a master private key (x)
// and returns the public key P.
// Input can be:
// - 32 bytes hex (Master Scalar x)
// - 64 bytes hex (Split Key r || k)
func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) {
keyBytes, err := hex.DecodeString(keyHex)
if err != nil {
return nil, fmt.Errorf("invalid hex: %w", err)
}
if len(keyBytes) == 32 {
// Master Key x
x, err := edwards25519.NewScalar().SetCanonicalBytes(keyBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar: %w", err)
}
return new(edwards25519.Point).ScalarBaseMult(x), nil
}
if len(keyBytes) == 64 {
// Split Key r || k
rBytes := keyBytes[:32]
kBytes := keyBytes[32:]
r, err := edwards25519.NewScalar().SetCanonicalBytes(rBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar r: %w", err)
}
k, err := edwards25519.NewScalar().SetCanonicalBytes(kBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar k: %w", err)
}
// sum = r + k
sum := new(edwards25519.Scalar).Add(r, k)
// P = sum * G
return new(edwards25519.Point).ScalarBaseMult(sum), nil
}
return nil, errors.New("invalid key length: must be 32 bytes (Master) or 64 bytes (Split)")
}
// EncodePoint returns the hex string of the compressed point
func EncodePoint(p *edwards25519.Point) string {
return hex.EncodeToString(p.Bytes())
}
// EncodeScalar returns the hex string of the scalar
func EncodeScalar(s *edwards25519.Scalar) string {
return hex.EncodeToString(s.Bytes())
}

View File

@@ -0,0 +1,452 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"golang.org/x/crypto/chacha20poly1305"
)
// KeyUpdateAfterBytes controls automatic key rotation based on plaintext bytes.
// It is a package var (not config) to enable targeted tests with smaller thresholds.
var KeyUpdateAfterBytes int64 = 32 << 20 // 32 MiB
const (
recordHeaderSize = 12 // epoch(uint32) + seq(uint64) - also used as nonce+AAD.
maxFrameBodySize = 65535
)
type recordKeys struct {
baseSend []byte
baseRecv []byte
}
// RecordConn is a framed AEAD net.Conn with:
// - deterministic per-record nonce (epoch+seq)
// - per-direction key rotation (epoch), driven by plaintext byte counters
// - replay/out-of-order protection within the connection (strict seq check)
//
// Wire format per record:
// - uint16 bodyLen
// - header[12] = epoch(uint32 BE) || seq(uint64 BE) (plaintext)
// - ciphertext = AEAD(header as nonce, plaintext, header as AAD)
type RecordConn struct {
net.Conn
method string
writeMu sync.Mutex
readMu sync.Mutex
keys recordKeys
sendAEAD cipher.AEAD
sendAEADEpoch uint32
recvAEAD cipher.AEAD
recvAEADEpoch uint32
// Send direction state.
sendEpoch uint32
sendSeq uint64
sendBytes int64
sendEpochUpdates uint32
// Receive direction state.
recvEpoch uint32
recvSeq uint64
recvInitialized bool
readBuf bytes.Buffer
// writeFrame is a reusable buffer for [len||header||ciphertext] on the wire.
// Guarded by writeMu.
writeFrame []byte
}
func (c *RecordConn) CloseWrite() error {
if c == nil {
return nil
}
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (c *RecordConn) CloseRead() error {
if c == nil {
return nil
}
if cr, ok := c.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewRecordConn(conn net.Conn, method string, baseSend, baseRecv []byte) (*RecordConn, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
method = normalizeAEADMethod(method)
if method != "none" {
if err := validateBaseKey(baseSend); err != nil {
return nil, fmt.Errorf("invalid send base key: %w", err)
}
if err := validateBaseKey(baseRecv); err != nil {
return nil, fmt.Errorf("invalid recv base key: %w", err)
}
}
rc := &RecordConn{Conn: conn, method: method}
rc.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)}
if err := rc.resetTrafficState(); err != nil {
return nil, err
}
return rc, nil
}
func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error {
if c == nil {
return fmt.Errorf("nil conn")
}
if c.method != "none" {
if err := validateBaseKey(baseSend); err != nil {
return fmt.Errorf("invalid send base key: %w", err)
}
if err := validateBaseKey(baseRecv); err != nil {
return fmt.Errorf("invalid recv base key: %w", err)
}
}
c.writeMu.Lock()
c.readMu.Lock()
defer c.readMu.Unlock()
defer c.writeMu.Unlock()
c.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)}
if err := c.resetTrafficState(); err != nil {
return err
}
c.readBuf.Reset()
c.sendAEAD = nil
c.recvAEAD = nil
c.sendAEADEpoch = 0
c.recvAEADEpoch = 0
return nil
}
func (c *RecordConn) resetTrafficState() error {
sendEpoch, sendSeq, err := randomRecordCounters()
if err != nil {
return fmt.Errorf("initialize record counters: %w", err)
}
c.sendEpoch = sendEpoch
c.sendSeq = sendSeq
c.sendBytes = 0
c.sendEpochUpdates = 0
c.recvEpoch = 0
c.recvSeq = 0
c.recvInitialized = false
return nil
}
func normalizeAEADMethod(method string) string {
switch method {
case "", "chacha20-poly1305":
return "chacha20-poly1305"
case "aes-128-gcm", "none":
return method
default:
return method
}
}
func validateBaseKey(b []byte) error {
if len(b) < 32 {
return fmt.Errorf("need at least 32 bytes, got %d", len(b))
}
return nil
}
func cloneBytes(b []byte) []byte {
if len(b) == 0 {
return nil
}
return append([]byte(nil), b...)
}
func randomRecordCounters() (uint32, uint64, error) {
epoch, err := randomNonZeroUint32()
if err != nil {
return 0, 0, err
}
seq, err := randomNonZeroUint64()
if err != nil {
return 0, 0, err
}
return epoch, seq, nil
}
func randomNonZeroUint32() (uint32, error) {
var b [4]byte
for {
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
return 0, err
}
v := binary.BigEndian.Uint32(b[:])
if v != 0 && v != ^uint32(0) {
return v, nil
}
}
}
func randomNonZeroUint64() (uint64, error) {
var b [8]byte
for {
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
return 0, err
}
v := binary.BigEndian.Uint64(b[:])
if v != 0 && v != ^uint64(0) {
return v, nil
}
}
}
func (c *RecordConn) newAEADFor(base []byte, epoch uint32) (cipher.AEAD, error) {
if c.method == "none" {
return nil, nil
}
key := deriveEpochKey(base, epoch, c.method)
switch c.method {
case "aes-128-gcm":
block, err := aes.NewCipher(key[:16])
if err != nil {
return nil, err
}
a, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if a.NonceSize() != recordHeaderSize {
return nil, fmt.Errorf("unexpected gcm nonce size: %d", a.NonceSize())
}
return a, nil
case "chacha20-poly1305":
a, err := chacha20poly1305.New(key[:32])
if err != nil {
return nil, err
}
if a.NonceSize() != recordHeaderSize {
return nil, fmt.Errorf("unexpected chacha nonce size: %d", a.NonceSize())
}
return a, nil
default:
return nil, fmt.Errorf("unsupported cipher: %s", c.method)
}
}
func deriveEpochKey(base []byte, epoch uint32, method string) []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], epoch)
mac := hmac.New(sha256.New, base)
_, _ = mac.Write([]byte("sudoku-record:"))
_, _ = mac.Write([]byte(method))
_, _ = mac.Write(b[:])
return mac.Sum(nil)
}
func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) error {
ku := atomic.LoadInt64(&KeyUpdateAfterBytes)
if ku <= 0 || c.method == "none" {
return nil
}
c.sendBytes += int64(addedPlain)
threshold := ku * int64(c.sendEpochUpdates+1)
if c.sendBytes < threshold {
return nil
}
c.sendEpoch++
c.sendEpochUpdates++
nextSeq, err := randomNonZeroUint64()
if err != nil {
return fmt.Errorf("rotate record seq: %w", err)
}
c.sendSeq = nextSeq
return nil
}
func (c *RecordConn) validateRecvPosition(epoch uint32, seq uint64) error {
if !c.recvInitialized {
return nil
}
if epoch < c.recvEpoch {
return fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch)
}
if epoch == c.recvEpoch && seq != c.recvSeq {
return fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq)
}
if epoch > c.recvEpoch {
const maxJump = 8
if epoch-c.recvEpoch > maxJump {
return fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump)
}
}
return nil
}
func (c *RecordConn) markRecvPosition(epoch uint32, seq uint64) {
c.recvEpoch = epoch
c.recvSeq = seq + 1
c.recvInitialized = true
}
func (c *RecordConn) Write(p []byte) (int, error) {
if c == nil || c.Conn == nil {
return 0, net.ErrClosed
}
if c.method == "none" {
return c.Conn.Write(p)
}
c.writeMu.Lock()
defer c.writeMu.Unlock()
total := 0
for len(p) > 0 {
if c.sendAEAD == nil || c.sendAEADEpoch != c.sendEpoch {
a, err := c.newAEADFor(c.keys.baseSend, c.sendEpoch)
if err != nil {
return total, err
}
c.sendAEAD = a
c.sendAEADEpoch = c.sendEpoch
}
aead := c.sendAEAD
maxPlain := maxFrameBodySize - recordHeaderSize - aead.Overhead()
if maxPlain <= 0 {
return total, errors.New("frame size too small")
}
n := len(p)
if n > maxPlain {
n = maxPlain
}
chunk := p[:n]
p = p[n:]
var header [recordHeaderSize]byte
binary.BigEndian.PutUint32(header[:4], c.sendEpoch)
binary.BigEndian.PutUint64(header[4:], c.sendSeq)
c.sendSeq++
cipherLen := n + aead.Overhead()
bodyLen := recordHeaderSize + cipherLen
frameLen := 2 + bodyLen
if bodyLen > maxFrameBodySize {
return total, errors.New("frame too large")
}
if cap(c.writeFrame) < frameLen {
c.writeFrame = make([]byte, frameLen)
}
frame := c.writeFrame[:frameLen]
binary.BigEndian.PutUint16(frame[:2], uint16(bodyLen))
copy(frame[2:2+recordHeaderSize], header[:])
dst := frame[2+recordHeaderSize : 2+recordHeaderSize : frameLen]
_ = aead.Seal(dst[:0], header[:], chunk, header[:])
if err := writeFull(c.Conn, frame); err != nil {
return total, err
}
total += n
if err := c.maybeBumpSendEpochLocked(n); err != nil {
return total, err
}
}
return total, nil
}
func (c *RecordConn) Read(p []byte) (int, error) {
if c == nil || c.Conn == nil {
return 0, net.ErrClosed
}
if c.method == "none" {
return c.Conn.Read(p)
}
c.readMu.Lock()
defer c.readMu.Unlock()
if c.readBuf.Len() > 0 {
return c.readBuf.Read(p)
}
var lenBuf [2]byte
if _, err := io.ReadFull(c.Conn, lenBuf[:]); err != nil {
return 0, err
}
bodyLen := int(binary.BigEndian.Uint16(lenBuf[:]))
if bodyLen < recordHeaderSize {
return 0, errors.New("frame too short")
}
if bodyLen > maxFrameBodySize {
return 0, errors.New("frame too large")
}
body := make([]byte, bodyLen)
if _, err := io.ReadFull(c.Conn, body); err != nil {
return 0, err
}
header := body[:recordHeaderSize]
ciphertext := body[recordHeaderSize:]
epoch := binary.BigEndian.Uint32(header[:4])
seq := binary.BigEndian.Uint64(header[4:])
if err := c.validateRecvPosition(epoch, seq); err != nil {
return 0, err
}
if c.recvAEAD == nil || c.recvAEADEpoch != epoch {
a, err := c.newAEADFor(c.keys.baseRecv, epoch)
if err != nil {
return 0, err
}
c.recvAEAD = a
c.recvAEADEpoch = epoch
}
aead := c.recvAEAD
plaintext, err := aead.Open(nil, header, ciphertext, header)
if err != nil {
return 0, fmt.Errorf("decryption failed: epoch=%d seq=%d: %w", epoch, seq, err)
}
c.markRecvPosition(epoch, seq)
c.readBuf.Write(plaintext)
return c.readBuf.Read(p)
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
b = b[n:]
}
return nil
}

View File

@@ -0,0 +1,343 @@
package sudoku
import (
"bytes"
"crypto/ecdh"
"crypto/rand"
"encoding/hex"
"fmt"
"net"
"time"
"github.com/sagernet/sing-box/transport/sudoku/crypto"
httpmaskobfs "github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
sudokuobfs "github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
const earlyKIPHandshakeTTL = 60 * time.Second
type EarlyCodecConfig struct {
PSK string
AEAD string
EnablePureDownlink bool
PaddingMin int
PaddingMax int
}
type EarlyClientState struct {
RequestPayload []byte
cfg EarlyCodecConfig
table *sudokuobfs.Table
nonce [kipHelloNonceSize]byte
ephemeral *ecdh.PrivateKey
sessionC2S []byte
sessionS2C []byte
responseSet bool
}
type EarlyServerState struct {
ResponsePayload []byte
UserHash string
cfg EarlyCodecConfig
table *sudokuobfs.Table
sessionC2S []byte
sessionS2C []byte
}
type ReplayAllowFunc func(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool
type earlyMemoryConn struct {
reader *bytes.Reader
write bytes.Buffer
}
func newEarlyMemoryConn(readBuf []byte) *earlyMemoryConn {
return &earlyMemoryConn{reader: bytes.NewReader(readBuf)}
}
func (c *earlyMemoryConn) Read(p []byte) (int, error) {
if c == nil || c.reader == nil {
return 0, net.ErrClosed
}
return c.reader.Read(p)
}
func (c *earlyMemoryConn) Write(p []byte) (int, error) {
if c == nil {
return 0, net.ErrClosed
}
return c.write.Write(p)
}
func (c *earlyMemoryConn) Close() error { return nil }
func (c *earlyMemoryConn) LocalAddr() net.Addr { return earlyDummyAddr("local") }
func (c *earlyMemoryConn) RemoteAddr() net.Addr { return earlyDummyAddr("remote") }
func (c *earlyMemoryConn) SetDeadline(time.Time) error { return nil }
func (c *earlyMemoryConn) SetReadDeadline(time.Time) error { return nil }
func (c *earlyMemoryConn) SetWriteDeadline(time.Time) error { return nil }
func (c *earlyMemoryConn) Written() []byte { return append([]byte(nil), c.write.Bytes()...) }
type earlyDummyAddr string
func (a earlyDummyAddr) Network() string { return string(a) }
func (a earlyDummyAddr) String() string { return string(a) }
func buildEarlyClientObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn {
base := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
downlinkReader := newClientDownlinkReader(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkReader == nil {
return base
}
return newDirectionalConn(raw, downlinkReader, base)
}
func buildEarlyServerObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn {
uplink := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
downlinkWriter, closers := newServerDownlinkWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkWriter == nil {
return uplink
}
return newDirectionalConn(raw, uplink, downlinkWriter, closers...)
}
func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, tableHint uint32, hasTableHint bool, userHash [kipHelloUserHashSize]byte, feats uint32) (*EarlyClientState, error) {
if table == nil {
return nil, fmt.Errorf("nil table")
}
curve := ecdh.X25519()
ephemeral, err := curve.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("ecdh generate failed: %w", err)
}
var nonce [kipHelloNonceSize]byte
if _, err := rand.Read(nonce[:]); err != nil {
return nil, fmt.Errorf("nonce generate failed: %w", err)
}
var clientPub [kipHelloPubSize]byte
copy(clientPub[:], ephemeral.PublicKey().Bytes())
hello := newKIPClientHello(userHash, nonce, clientPub, feats, tableHint, hasTableHint)
mem := newEarlyMemoryConn(nil)
obfsConn := buildEarlyClientObfsConn(mem, cfg, table)
pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK)
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskC2S, pskS2C)
if err != nil {
return nil, fmt.Errorf("client early crypto setup failed: %w", err)
}
if err := WriteKIPMessage(rc, KIPTypeClientHello, hello.EncodePayload()); err != nil {
return nil, fmt.Errorf("write early client hello failed: %w", err)
}
return &EarlyClientState{
RequestPayload: mem.Written(),
cfg: cfg,
table: table,
nonce: nonce,
ephemeral: ephemeral,
}, nil
}
func (s *EarlyClientState) ProcessResponse(payload []byte) error {
if s == nil {
return fmt.Errorf("nil client state")
}
mem := newEarlyMemoryConn(payload)
obfsConn := buildEarlyClientObfsConn(mem, s.cfg, s.table)
pskC2S, pskS2C := derivePSKDirectionalBases(s.cfg.PSK)
rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, pskC2S, pskS2C)
if err != nil {
return fmt.Errorf("client early crypto setup failed: %w", err)
}
msg, err := ReadKIPMessage(rc)
if err != nil {
return fmt.Errorf("read early server hello failed: %w", err)
}
if msg.Type != KIPTypeServerHello {
return fmt.Errorf("unexpected early handshake message: %d", msg.Type)
}
sh, err := DecodeKIPServerHelloPayload(msg.Payload)
if err != nil {
return fmt.Errorf("decode early server hello failed: %w", err)
}
if sh.Nonce != s.nonce {
return fmt.Errorf("early handshake nonce mismatch")
}
shared, err := x25519SharedSecret(s.ephemeral, sh.ServerPub[:])
if err != nil {
return fmt.Errorf("ecdh failed: %w", err)
}
s.sessionC2S, s.sessionS2C, err = deriveSessionDirectionalBases(s.cfg.PSK, shared, s.nonce)
if err != nil {
return fmt.Errorf("derive session keys failed: %w", err)
}
s.responseSet = true
return nil
}
func (s *EarlyClientState) WrapConn(raw net.Conn) (net.Conn, error) {
if s == nil {
return nil, fmt.Errorf("nil client state")
}
if !s.responseSet {
return nil, fmt.Errorf("early handshake not completed")
}
obfsConn := buildEarlyClientObfsConn(raw, s.cfg, s.table)
rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionC2S, s.sessionS2C)
if err != nil {
return nil, fmt.Errorf("setup client session crypto failed: %w", err)
}
return rc, nil
}
func (s *EarlyClientState) Ready() bool {
return s != nil && s.responseSet
}
func NewHTTPMaskClientEarlyHandshake(cfg EarlyCodecConfig, table *sudokuobfs.Table, tableHint uint32, hasTableHint bool, userHash [kipHelloUserHashSize]byte, feats uint32) (*httpmaskobfs.ClientEarlyHandshake, error) {
state, err := NewEarlyClientState(cfg, table, tableHint, hasTableHint, userHash, feats)
if err != nil {
return nil, err
}
return &httpmaskobfs.ClientEarlyHandshake{
RequestPayload: state.RequestPayload,
HandleResponse: state.ProcessResponse,
Ready: state.Ready,
WrapConn: state.WrapConn,
}, nil
}
func ProcessEarlyClientPayload(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) {
if len(payload) == 0 {
return nil, fmt.Errorf("empty early payload")
}
if len(tables) == 0 {
return nil, fmt.Errorf("no tables configured")
}
var firstErr error
for _, table := range tables {
state, err := processEarlyClientPayloadForTable(cfg, tables, table, payload, allowReplay)
if err == nil {
return state, nil
}
if firstErr == nil {
firstErr = err
}
}
if firstErr == nil {
firstErr = fmt.Errorf("early handshake probe failed")
}
return nil, firstErr
}
func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, table *sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) {
mem := newEarlyMemoryConn(payload)
obfsConn := buildEarlyServerObfsConn(mem, cfg, table)
pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK)
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskS2C, pskC2S)
if err != nil {
return nil, err
}
msg, err := ReadKIPMessage(rc)
if err != nil {
return nil, err
}
if msg.Type != KIPTypeClientHello {
return nil, fmt.Errorf("unexpected handshake message: %d", msg.Type)
}
ch, err := DecodeKIPClientHelloPayload(msg.Payload)
if err != nil {
return nil, err
}
if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(earlyKIPHandshakeTTL.Seconds()) {
return nil, fmt.Errorf("time skew/replay")
}
userHash := hex.EncodeToString(ch.UserHash[:])
if allowReplay != nil && !allowReplay(userHash, ch.Nonce, time.Now()) {
return nil, fmt.Errorf("replay detected")
}
resolvedTable, err := ResolveClientHelloTable(table, tables, ch)
if err != nil {
return nil, fmt.Errorf("resolve table hint failed: %w", err)
}
curve := ecdh.X25519()
serverEphemeral, err := curve.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("ecdh generate failed: %w", err)
}
shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:])
if err != nil {
return nil, fmt.Errorf("ecdh failed: %w", err)
}
sessionC2S, sessionS2C, err := deriveSessionDirectionalBases(cfg.PSK, shared, ch.Nonce)
if err != nil {
return nil, fmt.Errorf("derive session keys failed: %w", err)
}
var serverPub [kipHelloPubSize]byte
copy(serverPub[:], serverEphemeral.PublicKey().Bytes())
serverHello := &KIPServerHello{
Nonce: ch.Nonce,
ServerPub: serverPub,
SelectedFeats: ch.Features & KIPFeatAll,
}
respMem := newEarlyMemoryConn(nil)
respObfs := buildEarlyServerObfsConn(respMem, cfg, resolvedTable)
respConn, err := crypto.NewRecordConn(respObfs, cfg.AEAD, pskS2C, pskC2S)
if err != nil {
return nil, fmt.Errorf("server early crypto setup failed: %w", err)
}
if err := WriteKIPMessage(respConn, KIPTypeServerHello, serverHello.EncodePayload()); err != nil {
return nil, fmt.Errorf("write early server hello failed: %w", err)
}
return &EarlyServerState{
ResponsePayload: respMem.Written(),
UserHash: userHash,
cfg: cfg,
table: resolvedTable,
sessionC2S: sessionC2S,
sessionS2C: sessionS2C,
}, nil
}
func (s *EarlyServerState) WrapConn(raw net.Conn) (net.Conn, error) {
if s == nil {
return nil, fmt.Errorf("nil server state")
}
obfsConn := buildEarlyServerObfsConn(raw, s.cfg, s.table)
rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionS2C, s.sessionC2S)
if err != nil {
return nil, fmt.Errorf("setup server session crypto failed: %w", err)
}
return rc, nil
}
func NewHTTPMaskServerEarlyHandshake(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, allowReplay ReplayAllowFunc) *httpmaskobfs.TunnelServerEarlyHandshake {
return &httpmaskobfs.TunnelServerEarlyHandshake{
Prepare: func(payload []byte) (*httpmaskobfs.PreparedServerEarlyHandshake, error) {
state, err := ProcessEarlyClientPayload(cfg, tables, payload, allowReplay)
if err != nil {
return nil, err
}
return &httpmaskobfs.PreparedServerEarlyHandshake{
ResponsePayload: state.ResponsePayload,
WrapConn: state.WrapConn,
UserHash: state.UserHash,
}, nil
},
}
}

View File

@@ -0,0 +1,511 @@
package sudoku
import (
"bufio"
"bytes"
"crypto/ecdh"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/transport/sudoku/crypto"
"github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
"github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
type SessionType int
const (
SessionTypeTCP SessionType = iota
SessionTypeUoT
SessionTypeMultiplex
)
type ServerSession struct {
Conn net.Conn
Type SessionType
Target string
// UserHash is a stable per-key identifier derived from the client hello payload.
UserHash string
}
type HandshakeMeta struct {
UserHash string
}
// SuspiciousError indicates a potential probing attempt or protocol violation.
// When returned, Conn (if non-nil) should contain all bytes already consumed/buffered so the caller
// can perform a best-effort fallback relay (e.g. to a local web server) without losing the request.
type SuspiciousError struct {
Err error
Conn net.Conn
}
func (e *SuspiciousError) Error() string {
if e == nil || e.Err == nil {
return ""
}
return e.Err.Error()
}
func (e *SuspiciousError) Unwrap() error { return e.Err }
type recordedConn struct {
net.Conn
recorded []byte
}
func (rc *recordedConn) GetBufferedAndRecorded() []byte { return rc.recorded }
type prefixedRecorderConn struct {
net.Conn
prefix []byte
}
func (pc *prefixedRecorderConn) GetBufferedAndRecorded() []byte {
var rest []byte
if r, ok := pc.Conn.(interface{ GetBufferedAndRecorded() []byte }); ok {
rest = r.GetBufferedAndRecorded()
}
out := make([]byte, 0, len(pc.prefix)+len(rest))
out = append(out, pc.prefix...)
out = append(out, rest...)
return out
}
// bufferedRecorderConn wraps a net.Conn and a shared bufio.Reader so we can expose buffered bytes.
// This is used for legacy HTTP mask parsing errors so callers can fall back to a real HTTP server.
type bufferedRecorderConn struct {
net.Conn
r *bufio.Reader
recorder *bytes.Buffer
mu sync.Mutex
}
func (bc *bufferedRecorderConn) Read(p []byte) (n int, err error) {
n, err = bc.r.Read(p)
if n > 0 && bc.recorder != nil {
bc.mu.Lock()
bc.recorder.Write(p[:n])
bc.mu.Unlock()
}
return n, err
}
func (bc *bufferedRecorderConn) GetBufferedAndRecorded() []byte {
if bc == nil {
return nil
}
bc.mu.Lock()
defer bc.mu.Unlock()
var recorded []byte
if bc.recorder != nil {
recorded = bc.recorder.Bytes()
}
buffered := 0
if bc.r != nil {
buffered = bc.r.Buffered()
}
if buffered <= 0 {
return recorded
}
peeked, _ := bc.r.Peek(buffered)
full := make([]byte, len(recorded)+len(peeked))
copy(full, recorded)
copy(full[len(recorded):], peeked)
return full
}
type preBufferedConn struct {
net.Conn
buf []byte
}
func (p *preBufferedConn) Read(b []byte) (int, error) {
if len(p.buf) > 0 {
n := copy(b, p.buf)
p.buf = p.buf[n:]
return n, nil
}
if p.Conn == nil {
return 0, io.EOF
}
return p.Conn.Read(b)
}
func (p *preBufferedConn) CloseWrite() error {
if p == nil {
return nil
}
if cw, ok := p.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (p *preBufferedConn) CloseRead() error {
if p == nil {
return nil
}
if cr, ok := p.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
type directionalConn struct {
net.Conn
reader io.Reader
writer io.Writer
closers []func() error
}
func newDirectionalConn(base net.Conn, reader io.Reader, writer io.Writer, closers ...func() error) net.Conn {
return &directionalConn{
Conn: base,
reader: reader,
writer: writer,
closers: closers,
}
}
func (c *directionalConn) Read(p []byte) (int, error) {
return c.reader.Read(p)
}
func (c *directionalConn) Write(p []byte) (int, error) {
return c.writer.Write(p)
}
func (c *directionalConn) ReplaceWriter(writer io.Writer, closers ...func() error) {
if c == nil {
return
}
c.writer = writer
c.closers = closers
}
func (c *directionalConn) Close() error {
var firstErr error
for _, fn := range c.closers {
if fn == nil {
continue
}
if err := fn(); err != nil && firstErr == nil {
firstErr = err
}
}
if err := c.Conn.Close(); err != nil && firstErr == nil {
firstErr = err
}
return firstErr
}
func (c *directionalConn) CloseWrite() error {
if c == nil {
return nil
}
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (c *directionalConn) CloseRead() error {
if c == nil {
return nil
}
if cr, ok := c.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func absInt64(v int64) int64 {
if v < 0 {
return -v
}
return v
}
func oppositeDirectionTable(table *sudoku.Table) *sudoku.Table {
if table == nil {
return nil
}
if other := table.OppositeDirection(); other != nil {
return other
}
return table
}
func newClientDownlinkReader(raw net.Conn, table *sudoku.Table, paddingMin, paddingMax int, pureDownlink bool) io.Reader {
downlinkTable := oppositeDirectionTable(table)
if pureDownlink {
if downlinkTable == table {
return nil
}
return sudoku.NewConn(raw, downlinkTable, paddingMin, paddingMax, false)
}
return sudoku.NewPackedConn(raw, downlinkTable, paddingMin, paddingMax)
}
func newServerDownlinkWriter(raw net.Conn, table *sudoku.Table, paddingMin, paddingMax int, pureDownlink bool) (io.Writer, []func() error) {
downlinkTable := oppositeDirectionTable(table)
if pureDownlink {
if downlinkTable == table {
return nil, nil
}
return sudoku.NewConn(raw, downlinkTable, paddingMin, paddingMax, false), nil
}
packed := sudoku.NewPackedConn(raw, downlinkTable, paddingMin, paddingMax)
return packed, []func() error{packed.Flush}
}
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
baseSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
downlinkReader := newClientDownlinkReader(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkReader == nil {
return baseSudoku
}
return newDirectionalConn(raw, downlinkReader, baseSudoku)
}
func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
uplinkSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
downlinkWriter, closers := newServerDownlinkWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkWriter == nil {
return uplinkSudoku, uplinkSudoku
}
return uplinkSudoku, newDirectionalConn(raw, uplinkSudoku, downlinkWriter, closers...)
}
func isLegacyHTTPMaskMode(mode string) bool {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "", "legacy":
return true
default:
return false
}
}
// ClientHandshake performs the client-side Sudoku handshake (no target request).
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
if !cfg.DisableHTTPMask && isLegacyHTTPMaskMode(cfg.HTTPMaskMode) {
if err := httpmask.WriteRandomRequestHeaderWithPathRoot(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot); err != nil {
return nil, fmt.Errorf("write http mask failed: %w", err)
}
}
choice, err := pickClientTable(cfg)
if err != nil {
return nil, err
}
seed := ClientAEADSeed(cfg.Key)
obfsConn := buildClientObfsConn(rawConn, cfg, choice.Table)
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskC2S, pskS2C)
if err != nil {
return nil, fmt.Errorf("setup crypto failed: %w", err)
}
if _, err := kipHandshakeClient(rc, seed, kipUserHashFromKey(cfg.Key), KIPFeatAll, choice.Hint, choice.HasHint); err != nil {
_ = rc.Close()
return nil, err
}
return rc, nil
}
func readFirstSessionMessage(conn net.Conn) (*KIPMessage, error) {
for {
msg, err := ReadKIPMessage(conn)
if err != nil {
return nil, err
}
if msg.Type == KIPTypeKeepAlive {
continue
}
return msg, nil
}
}
func maybeConsumeLegacyHTTPMask(rawConn net.Conn, r *bufio.Reader, cfg *ProtocolConfig) ([]byte, *SuspiciousError) {
if rawConn == nil || r == nil || cfg == nil || cfg.DisableHTTPMask || !isLegacyHTTPMaskMode(cfg.HTTPMaskMode) {
return nil, nil
}
peekBytes, _ := r.Peek(4) // ignore error; subsequent read will handle it
if !httpmask.LooksLikeHTTPRequestStart(peekBytes) {
return nil, nil
}
consumed, err := httpmask.ConsumeHeader(r)
if err == nil {
return consumed, nil
}
recorder := new(bytes.Buffer)
if len(consumed) > 0 {
recorder.Write(consumed)
}
badConn := &bufferedRecorderConn{Conn: rawConn, r: r, recorder: recorder}
return consumed, &SuspiciousError{Err: fmt.Errorf("invalid http header: %w", err), Conn: badConn}
}
// ServerHandshake performs the server-side KIP handshake.
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, *HandshakeMeta, error) {
if rawConn == nil {
return nil, nil, fmt.Errorf("nil conn")
}
if cfg == nil {
return nil, nil, fmt.Errorf("config is required")
}
if err := cfg.Validate(); err != nil {
return nil, nil, fmt.Errorf("invalid config: %w", err)
}
if userHash, ok := httpmask.EarlyHandshakeUserHash(rawConn); ok {
return rawConn, &HandshakeMeta{UserHash: userHash}, nil
}
handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second
if handshakeTimeout <= 0 {
handshakeTimeout = 5 * time.Second
}
bufReader := bufio.NewReader(rawConn)
_ = rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout))
defer func() { _ = rawConn.SetReadDeadline(time.Time{}) }()
httpHeaderData, susp := maybeConsumeLegacyHTTPMask(rawConn, bufReader, cfg)
if susp != nil {
return nil, nil, susp
}
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
if err != nil {
combined := make([]byte, 0, len(httpHeaderData)+len(preRead))
combined = append(combined, httpHeaderData...)
combined = append(combined, preRead...)
return nil, nil, &SuspiciousError{Err: err, Conn: &recordedConn{Conn: rawConn, recorded: combined}}
}
baseConn := &preBufferedConn{Conn: rawConn, buf: preRead}
sConn, obfsConn := buildServerObfsConn(baseConn, cfg, selectedTable, true)
seed := ServerAEADSeed(cfg.Key)
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
// Server side: recv is client->server, send is server->client.
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskS2C, pskC2S)
if err != nil {
return nil, nil, fmt.Errorf("setup crypto failed: %w", err)
}
msg, err := ReadKIPMessage(rc)
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("handshake read failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
if msg.Type != KIPTypeClientHello {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("unexpected handshake message: %d", msg.Type), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
ch, err := DecodeKIPClientHelloPayload(msg.Payload)
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("decode client hello failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(kipHandshakeSkew.Seconds()) {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("time skew/replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
userHashHex := hex.EncodeToString(ch.UserHash[:])
if !globalHandshakeReplay.allow(userHashHex, ch.Nonce, time.Now()) {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
resolvedTable, err := ResolveClientHelloTable(selectedTable, cfg.tableCandidates(), ch)
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("resolve table hint failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
if resolvedTable != selectedTable {
downlinkWriter, closers := newServerDownlinkWriter(baseConn, resolvedTable, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
switchable, ok := obfsConn.(*directionalConn)
if !ok {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("switch downlink writer failed"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
switchable.ReplaceWriter(downlinkWriter, closers...)
}
curve := ecdh.X25519()
serverEphemeral, err := curve.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("ecdh generate failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:])
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("ecdh failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
sessC2S, sessS2C, err := deriveSessionDirectionalBases(seed, shared, ch.Nonce)
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("derive session keys failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
var serverPub [kipHelloPubSize]byte
copy(serverPub[:], serverEphemeral.PublicKey().Bytes())
sh := &KIPServerHello{
Nonce: ch.Nonce,
ServerPub: serverPub,
SelectedFeats: ch.Features & KIPFeatAll,
}
if err := WriteKIPMessage(rc, KIPTypeServerHello, sh.EncodePayload()); err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("write server hello failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
if err := rc.Rekey(sessS2C, sessC2S); err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("rekey failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
sConn.StopRecording()
return rc, &HandshakeMeta{UserHash: userHashHex}, nil
}
// ReadServerSession consumes the first post-handshake KIP control message and returns the session intent.
func ReadServerSession(conn net.Conn, meta *HandshakeMeta) (*ServerSession, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
userHash := ""
if meta != nil {
userHash = meta.UserHash
}
first, err := readFirstSessionMessage(conn)
if err != nil {
return nil, err
}
switch first.Type {
case KIPTypeStartUoT:
return &ServerSession{Conn: conn, Type: SessionTypeUoT, UserHash: userHash}, nil
case KIPTypeStartMux:
return &ServerSession{Conn: conn, Type: SessionTypeMultiplex, UserHash: userHash}, nil
case KIPTypeOpenTCP:
target, err := DecodeAddress(bytes.NewReader(first.Payload))
if err != nil {
return nil, fmt.Errorf("decode target address failed: %w", err)
}
return &ServerSession{Conn: conn, Type: SessionTypeTCP, Target: target, UserHash: userHash}, nil
default:
return nil, fmt.Errorf("unknown kip message: %d", first.Type)
}
}

View File

@@ -0,0 +1,67 @@
package sudoku
import (
"crypto/ecdh"
"crypto/rand"
"fmt"
"io"
"time"
"github.com/sagernet/sing-box/transport/sudoku/crypto"
)
const kipHandshakeSkew = 60 * time.Second
func kipHandshakeClient(rc *crypto.RecordConn, seed string, userHash [kipHelloUserHashSize]byte, feats uint32, tableHint uint32, hasTableHint bool) (uint32, error) {
if rc == nil {
return 0, fmt.Errorf("nil conn")
}
curve := ecdh.X25519()
ephemeral, err := curve.GenerateKey(rand.Reader)
if err != nil {
return 0, fmt.Errorf("ecdh generate failed: %w", err)
}
var nonce [kipHelloNonceSize]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
return 0, fmt.Errorf("nonce generate failed: %w", err)
}
var clientPub [kipHelloPubSize]byte
copy(clientPub[:], ephemeral.PublicKey().Bytes())
ch := newKIPClientHello(userHash, nonce, clientPub, feats, tableHint, hasTableHint)
if err := WriteKIPMessage(rc, KIPTypeClientHello, ch.EncodePayload()); err != nil {
return 0, fmt.Errorf("write client hello failed: %w", err)
}
msg, err := ReadKIPMessage(rc)
if err != nil {
return 0, fmt.Errorf("read server hello failed: %w", err)
}
if msg.Type != KIPTypeServerHello {
return 0, fmt.Errorf("unexpected handshake message: %d", msg.Type)
}
sh, err := DecodeKIPServerHelloPayload(msg.Payload)
if err != nil {
return 0, fmt.Errorf("decode server hello failed: %w", err)
}
if sh.Nonce != nonce {
return 0, fmt.Errorf("handshake nonce mismatch")
}
shared, err := x25519SharedSecret(ephemeral, sh.ServerPub[:])
if err != nil {
return 0, fmt.Errorf("ecdh failed: %w", err)
}
sessC2S, sessS2C, err := deriveSessionDirectionalBases(seed, shared, nonce)
if err != nil {
return 0, fmt.Errorf("derive session keys failed: %w", err)
}
if err := rc.Rekey(sessC2S, sessS2C); err != nil {
return 0, fmt.Errorf("rekey failed: %w", err)
}
return sh.SelectedFeats, nil
}

View File

@@ -0,0 +1,155 @@
package sudoku
import (
"context"
"fmt"
"net"
"strings"
"github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
)
type HTTPMaskTunnelServer struct {
cfg *ProtocolConfig
ts *httpmask.TunnelServer
}
func newHTTPMaskEarlyCodecConfig(cfg *ProtocolConfig, psk string) EarlyCodecConfig {
return EarlyCodecConfig{
PSK: psk,
AEAD: cfg.AEADMethod,
EnablePureDownlink: cfg.EnablePureDownlink,
PaddingMin: cfg.PaddingMin,
PaddingMax: cfg.PaddingMax,
}
}
func newClientHTTPMaskEarlyHandshake(cfg *ProtocolConfig) (*httpmask.ClientEarlyHandshake, error) {
choice, err := pickClientTable(cfg)
if err != nil {
return nil, err
}
return NewHTTPMaskClientEarlyHandshake(
newHTTPMaskEarlyCodecConfig(cfg, ClientAEADSeed(cfg.Key)),
choice.Table,
choice.Hint,
choice.HasHint,
kipUserHashFromKey(cfg.Key),
KIPFeatAll,
)
}
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
return newHTTPMaskTunnelServer(cfg, false)
}
func NewHTTPMaskTunnelServerWithFallback(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
return newHTTPMaskTunnelServer(cfg, true)
}
func newHTTPMaskTunnelServer(cfg *ProtocolConfig, passThroughOnReject bool) *HTTPMaskTunnelServer {
if cfg == nil {
return &HTTPMaskTunnelServer{}
}
var ts *httpmask.TunnelServer
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto", "ws":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
Mode: cfg.HTTPMaskMode,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ServerAEADSeed(cfg.Key),
EarlyHandshake: NewHTTPMaskServerEarlyHandshake(
newHTTPMaskEarlyCodecConfig(cfg, ServerAEADSeed(cfg.Key)),
cfg.tableCandidates(),
globalHandshakeReplay.allow,
),
// When upstream fallback is enabled, preserve rejected HTTP requests for the caller.
PassThroughOnReject: passThroughOnReject,
})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
}
// WrapConn inspects an accepted TCP connection and upgrades it to an HTTP tunnel stream when needed.
//
// Returns:
// - done=true: this TCP connection has been fully handled (e.g., stream/poll control request), caller should return
// - done=false: handshakeConn+cfg are ready for ServerHandshake
func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Conn, cfg *ProtocolConfig, done bool, err error) {
if rawConn == nil {
return nil, nil, true, fmt.Errorf("nil conn")
}
if s == nil {
return rawConn, nil, false, nil
}
if s.ts == nil {
return rawConn, s.cfg, false, nil
}
res, c, err := s.ts.HandleConn(rawConn)
if err != nil {
return nil, nil, true, err
}
switch res {
case httpmask.HandleDone:
return nil, nil, true, nil
case httpmask.HandlePassThrough:
return c, s.cfg, false, nil
case httpmask.HandleStartTunnel:
inner := *s.cfg
inner.DisableHTTPMask = true
// HTTPMask tunnel modes (stream/poll/auto/ws) add extra round trips before the first
// handshake bytes can reach ServerHandshake, especially under high concurrency.
// Bump the handshake timeout for tunneled conns to avoid flaky timeouts while keeping
// the default strict for raw TCP handshakes.
const minTunneledHandshakeTimeoutSeconds = 15
if inner.HandshakeTimeoutSeconds <= 0 || inner.HandshakeTimeoutSeconds < minTunneledHandshakeTimeoutSeconds {
inner.HandshakeTimeoutSeconds = minTunneledHandshakeTimeoutSeconds
}
return c, &inner, false, nil
default:
return nil, nil, true, nil
}
}
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto/ws) and returns a stream carrying raw Sudoku bytes.
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
if cfg.DisableHTTPMask {
return nil, fmt.Errorf("http mask is disabled")
}
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto", "ws":
default:
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
}
var (
earlyHandshake *httpmask.ClientEarlyHandshake
err error
)
if upgrade != nil {
earlyHandshake, err = newClientHTTPMaskEarlyHandshake(cfg)
if err != nil {
return nil, err
}
}
return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{
Mode: cfg.HTTPMaskMode,
HostOverride: cfg.HTTPMaskHost,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
EarlyHandshake: earlyHandshake,
Upgrade: upgrade,
Multiplex: cfg.HTTPMaskMultiplex,
DialContext: dial,
})
}

101
transport/sudoku/init.go Normal file
View File

@@ -0,0 +1,101 @@
package sudoku
import (
"encoding/hex"
"fmt"
"strings"
"filippo.io/edwards25519"
"github.com/sagernet/sing-box/transport/sudoku/crypto"
"github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
func NewTable(key string, tableType string) *sudoku.Table {
table, err := NewTableWithCustom(key, tableType, "")
if err != nil {
panic(fmt.Sprintf("[Sudoku] failed to init tables: %v", err))
}
return table
}
func NewTableWithCustom(key string, tableType string, customTable string) (*sudoku.Table, error) {
table, err := sudoku.NewTableWithCustom(key, tableType, customTable)
if err != nil {
return nil, err
}
return table, nil
}
// ClientAEADSeed returns a canonical "seed" that is stable between client private key material and server public key.
func ClientAEADSeed(key string) string {
key = strings.TrimSpace(key)
if key == "" {
return ""
}
b, err := hex.DecodeString(key)
if err != nil {
return key
}
// Client-side key material can be:
// - public key: 32 bytes hex compressed point
// - split private key: 64 bytes hex (r||k)
// - master private scalar: 32 bytes hex (x)
// - PSK string: non-hex
//
// 32-byte hex is ambiguous: it can be either a compressed public key or a
// master private scalar. Official Sudoku runtime accepts public keys directly,
// so when the bytes already decode as a point, preserve that point verbatim.
if len(b) == 32 {
if p, err := new(edwards25519.Point).SetBytes(b); err == nil {
return hex.EncodeToString(p.Bytes())
}
}
if len(b) != 64 && len(b) != 32 {
return key
}
if recovered, err := crypto.RecoverPublicKey(key); err == nil {
return crypto.EncodePoint(recovered)
}
return key
}
// ServerAEADSeed returns a canonical seed for server-side configuration.
//
// When key is a public key (32-byte compressed point, hex), it returns the canonical point encoding.
// When key is private key material (split/master scalar), it derives and returns the public key.
func ServerAEADSeed(key string) string {
key = strings.TrimSpace(key)
if key == "" {
return ""
}
b, err := hex.DecodeString(key)
if err != nil {
return key
}
// Prefer interpreting 32-byte hex as a public key point, to avoid accidental scalar parsing.
if len(b) == 32 {
if p, err := new(edwards25519.Point).SetBytes(b); err == nil {
return hex.EncodeToString(p.Bytes())
}
}
// Fall back to client-side rules for private key materials / other formats.
return ClientAEADSeed(key)
}
// GenKeyPair generates a client "available private key" and the corresponding server public key.
func GenKeyPair() (privateKey, publicKey string, err error) {
pair, err := crypto.GenerateMasterKey()
if err != nil {
return "", "", err
}
availablePrivateKey, err := crypto.SplitPrivateKey(pair.Private)
if err != nil {
return "", "", err
}
return availablePrivateKey, crypto.EncodePoint(pair.Public), nil
}

259
transport/sudoku/kip.go Normal file
View File

@@ -0,0 +1,259 @@
package sudoku
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
"time"
sudokuobfs "github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
const (
kipMagic = "kip"
KIPTypeClientHello byte = 0x01
KIPTypeServerHello byte = 0x02
KIPTypeOpenTCP byte = 0x10
KIPTypeStartMux byte = 0x11
KIPTypeStartUoT byte = 0x12
KIPTypeKeepAlive byte = 0x14
)
// KIP feature bits are advisory capability flags negotiated during the handshake.
// They represent control-plane message families.
const (
KIPFeatOpenTCP uint32 = 1 << 0
KIPFeatMux uint32 = 1 << 1
KIPFeatUoT uint32 = 1 << 2
KIPFeatKeepAlive uint32 = 1 << 4
KIPFeatAll = KIPFeatOpenTCP | KIPFeatMux | KIPFeatUoT | KIPFeatKeepAlive
)
const (
kipHelloUserHashSize = 8
kipHelloNonceSize = 16
kipHelloPubSize = 32
kipMaxPayload = 64 * 1024
)
const kipClientHelloTableHintSize = 4
var errKIP = errors.New("kip protocol error")
type KIPMessage struct {
Type byte
Payload []byte
}
func WriteKIPMessage(w io.Writer, typ byte, payload []byte) error {
if w == nil {
return fmt.Errorf("%w: nil writer", errKIP)
}
if len(payload) > kipMaxPayload {
return fmt.Errorf("%w: payload too large: %d", errKIP, len(payload))
}
var hdr [3 + 1 + 2]byte
copy(hdr[:3], []byte(kipMagic))
hdr[3] = typ
binary.BigEndian.PutUint16(hdr[4:], uint16(len(payload)))
return writeAllChunks(w, hdr[:], payload)
}
func ReadKIPMessage(r io.Reader) (*KIPMessage, error) {
if r == nil {
return nil, fmt.Errorf("%w: nil reader", errKIP)
}
var hdr [3 + 1 + 2]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return nil, err
}
if string(hdr[:3]) != kipMagic {
return nil, fmt.Errorf("%w: bad magic", errKIP)
}
typ := hdr[3]
n := int(binary.BigEndian.Uint16(hdr[4:]))
if n < 0 || n > kipMaxPayload {
return nil, fmt.Errorf("%w: invalid payload length: %d", errKIP, n)
}
var payload []byte
if n > 0 {
payload = make([]byte, n)
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
}
return &KIPMessage{Type: typ, Payload: payload}, nil
}
type KIPClientHello struct {
Timestamp time.Time
UserHash [kipHelloUserHashSize]byte
Nonce [kipHelloNonceSize]byte
ClientPub [kipHelloPubSize]byte
Features uint32
TableHint uint32
HasTableHint bool
}
type KIPServerHello struct {
Nonce [kipHelloNonceSize]byte
ServerPub [kipHelloPubSize]byte
SelectedFeats uint32
}
func newKIPClientHello(userHash [kipHelloUserHashSize]byte, nonce [kipHelloNonceSize]byte, clientPub [kipHelloPubSize]byte, feats uint32, tableHint uint32, hasTableHint bool) *KIPClientHello {
return &KIPClientHello{
Timestamp: time.Now(),
UserHash: userHash,
Nonce: nonce,
ClientPub: clientPub,
Features: feats,
TableHint: tableHint,
HasTableHint: hasTableHint,
}
}
func kipUserHashFromKey(psk string) [kipHelloUserHashSize]byte {
var out [kipHelloUserHashSize]byte
psk = strings.TrimSpace(psk)
if psk == "" {
return out
}
// Align with upstream: when the client carries private key material (or even just a public key),
// prefer hashing the raw hex bytes so different split/master keys can be distinguished.
if keyBytes, err := hex.DecodeString(psk); err == nil && len(keyBytes) > 0 {
sum := sha256.Sum256(keyBytes)
copy(out[:], sum[:kipHelloUserHashSize])
return out
}
sum := sha256.Sum256([]byte(psk))
copy(out[:], sum[:kipHelloUserHashSize])
return out
}
func KIPUserHashHexFromKey(psk string) string {
uh := kipUserHashFromKey(psk)
return hex.EncodeToString(uh[:])
}
func (m *KIPClientHello) EncodePayload() []byte {
var b bytes.Buffer
var tmp [8]byte
binary.BigEndian.PutUint64(tmp[:], uint64(m.Timestamp.Unix()))
b.Write(tmp[:])
b.Write(m.UserHash[:])
b.Write(m.Nonce[:])
b.Write(m.ClientPub[:])
var f [4]byte
binary.BigEndian.PutUint32(f[:], m.Features)
b.Write(f[:])
if m.HasTableHint {
var hint [kipClientHelloTableHintSize]byte
binary.BigEndian.PutUint32(hint[:], m.TableHint)
b.Write(hint[:])
}
return b.Bytes()
}
func DecodeKIPClientHelloPayload(payload []byte) (*KIPClientHello, error) {
const minLen = 8 + kipHelloUserHashSize + kipHelloNonceSize + kipHelloPubSize + 4
if len(payload) < minLen {
return nil, fmt.Errorf("%w: client hello too short", errKIP)
}
var h KIPClientHello
ts := int64(binary.BigEndian.Uint64(payload[:8]))
h.Timestamp = time.Unix(ts, 0)
off := 8
copy(h.UserHash[:], payload[off:off+kipHelloUserHashSize])
off += kipHelloUserHashSize
copy(h.Nonce[:], payload[off:off+kipHelloNonceSize])
off += kipHelloNonceSize
copy(h.ClientPub[:], payload[off:off+kipHelloPubSize])
off += kipHelloPubSize
h.Features = binary.BigEndian.Uint32(payload[off : off+4])
off += 4
if len(payload) >= off+kipClientHelloTableHintSize {
h.TableHint = binary.BigEndian.Uint32(payload[off : off+kipClientHelloTableHintSize])
h.HasTableHint = true
}
return &h, nil
}
func ResolveClientHelloTable(selected *sudokuobfs.Table, candidates []*sudokuobfs.Table, hello *KIPClientHello) (*sudokuobfs.Table, error) {
if selected == nil {
return nil, fmt.Errorf("nil selected table")
}
if hello == nil || !hello.HasTableHint {
return selected, nil
}
if selected.Hint() == hello.TableHint {
return selected, nil
}
if len(candidates) == 0 {
return nil, fmt.Errorf("no table candidates")
}
var hinted *sudokuobfs.Table
for _, candidate := range candidates {
if candidate == nil || candidate.Hint() != hello.TableHint {
continue
}
hinted = candidate
break
}
if hinted == nil {
return nil, fmt.Errorf("unknown table hint: %d", hello.TableHint)
}
if hinted != selected && (!hinted.IsASCII || !selected.IsASCII) {
return nil, fmt.Errorf("table hint %d mismatches probed uplink table", hello.TableHint)
}
return hinted, nil
}
func (m *KIPServerHello) EncodePayload() []byte {
var b bytes.Buffer
b.Write(m.Nonce[:])
b.Write(m.ServerPub[:])
var f [4]byte
binary.BigEndian.PutUint32(f[:], m.SelectedFeats)
b.Write(f[:])
return b.Bytes()
}
func DecodeKIPServerHelloPayload(payload []byte) (*KIPServerHello, error) {
const want = kipHelloNonceSize + kipHelloPubSize + 4
if len(payload) != want {
return nil, fmt.Errorf("%w: server hello bad len: %d", errKIP, len(payload))
}
var h KIPServerHello
off := 0
copy(h.Nonce[:], payload[off:off+kipHelloNonceSize])
off += kipHelloNonceSize
copy(h.ServerPub[:], payload[off:off+kipHelloPubSize])
off += kipHelloPubSize
h.SelectedFeats = binary.BigEndian.Uint32(payload[off : off+4])
return &h, nil
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
b = b[n:]
}
return nil
}

View File

@@ -0,0 +1,130 @@
package sudoku
import (
"bytes"
"context"
"fmt"
"net"
"strings"
"github.com/sagernet/sing-box/transport/sudoku/multiplex"
)
// StartMultiplexClient upgrades an already-handshaked Sudoku tunnel into a multiplex session.
func StartMultiplexClient(conn net.Conn) (*MultiplexClient, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
if err := WriteKIPMessage(conn, KIPTypeStartMux, nil); err != nil {
return nil, fmt.Errorf("write mux start failed: %w", err)
}
sess, err := multiplex.NewClientSession(conn)
if err != nil {
return nil, fmt.Errorf("start multiplex session failed: %w", err)
}
return &MultiplexClient{sess: sess}, nil
}
type MultiplexClient struct {
sess *multiplex.Session
}
// Dial opens a new logical stream, writes the target address, and returns the stream as net.Conn.
func (c *MultiplexClient) Dial(ctx context.Context, targetAddress string) (net.Conn, error) {
if c == nil || c.sess == nil || c.sess.IsClosed() {
return nil, fmt.Errorf("multiplex session is closed")
}
if strings.TrimSpace(targetAddress) == "" {
return nil, fmt.Errorf("target address cannot be empty")
}
addrBuf, err := EncodeAddress(targetAddress)
if err != nil {
return nil, fmt.Errorf("encode target address failed: %w", err)
}
if ctx != nil && ctx.Err() != nil {
return nil, ctx.Err()
}
stream, err := c.sess.OpenStream(addrBuf)
if err != nil {
return nil, err
}
return stream, nil
}
func (c *MultiplexClient) Close() error {
if c == nil || c.sess == nil {
return nil
}
return c.sess.Close()
}
func (c *MultiplexClient) IsClosed() bool {
if c == nil || c.sess == nil {
return true
}
return c.sess.IsClosed()
}
// AcceptMultiplexServer upgrades a server-side, already-handshaked Sudoku connection into a multiplex session.
func AcceptMultiplexServer(conn net.Conn) (*MultiplexServer, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
sess, err := multiplex.NewServerSession(conn)
if err != nil {
return nil, err
}
return &MultiplexServer{sess: sess}, nil
}
// MultiplexServer wraps a multiplex session created from a handshaked Sudoku tunnel connection.
type MultiplexServer struct {
sess *multiplex.Session
}
func (s *MultiplexServer) AcceptStream() (net.Conn, error) {
if s == nil || s.sess == nil {
return nil, fmt.Errorf("nil session")
}
c, _, err := s.sess.AcceptStream()
return c, err
}
// AcceptTCP accepts a multiplex stream and returns the target address declared in the open frame.
func (s *MultiplexServer) AcceptTCP() (net.Conn, string, error) {
if s == nil || s.sess == nil {
return nil, "", fmt.Errorf("nil session")
}
stream, payload, err := s.sess.AcceptStream()
if err != nil {
return nil, "", err
}
target, err := DecodeAddress(bytes.NewReader(payload))
if err != nil {
_ = stream.Close()
return nil, "", err
}
return stream, target, nil
}
func (s *MultiplexServer) Close() error {
if s == nil || s.sess == nil {
return nil
}
return s.sess.Close()
}
func (s *MultiplexServer) IsClosed() bool {
if s == nil || s.sess == nil {
return true
}
return s.sess.IsClosed()
}

View File

@@ -0,0 +1,493 @@
package multiplex
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
)
const (
frameOpen byte = 0x01
frameData byte = 0x02
frameClose byte = 0x03
frameReset byte = 0x04
)
const (
headerSize = 1 + 4 + 4
maxFrameSize = 256 * 1024
maxDataPayload = 32 * 1024
)
type acceptEvent struct {
stream *stream
payload []byte
}
type Session struct {
conn net.Conn
writeMu sync.Mutex
streamsMu sync.Mutex
streams map[uint32]*stream
nextID uint32
acceptCh chan acceptEvent
closed chan struct{}
closeOnce sync.Once
closeErr error
}
func NewClientSession(conn net.Conn) (*Session, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
s := &Session{
conn: conn,
streams: make(map[uint32]*stream),
closed: make(chan struct{}),
}
go s.readLoop()
return s, nil
}
func NewServerSession(conn net.Conn) (*Session, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
s := &Session{
conn: conn,
streams: make(map[uint32]*stream),
acceptCh: make(chan acceptEvent, 256),
closed: make(chan struct{}),
}
go s.readLoop()
return s, nil
}
func (s *Session) IsClosed() bool {
if s == nil {
return true
}
select {
case <-s.closed:
return true
default:
return false
}
}
func (s *Session) closedErr() error {
s.streamsMu.Lock()
err := s.closeErr
s.streamsMu.Unlock()
if err == nil {
return io.ErrClosedPipe
}
return err
}
func (s *Session) closeWithError(err error) {
if err == nil {
err = io.ErrClosedPipe
}
s.closeOnce.Do(func() {
s.streamsMu.Lock()
if s.closeErr == nil {
s.closeErr = err
}
streams := make([]*stream, 0, len(s.streams))
for _, st := range s.streams {
streams = append(streams, st)
}
s.streams = make(map[uint32]*stream)
s.streamsMu.Unlock()
for _, st := range streams {
st.closeNoSend(err)
}
close(s.closed)
_ = s.conn.Close()
})
}
func (s *Session) Close() error {
if s == nil {
return nil
}
s.closeWithError(io.ErrClosedPipe)
return nil
}
func (s *Session) registerStream(st *stream) {
s.streamsMu.Lock()
s.streams[st.id] = st
s.streamsMu.Unlock()
}
func (s *Session) getStream(id uint32) *stream {
s.streamsMu.Lock()
st := s.streams[id]
s.streamsMu.Unlock()
return st
}
func (s *Session) removeStream(id uint32) {
s.streamsMu.Lock()
delete(s.streams, id)
s.streamsMu.Unlock()
}
func (s *Session) nextStreamID() uint32 {
s.streamsMu.Lock()
s.nextID++
id := s.nextID
if id == 0 {
s.nextID++
id = s.nextID
}
s.streamsMu.Unlock()
return id
}
func (s *Session) sendFrame(frameType byte, streamID uint32, payload []byte) error {
if s.IsClosed() {
return s.closedErr()
}
if len(payload) > maxFrameSize {
return fmt.Errorf("mux payload too large: %d", len(payload))
}
var header [headerSize]byte
header[0] = frameType
binary.BigEndian.PutUint32(header[1:5], streamID)
binary.BigEndian.PutUint32(header[5:9], uint32(len(payload)))
s.writeMu.Lock()
defer s.writeMu.Unlock()
if err := writeAllChunks(s.conn, header[:], payload); err != nil {
s.closeWithError(err)
return err
}
return nil
}
func (s *Session) sendReset(streamID uint32, msg string) {
if msg == "" {
msg = "reset"
}
_ = s.sendFrame(frameReset, streamID, []byte(msg))
_ = s.sendFrame(frameClose, streamID, nil)
}
func (s *Session) OpenStream(openPayload []byte) (net.Conn, error) {
if s == nil {
return nil, fmt.Errorf("nil session")
}
if s.IsClosed() {
return nil, s.closedErr()
}
streamID := s.nextStreamID()
st := newStream(s, streamID)
s.registerStream(st)
if err := s.sendFrame(frameOpen, streamID, openPayload); err != nil {
st.closeNoSend(err)
s.removeStream(streamID)
return nil, fmt.Errorf("mux open failed: %w", err)
}
return st, nil
}
func (s *Session) AcceptStream() (net.Conn, []byte, error) {
if s == nil {
return nil, nil, fmt.Errorf("nil session")
}
if s.acceptCh == nil {
return nil, nil, fmt.Errorf("accept is not supported on client sessions")
}
select {
case ev := <-s.acceptCh:
return ev.stream, ev.payload, nil
case <-s.closed:
return nil, nil, s.closedErr()
}
}
func (s *Session) readLoop() {
var header [headerSize]byte
for {
if _, err := io.ReadFull(s.conn, header[:]); err != nil {
s.closeWithError(err)
return
}
frameType := header[0]
streamID := binary.BigEndian.Uint32(header[1:5])
n := int(binary.BigEndian.Uint32(header[5:9]))
if n < 0 || n > maxFrameSize {
s.closeWithError(fmt.Errorf("invalid mux frame length: %d", n))
return
}
var payload []byte
if n > 0 {
payload = make([]byte, n)
if _, err := io.ReadFull(s.conn, payload); err != nil {
s.closeWithError(err)
return
}
}
switch frameType {
case frameOpen:
if s.acceptCh == nil {
s.sendReset(streamID, "unexpected open")
continue
}
if streamID == 0 {
s.sendReset(streamID, "invalid stream id")
continue
}
if existing := s.getStream(streamID); existing != nil {
s.sendReset(streamID, "stream already exists")
continue
}
st := newStream(s, streamID)
s.registerStream(st)
go func() {
select {
case s.acceptCh <- acceptEvent{stream: st, payload: payload}:
case <-s.closed:
st.closeNoSend(io.ErrClosedPipe)
s.removeStream(streamID)
}
}()
case frameData:
st := s.getStream(streamID)
if st == nil {
continue
}
if len(payload) == 0 {
continue
}
st.enqueue(payload)
case frameClose:
st := s.getStream(streamID)
if st == nil {
continue
}
st.closeNoSend(io.EOF)
s.removeStream(streamID)
case frameReset:
st := s.getStream(streamID)
if st == nil {
continue
}
msg := trimASCII(payload)
if msg == "" {
msg = "reset"
}
st.closeNoSend(errors.New(msg))
s.removeStream(streamID)
default:
s.closeWithError(fmt.Errorf("unknown mux frame type: %d", frameType))
return
}
}
}
func trimASCII(b []byte) string {
i := 0
j := len(b)
for i < j {
c := b[i]
if c != ' ' && c != '\n' && c != '\r' && c != '\t' {
break
}
i++
}
for j > i {
c := b[j-1]
if c != ' ' && c != '\n' && c != '\r' && c != '\t' {
break
}
j--
}
if i >= j {
return ""
}
out := make([]byte, j-i)
copy(out, b[i:j])
return string(out)
}
type stream struct {
session *Session
id uint32
mu sync.Mutex
cond *sync.Cond
closed bool
closeErr error
readBuf []byte
queue [][]byte
localAddr net.Addr
remoteAddr net.Addr
}
func newStream(session *Session, id uint32) *stream {
st := &stream{
session: session,
id: id,
localAddr: &net.TCPAddr{},
remoteAddr: &net.TCPAddr{},
}
st.cond = sync.NewCond(&st.mu)
return st
}
func (c *stream) enqueue(payload []byte) {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return
}
if len(c.readBuf) == 0 && len(c.queue) == 0 {
c.readBuf = payload
} else {
c.queue = append(c.queue, payload)
}
c.cond.Signal()
c.mu.Unlock()
}
func (c *stream) closeNoSend(err error) {
if err == nil {
err = io.EOF
}
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return
}
c.closed = true
if c.closeErr == nil {
c.closeErr = err
}
c.cond.Broadcast()
c.mu.Unlock()
}
func (c *stream) closedErr() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closeErr == nil {
return io.ErrClosedPipe
}
return c.closeErr
}
func (c *stream) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
c.mu.Lock()
defer c.mu.Unlock()
for len(c.readBuf) == 0 && len(c.queue) == 0 && !c.closed {
c.cond.Wait()
}
if len(c.readBuf) == 0 && len(c.queue) > 0 {
c.readBuf = c.queue[0]
c.queue = c.queue[1:]
}
if len(c.readBuf) == 0 && c.closed {
if c.closeErr == nil {
return 0, io.ErrClosedPipe
}
return 0, c.closeErr
}
n := copy(p, c.readBuf)
c.readBuf = c.readBuf[n:]
return n, nil
}
func (c *stream) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
if c.session == nil || c.session.IsClosed() {
if c.session != nil {
return 0, c.session.closedErr()
}
return 0, io.ErrClosedPipe
}
c.mu.Lock()
closed := c.closed
c.mu.Unlock()
if closed {
return 0, c.closedErr()
}
written := 0
for len(p) > 0 {
chunk := p
if len(chunk) > maxDataPayload {
chunk = p[:maxDataPayload]
}
if err := c.session.sendFrame(frameData, c.id, chunk); err != nil {
return written, err
}
written += len(chunk)
p = p[len(chunk):]
}
return written, nil
}
func (c *stream) Close() error {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
c.closed = true
if c.closeErr == nil {
c.closeErr = io.ErrClosedPipe
}
c.cond.Broadcast()
c.mu.Unlock()
_ = c.session.sendFrame(frameClose, c.id, nil)
c.session.removeStream(c.id)
return nil
}
func (c *stream) CloseWrite() error { return c.Close() }
func (c *stream) CloseRead() error { return c.Close() }
func (c *stream) LocalAddr() net.Addr { return c.localAddr }
func (c *stream) RemoteAddr() net.Addr { return c.remoteAddr }
func (c *stream) SetDeadline(t time.Time) error {
_ = c.SetReadDeadline(t)
_ = c.SetWriteDeadline(t)
return nil
}
func (c *stream) SetReadDeadline(time.Time) error { return nil }
func (c *stream) SetWriteDeadline(time.Time) error { return nil }

View File

@@ -0,0 +1,19 @@
package multiplex
import "io"
func writeAllChunks(w io.Writer, chunks ...[]byte) error {
for _, chunk := range chunks {
for len(chunk) > 0 {
n, err := w.Write(chunk)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
chunk = chunk[n:]
}
}
return nil
}

View File

@@ -0,0 +1,161 @@
package httpmask
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"strings"
"time"
"net/http"
)
const (
tunnelAuthHeaderKey = "Authorization"
tunnelAuthHeaderPrefix = "Bearer "
tunnelAuthQueryKey = "auth"
)
type tunnelAuth struct {
key [32]byte // derived HMAC key
skew time.Duration
}
func newTunnelAuth(key string, skew time.Duration) *tunnelAuth {
key = strings.TrimSpace(key)
if key == "" {
return nil
}
if skew <= 0 {
skew = 60 * time.Second
}
// Domain separation: keep this HMAC key independent from other uses of cfg.Key.
h := sha256.New()
_, _ = h.Write([]byte("sudoku-httpmask-auth-v1:"))
_, _ = h.Write([]byte(key))
var sum [32]byte
h.Sum(sum[:0])
return &tunnelAuth{key: sum, skew: skew}
}
func (a *tunnelAuth) token(mode TunnelMode, method, path string, now time.Time) string {
if a == nil {
return ""
}
ts := now.Unix()
sig := a.sign(mode, method, path, ts)
var buf [8 + 16]byte
binary.BigEndian.PutUint64(buf[:8], uint64(ts))
copy(buf[8:], sig[:])
return base64.RawURLEncoding.EncodeToString(buf[:])
}
func (a *tunnelAuth) verify(headers map[string]string, mode TunnelMode, method, path string, now time.Time) bool {
if a == nil {
return true
}
if headers == nil {
return false
}
return a.verifyValue(headers["authorization"], mode, method, path, now)
}
func (a *tunnelAuth) verifyValue(val string, mode TunnelMode, method, path string, now time.Time) bool {
if a == nil {
return true
}
val = strings.TrimSpace(val)
if val == "" {
return false
}
// Accept both "Bearer <token>" and raw token forms (for forward proxies / CDNs that may normalize headers).
if len(val) > len(tunnelAuthHeaderPrefix) && strings.EqualFold(val[:len(tunnelAuthHeaderPrefix)], tunnelAuthHeaderPrefix) {
val = strings.TrimSpace(val[len(tunnelAuthHeaderPrefix):])
}
if val == "" {
return false
}
raw, err := base64.RawURLEncoding.DecodeString(val)
if err != nil || len(raw) != 8+16 {
return false
}
ts := int64(binary.BigEndian.Uint64(raw[:8]))
nowTS := now.Unix()
delta := nowTS - ts
if delta < 0 {
delta = -delta
}
if delta > int64(a.skew.Seconds()) {
return false
}
want := a.sign(mode, method, path, ts)
return subtle.ConstantTimeCompare(raw[8:], want[:]) == 1
}
func (a *tunnelAuth) sign(mode TunnelMode, method, path string, ts int64) [16]byte {
method = strings.ToUpper(strings.TrimSpace(method))
if method == "" {
method = "GET"
}
path = strings.TrimSpace(path)
var tsBuf [8]byte
binary.BigEndian.PutUint64(tsBuf[:], uint64(ts))
mac := hmac.New(sha256.New, a.key[:])
_, _ = mac.Write([]byte(mode))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(method))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(path))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write(tsBuf[:])
var full [32]byte
mac.Sum(full[:0])
var out [16]byte
copy(out[:], full[:16])
return out
}
type httpHeaderSetter = http.Header
func applyTunnelAuthHeader(h httpHeaderSetter, auth *tunnelAuth, mode TunnelMode, method, path string) {
if auth == nil || h == nil {
return
}
token := auth.token(mode, method, path, time.Now())
if token == "" {
return
}
h.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
}
func applyTunnelAuth(req *http.Request, auth *tunnelAuth, mode TunnelMode, method, path string) {
if auth == nil || req == nil {
return
}
token := auth.token(mode, method, path, time.Now())
if token == "" {
return
}
req.Header.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
if req.URL != nil {
q := req.URL.Query()
q.Set(tunnelAuthQueryKey, token)
req.URL.RawQuery = q.Encode()
}
}

View File

@@ -0,0 +1,174 @@
package httpmask
import (
"encoding/base64"
"errors"
"fmt"
"net"
"net/url"
"strings"
)
const (
tunnelEarlyDataQueryKey = "ed"
tunnelEarlyDataHeader = "X-Sudoku-Early"
)
type ClientEarlyHandshake struct {
RequestPayload []byte
HandleResponse func(payload []byte) error
Ready func() bool
WrapConn func(raw net.Conn) (net.Conn, error)
}
type TunnelServerEarlyHandshake struct {
Prepare func(payload []byte) (*PreparedServerEarlyHandshake, error)
}
type PreparedServerEarlyHandshake struct {
ResponsePayload []byte
WrapConn func(raw net.Conn) (net.Conn, error)
UserHash string
}
type earlyHandshakeMeta interface {
HTTPMaskEarlyHandshakeUserHash() string
}
type earlyHandshakeConn struct {
net.Conn
userHash string
}
func (c *earlyHandshakeConn) HTTPMaskEarlyHandshakeUserHash() string {
if c == nil {
return ""
}
return c.userHash
}
func wrapEarlyHandshakeConn(conn net.Conn, userHash string) net.Conn {
if conn == nil {
return nil
}
return &earlyHandshakeConn{Conn: conn, userHash: userHash}
}
func EarlyHandshakeUserHash(conn net.Conn) (string, bool) {
if conn == nil {
return "", false
}
v, ok := conn.(earlyHandshakeMeta)
if !ok {
return "", false
}
return v.HTTPMaskEarlyHandshakeUserHash(), true
}
type authorizeResponse struct {
token string
earlyPayload []byte
}
func isTunnelTokenByte(c byte) bool {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_'
}
func parseAuthorizeResponse(body []byte) (*authorizeResponse, error) {
s := strings.TrimSpace(string(body))
idx := strings.Index(s, "token=")
if idx < 0 {
return nil, errors.New("missing token")
}
s = s[idx+len("token="):]
if s == "" {
return nil, errors.New("empty token")
}
var b strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
if isTunnelTokenByte(c) {
b.WriteByte(c)
continue
}
break
}
token := b.String()
if token == "" {
return nil, errors.New("empty token")
}
out := &authorizeResponse{token: token}
if earlyLine := findAuthorizeField(body, "ed="); earlyLine != "" {
decoded, err := base64.RawURLEncoding.DecodeString(earlyLine)
if err != nil {
return nil, fmt.Errorf("decode early authorize payload failed: %w", err)
}
out.earlyPayload = decoded
}
return out, nil
}
func findAuthorizeField(body []byte, prefix string) string {
for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, prefix) {
return strings.TrimSpace(strings.TrimPrefix(line, prefix))
}
}
return ""
}
func setEarlyDataQuery(rawURL string, payload []byte) (string, error) {
if len(payload) == 0 {
return rawURL, nil
}
u, err := url.Parse(rawURL)
if err != nil {
return "", err
}
q := u.Query()
q.Set(tunnelEarlyDataQueryKey, base64.RawURLEncoding.EncodeToString(payload))
u.RawQuery = q.Encode()
return u.String(), nil
}
func parseEarlyDataQuery(u *url.URL) ([]byte, error) {
if u == nil {
return nil, nil
}
val := strings.TrimSpace(u.Query().Get(tunnelEarlyDataQueryKey))
if val == "" {
return nil, nil
}
return base64.RawURLEncoding.DecodeString(val)
}
func applyEarlyHandshakeOrUpgrade(raw net.Conn, opts TunnelDialOptions) (net.Conn, error) {
out := raw
if opts.EarlyHandshake != nil && opts.EarlyHandshake.WrapConn != nil && (opts.EarlyHandshake.Ready == nil || opts.EarlyHandshake.Ready()) {
wrapped, err := opts.EarlyHandshake.WrapConn(raw)
if err != nil {
return nil, err
}
if wrapped != nil {
out = wrapped
}
return out, nil
}
if opts.Upgrade != nil {
wrapped, err := opts.Upgrade(raw)
if err != nil {
return nil, err
}
if wrapped != nil {
out = wrapped
}
}
return out, nil
}

View File

@@ -0,0 +1,229 @@
package httpmask
import (
"io"
"net"
"os"
"sync"
"time"
)
type pipeDeadline struct {
mu sync.Mutex
timer *time.Timer
cancel chan struct{}
}
func makePipeDeadline() pipeDeadline {
return pipeDeadline{cancel: make(chan struct{})}
}
func (d *pipeDeadline) set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil && !d.timer.Stop() {
<-d.cancel
}
d.timer = nil
closed := isClosedPipeChan(d.cancel)
if t.IsZero() {
if closed {
d.cancel = make(chan struct{})
}
return
}
if dur := time.Until(t); dur > 0 {
if closed {
d.cancel = make(chan struct{})
}
d.timer = time.AfterFunc(dur, func() {
close(d.cancel)
})
return
}
if !closed {
close(d.cancel)
}
}
func (d *pipeDeadline) wait() <-chan struct{} {
d.mu.Lock()
ch := d.cancel
d.mu.Unlock()
return ch
}
func isClosedPipeChan(ch <-chan struct{}) bool {
select {
case <-ch:
return true
default:
return false
}
}
type halfPipeAddr struct{}
func (halfPipeAddr) Network() string { return "pipe" }
func (halfPipeAddr) String() string { return "pipe" }
type halfPipeConn struct {
wrMu sync.Mutex
rdRx <-chan []byte
rdTx chan<- int
wrTx chan<- []byte
wrRx <-chan int
readOnce sync.Once
writeOnce sync.Once
localReadDone chan struct{}
localWriteDone chan struct{}
remoteReadDone <-chan struct{}
remoteWriteDone <-chan struct{}
readDeadline pipeDeadline
writeDeadline pipeDeadline
}
func newHalfPipe() (net.Conn, net.Conn) {
cb1 := make(chan []byte)
cb2 := make(chan []byte)
cn1 := make(chan int)
cn2 := make(chan int)
r1 := make(chan struct{})
w1 := make(chan struct{})
r2 := make(chan struct{})
w2 := make(chan struct{})
c1 := &halfPipeConn{
rdRx: cb1,
rdTx: cn1,
wrTx: cb2,
wrRx: cn2,
localReadDone: r1,
localWriteDone: w1,
remoteReadDone: r2,
remoteWriteDone: w2,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
}
c2 := &halfPipeConn{
rdRx: cb2,
rdTx: cn2,
wrTx: cb1,
wrRx: cn1,
localReadDone: r2,
localWriteDone: w2,
remoteReadDone: r1,
remoteWriteDone: w1,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
}
return c1, c2
}
func (*halfPipeConn) LocalAddr() net.Addr { return halfPipeAddr{} }
func (*halfPipeConn) RemoteAddr() net.Addr { return halfPipeAddr{} }
func (c *halfPipeConn) Read(p []byte) (int, error) {
switch {
case isClosedPipeChan(c.localReadDone):
return 0, io.ErrClosedPipe
case isClosedPipeChan(c.remoteWriteDone):
return 0, io.EOF
case isClosedPipeChan(c.readDeadline.wait()):
return 0, os.ErrDeadlineExceeded
}
select {
case b := <-c.rdRx:
n := copy(p, b)
c.rdTx <- n
return n, nil
case <-c.localReadDone:
return 0, io.ErrClosedPipe
case <-c.remoteWriteDone:
return 0, io.EOF
case <-c.readDeadline.wait():
return 0, os.ErrDeadlineExceeded
}
}
func (c *halfPipeConn) Write(p []byte) (int, error) {
switch {
case isClosedPipeChan(c.localWriteDone):
return 0, io.ErrClosedPipe
case isClosedPipeChan(c.remoteReadDone):
return 0, io.ErrClosedPipe
case isClosedPipeChan(c.writeDeadline.wait()):
return 0, os.ErrDeadlineExceeded
}
c.wrMu.Lock()
defer c.wrMu.Unlock()
var (
total int
rest = p
)
for once := true; once || len(rest) > 0; once = false {
select {
case c.wrTx <- rest:
n := <-c.wrRx
rest = rest[n:]
total += n
case <-c.localWriteDone:
return total, io.ErrClosedPipe
case <-c.remoteReadDone:
return total, io.ErrClosedPipe
case <-c.writeDeadline.wait():
return total, os.ErrDeadlineExceeded
}
}
return total, nil
}
func (c *halfPipeConn) CloseWrite() error {
c.writeOnce.Do(func() { close(c.localWriteDone) })
return nil
}
func (c *halfPipeConn) CloseRead() error {
c.readOnce.Do(func() { close(c.localReadDone) })
return nil
}
func (c *halfPipeConn) Close() error {
_ = c.CloseRead()
_ = c.CloseWrite()
return nil
}
func (c *halfPipeConn) SetDeadline(t time.Time) error {
c.readDeadline.set(t)
c.writeDeadline.set(t)
return nil
}
func (c *halfPipeConn) SetReadDeadline(t time.Time) error {
c.readDeadline.set(t)
return nil
}
func (c *halfPipeConn) SetWriteDeadline(t time.Time) error {
c.writeDeadline.set(t)
return nil
}

View File

@@ -0,0 +1,252 @@
package httpmask
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
)
var (
userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
}
accepts = []string{
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"application/json, text/plain, */*",
"application/octet-stream",
"*/*",
}
acceptLanguages = []string{
"en-US,en;q=0.9",
"en-GB,en;q=0.9",
"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}
acceptEncodings = []string{
"gzip, deflate, br",
"gzip, deflate",
"br, gzip, deflate",
}
paths = []string{
"/api/v1/upload",
"/data/sync",
"/uploads/raw",
"/api/report",
"/feed/update",
"/v2/events",
"/v1/telemetry",
"/session",
"/stream",
"/ws",
}
contentTypes = []string{
"application/octet-stream",
"application/x-protobuf",
"application/json",
}
)
var (
rngPool = sync.Pool{
New: func() interface{} {
return rand.New(rand.NewSource(time.Now().UnixNano()))
},
}
headerBufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 0, 1024)
return &b
},
}
)
// LooksLikeHTTPRequestStart reports whether peek4 looks like a supported HTTP/1.x request method prefix.
func LooksLikeHTTPRequestStart(peek4 []byte) bool {
if len(peek4) < 4 {
return false
}
// Common methods: "GET ", "POST", "HEAD", "PUT ", "OPTI" (OPTIONS), "PATC" (PATCH), "DELE" (DELETE)
return bytes.Equal(peek4, []byte("GET ")) ||
bytes.Equal(peek4, []byte("POST")) ||
bytes.Equal(peek4, []byte("HEAD")) ||
bytes.Equal(peek4, []byte("PUT ")) ||
bytes.Equal(peek4, []byte("OPTI")) ||
bytes.Equal(peek4, []byte("PATC")) ||
bytes.Equal(peek4, []byte("DELE"))
}
func trimPortForHost(host string) string {
if host == "" {
return host
}
// Accept "example.com:443" / "1.2.3.4:443" / "[::1]:443"
h, _, err := net.SplitHostPort(host)
if err == nil && h != "" {
return h
}
// If it's not in host:port form, keep as-is.
return host
}
func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
ua := userAgents[r.Intn(len(userAgents))]
accept := accepts[r.Intn(len(accepts))]
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
buf = append(buf, "Host: "...)
buf = append(buf, host...)
buf = append(buf, "\r\nUser-Agent: "...)
buf = append(buf, ua...)
buf = append(buf, "\r\nAccept: "...)
buf = append(buf, accept...)
buf = append(buf, "\r\nAccept-Language: "...)
buf = append(buf, lang...)
buf = append(buf, "\r\nAccept-Encoding: "...)
buf = append(buf, enc...)
buf = append(buf, "\r\nConnection: keep-alive\r\n"...)
// A couple of common cache headers; keep them static for simplicity.
buf = append(buf, "Cache-Control: no-cache\r\nPragma: no-cache\r\n"...)
return buf
}
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
func WriteRandomRequestHeader(w io.Writer, host string) error {
return WriteRandomRequestHeaderWithPathRoot(w, host, "")
}
// WriteRandomRequestHeaderWithPathRoot is like WriteRandomRequestHeader but prefixes all paths with pathRoot.
// pathRoot must be a single segment (e.g. "aabbcc"); invalid inputs are treated as empty (disabled).
func WriteRandomRequestHeaderWithPathRoot(w io.Writer, host string, pathRoot string) error {
// Get RNG from pool
r := rngPool.Get().(*rand.Rand)
defer rngPool.Put(r)
path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))])
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool
bufPtr := headerBufPool.Get().(*[]byte)
buf := *bufPtr
buf = buf[:0]
defer func() {
if cap(buf) <= 4096 {
*bufPtr = buf
headerBufPool.Put(bufPtr)
}
}()
// Weighted template selection. Keep a conservative default (POST w/ Content-Length),
// but occasionally rotate to other realistic templates (e.g. WebSocket upgrade).
switch r.Intn(10) {
case 0, 1: // ~20% WebSocket-like upgrade
hostNoPort := trimPortForHost(host)
var keyBytes [16]byte
for i := 0; i < len(keyBytes); i++ {
keyBytes[i] = byte(r.Intn(256))
}
wsKey := base64.StdEncoding.EncodeToString(keyBytes[:])
buf = append(buf, "GET "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...)
buf = append(buf, wsKey...)
buf = append(buf, "\r\nOrigin: https://"...)
buf = append(buf, hostNoPort...)
buf = append(buf, "\r\n\r\n"...)
default: // ~80% POST upload
// Random Content-Length: 4KB10MB. Small enough to look plausible, large enough
// to justify long-lived writes on keep-alive connections.
const minCL = int64(4 * 1024)
const maxCL = int64(10 * 1024 * 1024)
contentLength := minCL + r.Int63n(maxCL-minCL+1)
buf = append(buf, "POST "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Content-Type: "...)
buf = append(buf, ctype...)
buf = append(buf, "\r\nContent-Length: "...)
buf = strconv.AppendInt(buf, contentLength, 10)
// A couple of extra headers seen in real clients.
if r.Intn(2) == 0 {
buf = append(buf, "\r\nX-Requested-With: XMLHttpRequest"...)
}
if r.Intn(3) == 0 {
buf = append(buf, "\r\nReferer: https://"...)
buf = append(buf, trimPortForHost(host)...)
buf = append(buf, "/"...)
}
buf = append(buf, "\r\n\r\n"...)
}
_, err := w.Write(buf)
return err
}
// ConsumeHeader 读取并消耗 HTTP 头部,返回消耗的数据和剩余的 reader 数据
// 如果不是 POST 请求或格式严重错误,返回 error
func ConsumeHeader(r *bufio.Reader) ([]byte, error) {
var consumed bytes.Buffer
// 1. 读取请求行
// Use ReadSlice to avoid allocation if line fits in buffer
line, err := r.ReadSlice('\n')
if err != nil {
return nil, err
}
consumed.Write(line)
// Basic method validation: accept common HTTP/1.x methods used by our masker.
// Keep it strict enough to reject obvious garbage.
switch {
case bytes.HasPrefix(line, []byte("POST ")),
bytes.HasPrefix(line, []byte("GET ")),
bytes.HasPrefix(line, []byte("HEAD ")),
bytes.HasPrefix(line, []byte("PUT ")),
bytes.HasPrefix(line, []byte("DELETE ")),
bytes.HasPrefix(line, []byte("OPTIONS ")),
bytes.HasPrefix(line, []byte("PATCH ")):
default:
return consumed.Bytes(), fmt.Errorf("invalid method or garbage: %s", strings.TrimSpace(string(line)))
}
// 2. 循环读取头部,直到遇到空行
for {
line, err = r.ReadSlice('\n')
if err != nil {
return consumed.Bytes(), err
}
consumed.Write(line)
// Check for empty line (\r\n or \n)
// ReadSlice includes the delimiter
n := len(line)
if n == 2 && line[0] == '\r' && line[1] == '\n' {
return consumed.Bytes(), nil
}
if n == 1 && line[0] == '\n' {
return consumed.Bytes(), nil
}
}
}

View File

@@ -0,0 +1,52 @@
package httpmask
import "strings"
// normalizePathRoot normalizes the configured path root into "/<segment>" form.
//
// It is intentionally strict: only a single path segment is allowed, consisting of
// [A-Za-z0-9_-]. Invalid inputs are treated as empty (disabled).
func normalizePathRoot(root string) string {
root = strings.TrimSpace(root)
root = strings.Trim(root, "/")
if root == "" {
return ""
}
for i := 0; i < len(root); i++ {
c := root[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '-':
default:
return ""
}
}
return "/" + root
}
func joinPathRoot(root, path string) string {
root = normalizePathRoot(root)
if root == "" {
return path
}
if path == "" {
return root
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return root + path
}
func stripPathRoot(root, fullPath string) (string, bool) {
root = normalizePathRoot(root)
if root == "" {
return fullPath, true
}
if !strings.HasPrefix(fullPath, root+"/") {
return "", false
}
return strings.TrimPrefix(fullPath, root), true
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
package httpmask
import (
"context"
"encoding/base64"
"fmt"
"io"
mrand "math/rand"
"net"
stdhttp "net/http"
"net/url"
"strings"
"time"
"github.com/gobwas/ws"
)
func normalizeWSSchemeFromAddress(serverAddress string, tlsEnabled bool) (string, string) {
addr := strings.TrimSpace(serverAddress)
if strings.Contains(addr, "://") {
if u, err := url.Parse(addr); err == nil && u != nil {
switch strings.ToLower(strings.TrimSpace(u.Scheme)) {
case "ws":
return "ws", u.Host
case "wss":
return "wss", u.Host
}
}
}
if tlsEnabled {
return "wss", addr
}
return "ws", addr
}
func normalizeWSDialTarget(serverAddress string, tlsEnabled bool, hostOverride string) (scheme, urlHost, dialAddr, serverName string, err error) {
scheme, addr := normalizeWSSchemeFromAddress(serverAddress, tlsEnabled)
host, port, err := net.SplitHostPort(addr)
if err != nil {
// Allow ws(s)://host without port.
if strings.Contains(addr, ":") {
return "", "", "", "", fmt.Errorf("invalid server address %q: %w", serverAddress, err)
}
switch scheme {
case "wss":
port = "443"
default:
port = "80"
}
host = addr
}
if hostOverride != "" {
// Allow "example.com" or "example.com:443"
if h, p, splitErr := net.SplitHostPort(hostOverride); splitErr == nil {
if h != "" {
hostOverride = h
}
if p != "" {
port = p
}
}
serverName = hostOverride
urlHost = net.JoinHostPort(hostOverride, port)
} else {
serverName = host
urlHost = net.JoinHostPort(host, port)
}
dialAddr = net.JoinHostPort(host, port)
return scheme, urlHost, dialAddr, trimPortForHost(serverName), nil
}
func applyWSHeaders(h stdhttp.Header, host string) {
if h == nil {
return
}
r := rngPool.Get().(*mrand.Rand)
ua := userAgents[r.Intn(len(userAgents))]
accept := accepts[r.Intn(len(accepts))]
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
rngPool.Put(r)
h.Set("User-Agent", ua)
h.Set("Accept", accept)
h.Set("Accept-Language", lang)
h.Set("Accept-Encoding", enc)
h.Set("Cache-Control", "no-cache")
h.Set("Pragma", "no-cache")
h.Set("X-Sudoku-Tunnel", string(TunnelModeWS))
h.Set("X-Sudoku-Version", "1")
}
func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
if opts.DialContext == nil {
panic("httpmask: DialContext is nil")
}
scheme, urlHost, dialAddr, _, err := normalizeWSDialTarget(serverAddress, opts.TLSConfig != nil, opts.HostOverride)
if err != nil {
return nil, err
}
httpScheme := "http"
if scheme == "wss" {
httpScheme = "https"
}
headerHost := canonicalHeaderHost(urlHost, httpScheme)
auth := newTunnelAuth(opts.AuthKey, 0)
u := &url.URL{
Scheme: scheme,
Host: urlHost,
Path: joinPathRoot(opts.PathRoot, "/ws"),
}
if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 {
rawURL, err := setEarlyDataQuery(u.String(), opts.EarlyHandshake.RequestPayload)
if err != nil {
return nil, err
}
u, err = url.Parse(rawURL)
if err != nil {
return nil, err
}
}
header := make(stdhttp.Header)
applyWSHeaders(header, headerHost)
if auth != nil {
token := auth.token(TunnelModeWS, stdhttp.MethodGet, "/ws", time.Now())
if token != "" {
header.Set("Authorization", "Bearer "+token)
q := u.Query()
q.Set(tunnelAuthQueryKey, token)
u.RawQuery = q.Encode()
}
}
d := ws.Dialer{
Host: headerHost,
Header: ws.HandshakeHeaderHTTP(header),
OnHeader: func(key, value []byte) error {
if !strings.EqualFold(string(key), tunnelEarlyDataHeader) || opts.EarlyHandshake == nil || opts.EarlyHandshake.HandleResponse == nil {
return nil
}
decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(string(value)))
if err != nil {
return err
}
return opts.EarlyHandshake.HandleResponse(decoded)
},
NetDial: func(dialCtx context.Context, network, addr string) (net.Conn, error) {
if addr == urlHost {
addr = dialAddr
}
return opts.DialContext(dialCtx, network, addr)
},
}
if scheme == "wss" {
if opts.TLSConfig == nil {
return nil, fmt.Errorf("httpmask: TLSConfig is required for wss")
}
d.TLSClient = func(conn net.Conn, hostname string) net.Conn {
tlsConn, _ := opts.TLSConfig.Client(conn)
return tlsConn
}
}
conn, br, _, err := d.Dial(ctx, u.String())
if err != nil {
return nil, err
}
if br != nil && br.Buffered() > 0 {
pre := make([]byte, br.Buffered())
_, _ = io.ReadFull(br, pre)
conn = newPreBufferedConn(conn, pre)
}
wsConn := newWSStreamConn(conn, ws.StateClientSide)
upgraded, err := applyEarlyHandshakeOrUpgrade(wsConn, opts)
if err != nil {
_ = wsConn.Close()
return nil, err
}
return upgraded, nil
}

View File

@@ -0,0 +1,109 @@
package httpmask
import (
"encoding/base64"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/gobwas/ws"
)
func looksLikeWebSocketUpgrade(headers map[string]string) bool {
if headers == nil {
return false
}
if !strings.EqualFold(strings.TrimSpace(headers["upgrade"]), "websocket") {
return false
}
conn := headers["connection"]
for _, part := range strings.Split(conn, ",") {
if strings.EqualFold(strings.TrimSpace(part), "upgrade") {
return true
}
}
return false
}
func (s *TunnelServer) handleWS(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, code, body)
_ = rawConn.Close()
return HandleDone, nil, nil
}
u, err := url.ParseRequestURI(req.target)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
path, ok := stripPathRoot(s.pathRoot, u.Path)
if !ok || path != "/ws" {
return rejectOrReply(http.StatusNotFound, "not found")
}
if strings.ToUpper(strings.TrimSpace(req.method)) != http.MethodGet {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
if !looksLikeWebSocketUpgrade(req.headers) {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
authVal := req.headers["authorization"]
if authVal == "" {
authVal = u.Query().Get(tunnelAuthQueryKey)
}
if !s.auth.verifyValue(authVal, TunnelModeWS, req.method, path, time.Now()) {
return rejectOrReply(http.StatusNotFound, "not found")
}
earlyPayload, err := parseEarlyDataQuery(u)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
var prepared *PreparedServerEarlyHandshake
if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil {
prepared, err = s.earlyHandshake.Prepare(earlyPayload)
if err != nil {
return rejectOrReply(http.StatusNotFound, "not found")
}
}
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
wsConnRaw := newPreBufferedConn(rawConn, prefix)
upgrader := ws.Upgrader{}
if prepared != nil && len(prepared.ResponsePayload) > 0 {
upgrader.OnBeforeUpgrade = func() (ws.HandshakeHeader, error) {
h := http.Header{}
h.Set(tunnelEarlyDataHeader, base64.RawURLEncoding.EncodeToString(prepared.ResponsePayload))
return ws.HandshakeHeaderHTTP(h), nil
}
}
if _, err := upgrader.Upgrade(wsConnRaw); err != nil {
_ = rawConn.Close()
return HandleDone, nil, nil
}
outConn := net.Conn(newWSStreamConn(wsConnRaw, ws.StateServerSide))
if prepared != nil && prepared.WrapConn != nil {
wrapped, err := prepared.WrapConn(outConn)
if err != nil {
_ = outConn.Close()
return HandleDone, nil, nil
}
if wrapped != nil {
outConn = wrapEarlyHandshakeConn(wrapped, prepared.UserHash)
}
}
return HandleStartTunnel, outConn, nil
}

View File

@@ -0,0 +1,78 @@
package httpmask
import (
"errors"
"fmt"
"io"
"net"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
type wsStreamConn struct {
net.Conn
state ws.State
reader *wsutil.Reader
controlHandler wsutil.FrameHandlerFunc
}
func newWSStreamConn(conn net.Conn, state ws.State) net.Conn {
controlHandler := wsutil.ControlFrameHandler(conn, state)
return &wsStreamConn{
Conn: conn,
state: state,
reader: &wsutil.Reader{
Source: conn,
State: state,
},
controlHandler: controlHandler,
}
}
func (c *wsStreamConn) Read(b []byte) (n int, err error) {
defer func() {
if v := recover(); v != nil {
err = fmt.Errorf("websocket error: %v", v)
}
}()
for {
n, err = c.reader.Read(b)
if errors.Is(err, io.EOF) {
err = nil
}
if !errors.Is(err, wsutil.ErrNoFrameAdvance) {
return n, err
}
hdr, err2 := c.reader.NextFrame()
if err2 != nil {
return 0, err2
}
if hdr.OpCode.IsControl() {
if err := c.controlHandler(hdr, c.reader); err != nil {
return 0, err
}
continue
}
if hdr.OpCode&(ws.OpBinary|ws.OpText) == 0 {
if err := c.reader.Discard(); err != nil {
return 0, err
}
continue
}
}
}
func (c *wsStreamConn) Write(b []byte) (int, error) {
if err := wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, b); err != nil {
return 0, err
}
return len(b), nil
}
func (c *wsStreamConn) Close() error {
_ = wsutil.WriteMessage(c.Conn, c.state, ws.OpClose, ws.NewCloseFrameBody(ws.StatusNormalClosure, ""))
return c.Conn.Close()
}

View File

@@ -0,0 +1,93 @@
package sudoku
import (
"fmt"
"strings"
)
const (
asciiModeTokenASCII = "ascii"
asciiModeTokenEntropy = "entropy"
)
// ASCIIMode describes the preferred wire layout for each traffic direction.
// Uplink is client->server, Downlink is server->client.
type ASCIIMode struct {
Uplink string
Downlink string
}
// ParseASCIIMode accepts legacy symmetric values ("ascii"/"entropy"/"prefer_*")
// and directional values like "up_ascii_down_entropy".
func ParseASCIIMode(mode string) (ASCIIMode, error) {
raw := strings.ToLower(strings.TrimSpace(mode))
switch raw {
case "", "entropy", "prefer_entropy":
return ASCIIMode{Uplink: asciiModeTokenEntropy, Downlink: asciiModeTokenEntropy}, nil
case "ascii", "prefer_ascii":
return ASCIIMode{Uplink: asciiModeTokenASCII, Downlink: asciiModeTokenASCII}, nil
}
if !strings.HasPrefix(raw, "up_") {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
parts := strings.SplitN(strings.TrimPrefix(raw, "up_"), "_down_", 2)
if len(parts) != 2 {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
up, ok := normalizeASCIIModeToken(parts[0])
if !ok {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
down, ok := normalizeASCIIModeToken(parts[1])
if !ok {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
return ASCIIMode{Uplink: up, Downlink: down}, nil
}
// NormalizeASCIIMode returns the canonical config string for a supported mode.
func NormalizeASCIIMode(mode string) (string, error) {
parsed, err := ParseASCIIMode(mode)
if err != nil {
return "", err
}
return parsed.Canonical(), nil
}
func (m ASCIIMode) Canonical() string {
if m.Uplink == asciiModeTokenASCII && m.Downlink == asciiModeTokenASCII {
return "prefer_ascii"
}
if m.Uplink == asciiModeTokenEntropy && m.Downlink == asciiModeTokenEntropy {
return "prefer_entropy"
}
return "up_" + m.Uplink + "_down_" + m.Downlink
}
func (m ASCIIMode) uplinkPreference() string {
return singleDirectionPreference(m.Uplink)
}
func (m ASCIIMode) downlinkPreference() string {
return singleDirectionPreference(m.Downlink)
}
func normalizeASCIIModeToken(token string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(token)) {
case "ascii", "prefer_ascii":
return asciiModeTokenASCII, true
case "entropy", "prefer_entropy", "":
return asciiModeTokenEntropy, true
default:
return "", false
}
}
func singleDirectionPreference(token string) string {
if token == asciiModeTokenASCII {
return "prefer_ascii"
}
return "prefer_entropy"
}

View File

@@ -0,0 +1,193 @@
package sudoku
import (
"bufio"
"bytes"
"net"
"sync"
"sync/atomic"
)
const IOBufferSize = 32 * 1024
var perm4 = [24][4]byte{
{0, 1, 2, 3},
{0, 1, 3, 2},
{0, 2, 1, 3},
{0, 2, 3, 1},
{0, 3, 1, 2},
{0, 3, 2, 1},
{1, 0, 2, 3},
{1, 0, 3, 2},
{1, 2, 0, 3},
{1, 2, 3, 0},
{1, 3, 0, 2},
{1, 3, 2, 0},
{2, 0, 1, 3},
{2, 0, 3, 1},
{2, 1, 0, 3},
{2, 1, 3, 0},
{2, 3, 0, 1},
{2, 3, 1, 0},
{3, 0, 1, 2},
{3, 0, 2, 1},
{3, 1, 0, 2},
{3, 1, 2, 0},
{3, 2, 0, 1},
{3, 2, 1, 0},
}
type Conn struct {
net.Conn
table *Table
reader *bufio.Reader
recorder *bytes.Buffer
recording atomic.Bool
recordLock sync.Mutex
rawBuf []byte
pendingData pendingBuffer
hintBuf [4]byte
hintCount int
writeMu sync.Mutex
writeBuf []byte
rng randomSource
paddingThreshold uint64
}
func (sc *Conn) CloseWrite() error {
if sc == nil || sc.Conn == nil {
return nil
}
if cw, ok := sc.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (sc *Conn) CloseRead() error {
if sc == nil || sc.Conn == nil {
return nil
}
if cr, ok := sc.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
localRng := newSeededRand()
sc := &Conn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: newPendingBuffer(4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
}
if record {
sc.recorder = new(bytes.Buffer)
sc.recording.Store(true)
}
return sc
}
func (sc *Conn) StopRecording() {
sc.recordLock.Lock()
sc.recording.Store(false)
sc.recorder = nil
sc.recordLock.Unlock()
}
func (sc *Conn) GetBufferedAndRecorded() []byte {
if sc == nil {
return nil
}
sc.recordLock.Lock()
defer sc.recordLock.Unlock()
var recorded []byte
if sc.recorder != nil {
recorded = sc.recorder.Bytes()
}
buffered := sc.reader.Buffered()
if buffered > 0 {
peeked, _ := sc.reader.Peek(buffered)
full := make([]byte, len(recorded)+len(peeked))
copy(full, recorded)
copy(full[len(recorded):], peeked)
return full
}
return recorded
}
func (sc *Conn) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
sc.writeMu.Lock()
defer sc.writeMu.Unlock()
sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p)
return len(p), writeFull(sc.Conn, sc.writeBuf)
}
func (sc *Conn) Read(p []byte) (n int, err error) {
if n, ok := drainPending(p, &sc.pendingData); ok {
return n, nil
}
for {
if sc.pendingData.available() > 0 {
break
}
nr, rErr := sc.reader.Read(sc.rawBuf)
if nr > 0 {
chunk := sc.rawBuf[:nr]
if sc.recording.Load() {
sc.recordLock.Lock()
if sc.recording.Load() && sc.recorder != nil {
sc.recorder.Write(chunk)
}
sc.recordLock.Unlock()
}
layout := sc.table.layout
for _, b := range chunk {
if !layout.hintTable[b] {
continue
}
sc.hintBuf[sc.hintCount] = b
sc.hintCount++
if sc.hintCount == len(sc.hintBuf) {
key := packHintsToKey(sc.hintBuf)
val, ok := sc.table.DecodeMap[key]
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
sc.pendingData.appendByte(val)
sc.hintCount = 0
}
}
}
if rErr != nil {
return 0, rErr
}
if sc.pendingData.available() > 0 {
break
}
}
n, _ = drainPending(p, &sc.pendingData)
return n, nil
}

View File

@@ -0,0 +1,36 @@
package sudoku
func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte {
if len(p) == 0 {
return dst[:0]
}
outCapacity := len(p)*6 + 1
if cap(dst) < outCapacity {
dst = make([]byte, 0, outCapacity)
}
out := dst[:0]
pads := table.PaddingPool
padLen := len(pads)
for _, b := range p {
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
puzzles := table.EncodeTable[b]
puzzle := puzzles[rng.Intn(len(puzzles))]
perm := perm4[rng.Intn(len(perm4))]
for _, idx := range perm {
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
out = append(out, puzzle[idx])
}
}
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
return out
}

View File

@@ -0,0 +1,46 @@
package sudoku
// Grid represents a 4x4 sudoku grid
type Grid [16]uint8
// GenerateAllGrids generates all valid 4x4 Sudoku grids
func GenerateAllGrids() []Grid {
var grids []Grid
var g Grid
var backtrack func(int)
backtrack = func(idx int) {
if idx == 16 {
grids = append(grids, g)
return
}
row, col := idx/4, idx%4
br, bc := (row/2)*2, (col/2)*2
for num := uint8(1); num <= 4; num++ {
valid := true
for i := 0; i < 4; i++ {
if g[row*4+i] == num || g[i*4+col] == num {
valid = false
break
}
}
if valid {
for r := 0; r < 2; r++ {
for c := 0; c < 2; c++ {
if g[(br+r)*4+(bc+c)] == num {
valid = false
break
}
}
}
}
if valid {
g[idx] = num
backtrack(idx + 1)
g[idx] = 0
}
}
}
backtrack(0)
return grids
}

View File

@@ -0,0 +1,255 @@
package sudoku
import (
"fmt"
"math/bits"
"sort"
"strings"
)
type byteLayout struct {
name string
hintMask byte
hintValue byte
padMarker byte
paddingPool []byte
hintTable [256]bool
encodeHint [4][16]byte
encodeGroup [64]byte
decodeGroup [256]byte
groupValid [256]bool
}
func (l *byteLayout) isHint(b byte) bool {
return l != nil && l.hintTable[b]
}
func (l *byteLayout) hintByte(val, pos byte) byte {
return l.encodeHint[val&0x03][pos&0x0F]
}
func (l *byteLayout) groupByte(group byte) byte {
return l.encodeGroup[group&0x3F]
}
func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) {
if l == nil {
return 0, false
}
return l.decodeGroup[b], l.groupValid[b]
}
// resolveLayout picks the byte layout for a single traffic direction.
// ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred.
func resolveLayout(mode string, customPattern string) (*byteLayout, error) {
switch strings.ToLower(mode) {
case "ascii", "prefer_ascii":
return newASCIILayout(), nil
case "entropy", "prefer_entropy", "":
// fallback to entropy unless a custom pattern is provided
default:
return nil, fmt.Errorf("invalid ascii mode: %s", mode)
}
if strings.TrimSpace(customPattern) != "" {
return newCustomLayout(customPattern)
}
return newEntropyLayout(), nil
}
func newASCIILayout() *byteLayout {
padding := make([]byte, 0, 32)
for i := 0; i < 32; i++ {
padding = append(padding, byte(0x20+i))
}
layout := &byteLayout{
name: "ascii",
hintMask: 0x40,
hintValue: 0x40,
padMarker: 0x3F,
paddingPool: padding,
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := byte(0x40 | (byte(val) << 4) | byte(pos))
if b == 0x7F {
b = '\n'
}
layout.encodeHint[val][pos] = b
}
}
for group := 0; group < 64; group++ {
b := byte(0x40 | byte(group))
if b == 0x7F {
b = '\n'
}
layout.encodeGroup[group] = b
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & 0x40) == 0x40 {
layout.hintTable[wire] = true
layout.decodeGroup[wire] = wire & 0x3F
layout.groupValid[wire] = true
}
}
layout.hintTable['\n'] = true
layout.decodeGroup['\n'] = 0x3F
layout.groupValid['\n'] = true
return layout
}
func newEntropyLayout() *byteLayout {
padding := make([]byte, 0, 16)
for i := 0; i < 8; i++ {
padding = append(padding, byte(0x80+i))
padding = append(padding, byte(0x10+i))
}
layout := &byteLayout{
name: "entropy",
hintMask: 0x90,
hintValue: 0x00,
padMarker: 0x80,
paddingPool: padding,
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos)
}
}
for group := 0; group < 64; group++ {
v := byte(group)
layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F)
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & 0x90) != 0 {
continue
}
layout.hintTable[wire] = true
layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F)
layout.groupValid[wire] = true
}
return layout
}
func newCustomLayout(pattern string) (*byteLayout, error) {
cleaned := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(pattern), " ", ""))
if len(cleaned) != 8 {
return nil, fmt.Errorf("custom table must have 8 symbols, got %d", len(cleaned))
}
var xBits, pBits, vBits []uint8
for i, c := range cleaned {
bit := uint8(7 - i)
switch c {
case 'x':
xBits = append(xBits, bit)
case 'p':
pBits = append(pBits, bit)
case 'v':
vBits = append(vBits, bit)
default:
return nil, fmt.Errorf("invalid char %q in custom table", c)
}
}
if len(xBits) != 2 || len(pBits) != 2 || len(vBits) != 4 {
return nil, fmt.Errorf("custom table must contain exactly 2 x, 2 p, 4 v")
}
xMask := byte(0)
for _, b := range xBits {
xMask |= 1 << b
}
encodeBits := func(val, pos byte, dropX int) byte {
var out byte
out |= xMask
if dropX >= 0 {
out &^= 1 << xBits[dropX]
}
if (val & 0x02) != 0 {
out |= 1 << pBits[0]
}
if (val & 0x01) != 0 {
out |= 1 << pBits[1]
}
for i, bit := range vBits {
if (pos>>(3-uint8(i)))&0x01 == 1 {
out |= 1 << bit
}
}
return out
}
paddingSet := make(map[byte]struct{})
var padding []byte
for drop := range xBits {
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := encodeBits(byte(val), byte(pos), drop)
if bits.OnesCount8(b) >= 5 {
if _, ok := paddingSet[b]; !ok {
paddingSet[b] = struct{}{}
padding = append(padding, b)
}
}
}
}
}
sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] })
if len(padding) == 0 {
return nil, fmt.Errorf("custom table produced empty padding pool")
}
layout := &byteLayout{
name: fmt.Sprintf("custom(%s)", cleaned),
hintMask: xMask,
hintValue: xMask,
padMarker: padding[0],
paddingPool: padding,
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1)
}
}
for group := 0; group < 64; group++ {
val := byte(group>>4) & 0x03
pos := byte(group) & 0x0F
layout.encodeGroup[group] = encodeBits(val, pos, -1)
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & xMask) != xMask {
continue
}
layout.hintTable[wire] = true
var val, pos byte
if wire&(1<<pBits[0]) != 0 {
val |= 0x02
}
if wire&(1<<pBits[1]) != 0 {
val |= 0x01
}
for i, bit := range vBits {
if wire&(1<<bit) != 0 {
pos |= 1 << (3 - uint8(i))
}
}
layout.decodeGroup[wire] = (val << 4) | pos
layout.groupValid[wire] = true
}
return layout, nil
}

View File

@@ -0,0 +1,359 @@
package sudoku
import (
"bufio"
"io"
"net"
"sync"
)
const (
RngBatchSize = 128
packedProtectedPrefixBytes = 14
)
// PackedConn encodes traffic with the packed Sudoku layout while preserving
// the same padding model as the regular connection.
type PackedConn struct {
net.Conn
table *Table
reader *bufio.Reader
// Read-side buffers.
rawBuf []byte
pendingData pendingBuffer
// Write-side state.
writeMu sync.Mutex
writeBuf []byte
bitBuf uint64
bitCount int
// Read-side bit accumulator.
readBitBuf uint64
readBits int
// Padding selection matches Conn's threshold-based model.
rng randomSource
paddingThreshold uint64
padMarker byte
padPool []byte
}
func (pc *PackedConn) CloseWrite() error {
if pc == nil || pc.Conn == nil {
return nil
}
if cw, ok := pc.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (pc *PackedConn) CloseRead() error {
if pc == nil || pc.Conn == nil {
return nil
}
if cr, ok := pc.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
localRng := newSeededRand()
pc := &PackedConn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: newPendingBuffer(4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
}
pc.padMarker = table.layout.padMarker
for _, b := range table.PaddingPool {
if b != pc.padMarker {
pc.padPool = append(pc.padPool, b)
}
}
if len(pc.padPool) == 0 {
pc.padPool = append(pc.padPool, pc.padMarker)
}
return pc
}
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
if shouldPad(pc.rng, pc.paddingThreshold) {
out = append(out, pc.getPaddingByte())
}
return out
}
func (pc *PackedConn) appendGroup(out []byte, group byte) []byte {
out = pc.maybeAddPadding(out)
return append(out, pc.table.layout.groupByte(group))
}
func (pc *PackedConn) appendForcedPadding(out []byte) []byte {
return append(out, pc.getPaddingByte())
}
func (pc *PackedConn) nextProtectedPrefixGap() int {
return 1 + pc.rng.Intn(2)
}
func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) {
if len(p) == 0 {
return out, 0
}
limit := len(p)
if limit > packedProtectedPrefixBytes {
limit = packedProtectedPrefixBytes
}
for padCount := 0; padCount < 1+pc.rng.Intn(2); padCount++ {
out = pc.appendForcedPadding(out)
}
gap := pc.nextProtectedPrefixGap()
effective := 0
for i := 0; i < limit; i++ {
pc.bitBuf = (pc.bitBuf << 8) | uint64(p[i])
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.appendGroup(out, group&0x3F)
}
effective++
if effective >= gap {
out = pc.appendForcedPadding(out)
effective = 0
gap = pc.nextProtectedPrefixGap()
}
}
return out, limit
}
func (pc *PackedConn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
needed := len(p)*3/2 + 32
if cap(pc.writeBuf) < needed {
pc.writeBuf = make([]byte, 0, needed)
}
out := pc.writeBuf[:0]
var prefixN int
out, prefixN = pc.writeProtectedPrefix(out, p)
i := prefixN
n := len(p)
for pc.bitCount > 0 && i < n {
b := p[i]
i++
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.appendGroup(out, group&0x3F)
}
}
for i+11 < n {
for batch := 0; batch < 4; batch++ {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
out = pc.appendGroup(out, g1)
out = pc.appendGroup(out, g2)
out = pc.appendGroup(out, g3)
out = pc.appendGroup(out, g4)
}
}
for i+2 < n {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
out = pc.appendGroup(out, g1)
out = pc.appendGroup(out, g2)
out = pc.appendGroup(out, g3)
out = pc.appendGroup(out, g4)
}
for ; i < n; i++ {
b := p[i]
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.appendGroup(out, group&0x3F)
}
}
if pc.bitCount > 0 {
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = pc.appendGroup(out, group&0x3F)
out = append(out, pc.padMarker)
}
out = pc.maybeAddPadding(out)
if len(out) > 0 {
pc.writeBuf = out[:0]
return len(p), writeFull(pc.Conn, out)
}
pc.writeBuf = out[:0]
return len(p), nil
}
func (pc *PackedConn) Flush() error {
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
out := pc.writeBuf[:0]
if pc.bitCount > 0 {
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.table.layout.groupByte(group&0x3F))
out = append(out, pc.padMarker)
}
out = pc.maybeAddPadding(out)
if len(out) > 0 {
pc.writeBuf = out[:0]
return writeFull(pc.Conn, out)
}
return nil
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
b = b[n:]
}
return nil
}
func (pc *PackedConn) Read(p []byte) (int, error) {
if n, ok := drainPending(p, &pc.pendingData); ok {
return n, nil
}
for {
nr, rErr := pc.reader.Read(pc.rawBuf)
if nr > 0 {
rBuf := pc.readBitBuf
rBits := pc.readBits
padMarker := pc.padMarker
layout := pc.table.layout
for _, b := range pc.rawBuf[:nr] {
if !layout.hintTable[b] {
if b == padMarker {
rBuf = 0
rBits = 0
}
continue
}
group, ok := layout.decodePackedGroup(b)
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
rBuf = (rBuf << 6) | uint64(group)
rBits += 6
if rBits >= 8 {
rBits -= 8
val := byte(rBuf >> rBits)
pc.pendingData.appendByte(val)
if rBits == 0 {
rBuf = 0
} else {
rBuf &= (uint64(1) << rBits) - 1
}
}
}
pc.readBitBuf = rBuf
pc.readBits = rBits
}
if rErr != nil {
if rErr == io.EOF {
pc.readBitBuf = 0
pc.readBits = 0
}
if pc.pendingData.available() > 0 {
break
}
return 0, rErr
}
if pc.pendingData.available() > 0 {
break
}
}
n, _ := drainPending(p, &pc.pendingData)
return n, nil
}
func (pc *PackedConn) getPaddingByte() byte {
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
}

View File

@@ -0,0 +1,42 @@
package sudoku
const probOne = uint64(1) << 32
func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 {
if r == nil {
return 0
}
if pMin < 0 {
pMin = 0
}
if pMax < pMin {
pMax = pMin
}
if pMax > 100 {
pMax = 100
}
if pMin > 100 {
pMin = 100
}
min := uint64(pMin) * probOne / 100
max := uint64(pMax) * probOne / 100
if max <= min {
return min
}
u := uint64(r.Uint32())
return min + (u * (max - min) >> 32)
}
func shouldPad(r randomSource, threshold uint64) bool {
if threshold == 0 {
return false
}
if threshold >= probOne {
return true
}
if r == nil {
return false
}
return uint64(r.Uint32()) < threshold
}

View File

@@ -0,0 +1,57 @@
package sudoku
type pendingBuffer struct {
data []byte
off int
}
func newPendingBuffer(capacity int) pendingBuffer {
return pendingBuffer{data: make([]byte, 0, capacity)}
}
func (p *pendingBuffer) available() int {
if p == nil {
return 0
}
return len(p.data) - p.off
}
func (p *pendingBuffer) reset() {
if p == nil {
return
}
p.data = p.data[:0]
p.off = 0
}
func (p *pendingBuffer) ensureAppendCapacity(extra int) {
if p == nil || extra <= 0 || p.off == 0 {
return
}
if cap(p.data)-len(p.data) >= extra {
return
}
unread := len(p.data) - p.off
copy(p.data[:unread], p.data[p.off:])
p.data = p.data[:unread]
p.off = 0
}
func (p *pendingBuffer) appendByte(b byte) {
p.ensureAppendCapacity(1)
p.data = append(p.data, b)
}
func drainPending(dst []byte, pending *pendingBuffer) (int, bool) {
if pending == nil || pending.available() == 0 {
return 0, false
}
n := copy(dst, pending.data[pending.off:])
pending.off += n
if pending.off == len(pending.data) {
pending.reset()
}
return n, true
}

View File

@@ -0,0 +1,56 @@
package sudoku
import (
crypto_rand "crypto/rand"
"encoding/binary"
"time"
)
type randomSource interface {
Uint32() uint32
Uint64() uint64
Intn(n int) int
}
type sudokuRand struct {
state uint64
}
func newSeededRand() *sudokuRand {
seed := time.Now().UnixNano()
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err == nil {
seed = int64(binary.BigEndian.Uint64(seedBytes[:]))
}
return newSudokuRand(seed)
}
func newSudokuRand(seed int64) *sudokuRand {
state := uint64(seed)
if state == 0 {
state = 0x9e3779b97f4a7c15
}
return &sudokuRand{state: state}
}
func (r *sudokuRand) Uint64() uint64 {
if r == nil {
return 0
}
r.state += 0x9e3779b97f4a7c15
z := r.state
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
z = (z ^ (z >> 27)) * 0x94d049bb133111eb
return z ^ (z >> 31)
}
func (r *sudokuRand) Uint32() uint32 {
return uint32(r.Uint64() >> 32)
}
func (r *sudokuRand) Intn(n int) int {
if n <= 1 {
return 0
}
return int((uint64(r.Uint32()) * uint64(n)) >> 32)
}

View File

@@ -0,0 +1,214 @@
package sudoku
import (
"crypto/sha256"
"encoding/binary"
"errors"
"math/rand"
"strings"
)
var (
ErrInvalidSudokuMapMiss = errors.New("INVALID_SUDOKU_MAP_MISS")
)
type Table struct {
EncodeTable [256][][4]byte
DecodeMap map[uint32]byte
PaddingPool []byte
IsASCII bool // 标记当前模式
layout *byteLayout
opposite *Table
hint uint32
}
// NewTable initializes the obfuscation tables with built-in layouts.
// Equivalent to calling NewTableWithCustom(key, mode, "").
func NewTable(key string, mode string) *Table {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
panic(err)
}
return t
}
// NewTableWithCustom initializes the uplink/probe Sudoku table using either predefined
// or directional layouts. Directional modes such as "up_ascii_down_entropy" return the
// client->server table and internally attach the opposite direction table for runtime use.
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
asciiMode, err := ParseASCIIMode(mode)
if err != nil {
return nil, err
}
uplinkPattern := customPatternForToken(asciiMode.Uplink, customPattern)
downlinkPattern := customPatternForToken(asciiMode.Downlink, customPattern)
hint := tableHintFingerprint(key, asciiMode.Canonical(), uplinkPattern, downlinkPattern)
uplink, err := newSingleDirectionTable(key, asciiMode.uplinkPreference(), uplinkPattern)
if err != nil {
return nil, err
}
uplink.hint = hint
if asciiMode.Uplink == asciiMode.Downlink {
uplink.opposite = uplink
return uplink, nil
}
downlink, err := newSingleDirectionTable(key, asciiMode.downlinkPreference(), downlinkPattern)
if err != nil {
return nil, err
}
downlink.hint = hint
uplink.opposite = downlink
downlink.opposite = uplink
return uplink, nil
}
func newSingleDirectionTable(key string, mode string, customPattern string) (*Table, error) {
layout, err := resolveLayout(mode, customPattern)
if err != nil {
return nil, err
}
t := &Table{
DecodeMap: make(map[uint32]byte),
IsASCII: layout.name == "ascii",
layout: layout,
}
t.PaddingPool = append(t.PaddingPool, layout.paddingPool...)
// 生成数独网格 (逻辑不变)
allGrids := GenerateAllGrids()
h := sha256.New()
h.Write([]byte(key))
seed := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
rng := rand.New(rand.NewSource(seed))
shuffledGrids := make([]Grid, 288)
copy(shuffledGrids, allGrids)
rng.Shuffle(len(shuffledGrids), func(i, j int) {
shuffledGrids[i], shuffledGrids[j] = shuffledGrids[j], shuffledGrids[i]
})
// 预计算组合
var combinations [][]int
var combine func(int, int, []int)
combine = func(s, k int, c []int) {
if k == 0 {
tmp := make([]int, len(c))
copy(tmp, c)
combinations = append(combinations, tmp)
return
}
for i := s; i <= 16-k; i++ {
c = append(c, i)
combine(i+1, k-1, c)
c = c[:len(c)-1]
}
}
combine(0, 4, []int{})
// 构建映射表
for byteVal := 0; byteVal < 256; byteVal++ {
targetGrid := shuffledGrids[byteVal]
for _, positions := range combinations {
var currentHints [4]byte
// 1. 计算抽象提示 (Abstract Hints)
// 我们先计算出 val 和 pos后面再根据模式编码成 byte
var rawParts [4]struct{ val, pos byte }
for i, pos := range positions {
val := targetGrid[pos] // 1..4
rawParts[i] = struct{ val, pos byte }{val, uint8(pos)}
}
// 检查唯一性 (数独逻辑)
matchCount := 0
for _, g := range allGrids {
match := true
for _, p := range rawParts {
if g[p.pos] != p.val {
match = false
break
}
}
if match {
matchCount++
if matchCount > 1 {
break
}
}
}
if matchCount == 1 {
// 唯一确定,生成最终编码字节
for i, p := range rawParts {
currentHints[i] = t.layout.hintByte(p.val-1, p.pos)
}
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
key := packHintsToKey(currentHints)
t.DecodeMap[key] = byte(byteVal)
}
}
}
return t, nil
}
func customPatternForToken(token string, customPattern string) string {
if token == asciiModeTokenEntropy {
return customPattern
}
return ""
}
func (t *Table) OppositeDirection() *Table {
if t == nil || t.opposite == nil {
return t
}
return t.opposite
}
func (t *Table) Hint() uint32 {
if t == nil {
return 0
}
return t.hint
}
func tableHintFingerprint(key string, mode string, uplinkPattern string, downlinkPattern string) uint32 {
sum := sha256.Sum256([]byte(strings.Join([]string{
"sudoku-table-hint",
key,
mode,
strings.ToLower(strings.TrimSpace(uplinkPattern)),
strings.ToLower(strings.TrimSpace(downlinkPattern)),
}, "\x00")))
return binary.BigEndian.Uint32(sum[:4])
}
func packHintsToKey(hints [4]byte) uint32 {
// Sorting network for 4 elements (Bubble sort unrolled)
// Swap if a > b
if hints[0] > hints[1] {
hints[0], hints[1] = hints[1], hints[0]
}
if hints[2] > hints[3] {
hints[2], hints[3] = hints[3], hints[2]
}
if hints[0] > hints[2] {
hints[0], hints[2] = hints[2], hints[0]
}
if hints[1] > hints[3] {
hints[1], hints[3] = hints[3], hints[1]
}
if hints[1] > hints[2] {
hints[1], hints[2] = hints[2], hints[1]
}
return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3])
}

View File

@@ -0,0 +1,38 @@
package sudoku
import "fmt"
// TableSet is a small helper for managing multiple Sudoku tables (e.g. for per-connection rotation).
// It is intentionally decoupled from the tunnel/app layers.
type TableSet struct {
Tables []*Table
}
// NewTableSet builds one or more tables from key/mode and a list of custom X/P/V patterns.
// If patterns is empty, it builds a single default table (customPattern="").
func NewTableSet(key string, mode string, patterns []string) (*TableSet, error) {
if len(patterns) == 0 {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
return nil, err
}
return &TableSet{Tables: []*Table{t}}, nil
}
tables := make([]*Table, 0, len(patterns))
for i, pattern := range patterns {
t, err := NewTableWithCustom(key, mode, pattern)
if err != nil {
return nil, fmt.Errorf("build table[%d] (%q): %w", i, pattern, err)
}
tables = append(tables, t)
}
return &TableSet{Tables: tables}, nil
}
func (ts *TableSet) Candidates() []*Table {
if ts == nil {
return nil
}
return ts.Tables
}

View File

@@ -0,0 +1,74 @@
package sudoku
import (
"sync"
"time"
)
var handshakeReplayTTL = 60 * time.Second
type nonceSet struct {
mu sync.Mutex
m map[[kipHelloNonceSize]byte]time.Time
maxEntries int
lastPrune time.Time
}
func newNonceSet(maxEntries int) *nonceSet {
if maxEntries <= 0 {
maxEntries = 4096
}
return &nonceSet{
m: make(map[[kipHelloNonceSize]byte]time.Time),
maxEntries: maxEntries,
}
}
func (s *nonceSet) allow(nonce [kipHelloNonceSize]byte, now time.Time, ttl time.Duration) bool {
s.mu.Lock()
defer s.mu.Unlock()
if ttl <= 0 {
ttl = 60 * time.Second
}
if now.Sub(s.lastPrune) > ttl/2 || len(s.m) > s.maxEntries {
for k, exp := range s.m {
if !now.Before(exp) {
delete(s.m, k)
}
}
s.lastPrune = now
for len(s.m) > s.maxEntries {
for k := range s.m {
delete(s.m, k)
break
}
}
}
if exp, ok := s.m[nonce]; ok && now.Before(exp) {
return false
}
s.m[nonce] = now.Add(ttl)
return true
}
type handshakeReplayProtector struct {
users sync.Map // map[userHash string]*nonceSet
}
func (p *handshakeReplayProtector) allow(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool {
if userHash == "" {
userHash = "_"
}
val, _ := p.users.LoadOrStore(userHash, newNonceSet(4096))
set, ok := val.(*nonceSet)
if !ok || set == nil {
set = newNonceSet(4096)
p.users.Store(userHash, set)
}
return set.allow(nonce, now, handshakeReplayTTL)
}
var globalHandshakeReplay = &handshakeReplayProtector{}

View File

@@ -0,0 +1,58 @@
package sudoku
import (
"crypto/ecdh"
"crypto/sha256"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
func derivePSKDirectionalBases(psk string) (c2s, s2c []byte) {
sum := sha256.Sum256([]byte(psk))
c2sKey := make([]byte, 32)
s2cKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, sum[:], []byte("sudoku-psk-c2s")), c2sKey); err != nil {
panic("sudoku: hkdf expand failed")
}
if _, err := io.ReadFull(hkdf.Expand(sha256.New, sum[:], []byte("sudoku-psk-s2c")), s2cKey); err != nil {
panic("sudoku: hkdf expand failed")
}
return c2sKey, s2cKey
}
func deriveSessionDirectionalBases(psk string, shared []byte, nonce [kipHelloNonceSize]byte) (c2s, s2c []byte, err error) {
sum := sha256.Sum256([]byte(psk))
ikm := make([]byte, 0, len(shared)+len(nonce))
ikm = append(ikm, shared...)
ikm = append(ikm, nonce[:]...)
prk := hkdf.Extract(sha256.New, ikm, sum[:])
c2sKey := make([]byte, 32)
s2cKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, prk, []byte("sudoku-session-c2s")), c2sKey); err != nil {
return nil, nil, fmt.Errorf("hkdf expand c2s: %w", err)
}
if _, err := io.ReadFull(hkdf.Expand(sha256.New, prk, []byte("sudoku-session-s2c")), s2cKey); err != nil {
return nil, nil, fmt.Errorf("hkdf expand s2c: %w", err)
}
return c2sKey, s2cKey, nil
}
func x25519SharedSecret(priv *ecdh.PrivateKey, peerPub []byte) ([]byte, error) {
if priv == nil {
return nil, fmt.Errorf("nil priv")
}
curve := ecdh.X25519()
pk, err := curve.NewPublicKey(peerPub)
if err != nil {
return nil, fmt.Errorf("parse peer pub: %w", err)
}
secret, err := priv.ECDH(pk)
if err != nil {
return nil, fmt.Errorf("ecdh: %w", err)
}
return secret, nil
}

View File

@@ -0,0 +1,164 @@
package sudoku
import (
"bufio"
"bytes"
crand "crypto/rand"
"errors"
"fmt"
"io"
"net"
"time"
"github.com/sagernet/sing-box/transport/sudoku/crypto"
"github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
type clientTableChoice struct {
Table *sudoku.Table
Hint uint32
HasHint bool
}
func pickClientTable(cfg *ProtocolConfig) (clientTableChoice, error) {
candidates := cfg.tableCandidates()
if len(candidates) == 0 {
return clientTableChoice{}, fmt.Errorf("no table configured")
}
if len(candidates) == 1 {
return clientTableChoice{Table: candidates[0], Hint: candidates[0].Hint()}, nil
}
var b [1]byte
if _, err := crand.Read(b[:]); err != nil {
return clientTableChoice{}, fmt.Errorf("random table pick failed: %w", err)
}
idx := int(b[0]) % len(candidates)
return clientTableChoice{Table: candidates[idx], Hint: candidates[idx].Hint(), HasHint: true}, nil
}
type readOnlyConn struct {
*bytes.Reader
}
func (c *readOnlyConn) Write([]byte) (int, error) { return 0, io.ErrClosedPipe }
func (c *readOnlyConn) Close() error { return nil }
func (c *readOnlyConn) LocalAddr() net.Addr { return nil }
func (c *readOnlyConn) RemoteAddr() net.Addr { return nil }
func (c *readOnlyConn) SetDeadline(time.Time) error { return nil }
func (c *readOnlyConn) SetReadDeadline(time.Time) error { return nil }
func (c *readOnlyConn) SetWriteDeadline(time.Time) error { return nil }
func drainBuffered(r *bufio.Reader) ([]byte, error) {
n := r.Buffered()
if n <= 0 {
return nil, nil
}
out := make([]byte, n)
_, err := io.ReadFull(r, out)
return out, err
}
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
seed := ServerAEADSeed(cfg.Key)
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
// Server side: recv is client->server, send is server->client.
cConn, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskS2C, pskC2S)
if err != nil {
return err
}
msg, err := ReadKIPMessage(cConn)
if err != nil {
return err
}
if msg.Type != KIPTypeClientHello {
return fmt.Errorf("unexpected handshake message: %d", msg.Type)
}
ch, err := DecodeKIPClientHelloPayload(msg.Payload)
if err != nil {
return err
}
if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(kipHandshakeSkew.Seconds()) {
return fmt.Errorf("time skew/replay")
}
return nil
}
func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
const (
maxProbeBytes = 64 * 1024
readChunk = 4 * 1024
)
if len(tables) == 0 {
return nil, nil, fmt.Errorf("no table candidates")
}
if len(tables) > 255 {
return nil, nil, fmt.Errorf("too many table candidates: %d", len(tables))
}
// Copy so we can prune candidates without mutating the caller slice.
candidates := make([]*sudoku.Table, 0, len(tables))
for _, t := range tables {
if t != nil {
candidates = append(candidates, t)
}
}
if len(candidates) == 0 {
return nil, nil, fmt.Errorf("no table candidates")
}
probe, err := drainBuffered(r)
if err != nil {
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
}
tmp := make([]byte, readChunk)
for {
if len(candidates) == 1 {
tail, err := drainBuffered(r)
if err != nil {
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
}
probe = append(probe, tail...)
return candidates[0], probe, nil
}
needMore := false
next := candidates[:0]
for _, table := range candidates {
err := probeHandshakeBytes(probe, cfg, table)
if err == nil {
tail, err := drainBuffered(r)
if err != nil {
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
}
probe = append(probe, tail...)
return table, probe, nil
}
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
needMore = true
next = append(next, table)
}
// Definitive mismatch: drop table.
}
candidates = next
if len(candidates) == 0 || !needMore {
return nil, probe, fmt.Errorf("handshake table selection failed")
}
if len(probe) >= maxProbeBytes {
return nil, probe, fmt.Errorf("handshake probe exceeded %d bytes", maxProbeBytes)
}
n, err := r.Read(tmp)
if n > 0 {
probe = append(probe, tmp[:n]...)
}
if err != nil {
return nil, probe, fmt.Errorf("handshake probe read failed: %w", err)
}
}
}

View File

@@ -0,0 +1,68 @@
package sudoku
import (
"strings"
"github.com/sagernet/sing-box/transport/sudoku/obfs/sudoku"
)
func normalizeCustomPatterns(customTable string, customTables []string) []string {
patterns := customTables
if len(patterns) == 0 && strings.TrimSpace(customTable) != "" {
patterns = []string{customTable}
}
if len(patterns) == 0 {
patterns = []string{""}
}
return patterns
}
func normalizeTablePatterns(tableType string, customTable string, customTables []string) ([]string, error) {
patterns := normalizeCustomPatterns(customTable, customTables)
if _, err := sudoku.ParseASCIIMode(tableType); err != nil {
return nil, err
}
return patterns, nil
}
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
// When customTables is non-empty it overrides customTable (matching upstream Sudoku behavior).
//
// Deprecated-ish: prefer NewClientTablesWithCustomPatterns / NewServerTablesWithCustomPatterns.
func NewTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
patterns, err := normalizeTablePatterns(tableType, customTable, customTables)
if err != nil {
return nil, err
}
tables := make([]*sudoku.Table, 0, len(patterns))
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
t, err := NewTableWithCustom(key, tableType, pattern)
if err != nil {
return nil, err
}
tables = append(tables, t)
}
return tables, nil
}
func NewClientTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
return NewTablesWithCustomPatterns(key, tableType, customTable, customTables)
}
// NewServerTablesWithCustomPatterns matches upstream server behavior: when probeable custom table
// rotation is enabled, also accept the default table to avoid forcing clients to update in lockstep.
func NewServerTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
patterns, err := normalizeTablePatterns(tableType, customTable, customTables)
if err != nil {
return nil, err
}
asciiMode, err := sudoku.ParseASCIIMode(tableType)
if err != nil {
return nil, err
}
if asciiMode.Uplink == "entropy" && len(patterns) > 0 && strings.TrimSpace(patterns[0]) != "" {
patterns = append([]string{""}, patterns...)
}
return NewTablesWithCustomPatterns(key, tableType, "", patterns)
}

165
transport/sudoku/uot.go Normal file
View File

@@ -0,0 +1,165 @@
package sudoku
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"sync"
"time"
)
const (
maxUoTPayload = 64 * 1024
)
// WriteDatagram sends a single UDP datagram frame over a reliable stream.
func WriteDatagram(w io.Writer, addr string, payload []byte) error {
addrBuf, err := EncodeAddress(addr)
if err != nil {
return fmt.Errorf("encode address: %w", err)
}
if addrLen := len(addrBuf); addrLen == 0 || addrLen > maxUoTPayload {
return fmt.Errorf("address too long: %d", len(addrBuf))
}
if payloadLen := len(payload); payloadLen > maxUoTPayload {
return fmt.Errorf("payload too large: %d", payloadLen)
}
var header [4]byte
binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf)))
binary.BigEndian.PutUint16(header[2:], uint16(len(payload)))
return writeAllChunks(w, header[:], addrBuf, payload)
}
// ReadDatagram parses a single UDP datagram frame from the reliable stream.
func ReadDatagram(r io.Reader) (string, []byte, error) {
addr, payloadLen, err := readDatagramHeaderAndAddress(r)
if err != nil {
return "", nil, err
}
payload := make([]byte, payloadLen)
if _, err := io.ReadFull(r, payload); err != nil {
return "", nil, err
}
return addr, payload, nil
}
// UoTPacketConn adapts a net.Conn with the Sudoku UoT framing to net.PacketConn.
type UoTPacketConn struct {
conn net.Conn
writeMu sync.Mutex
}
func NewUoTPacketConn(conn net.Conn) *UoTPacketConn {
return &UoTPacketConn{conn: conn}
}
func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
for {
addrStr, payloadLen, err := readDatagramHeaderAndAddress(c.conn)
if err != nil {
return 0, nil, err
}
udpAddr, err := parseDatagramUDPAddr(addrStr)
if payloadLen > len(p) {
if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil {
return 0, nil, discardErr
}
return 0, nil, io.ErrShortBuffer
}
if err != nil {
if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil {
return 0, nil, discardErr
}
continue
}
if _, err := io.ReadFull(c.conn, p[:payloadLen]); err != nil {
return 0, nil, err
}
return payloadLen, udpAddr, nil
}
}
func (c *UoTPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
if addr == nil {
return 0, errors.New("address is nil")
}
c.writeMu.Lock()
defer c.writeMu.Unlock()
if err := WriteDatagram(c.conn, addr.String(), p); err != nil {
return 0, err
}
return len(p), nil
}
func (c *UoTPacketConn) Close() error {
return c.conn.Close()
}
func (c *UoTPacketConn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *UoTPacketConn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *UoTPacketConn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}
func readDatagramHeaderAndAddress(r io.Reader) (string, int, error) {
var header [4]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return "", 0, err
}
addrLen := int(binary.BigEndian.Uint16(header[:2]))
payloadLen := int(binary.BigEndian.Uint16(header[2:]))
if addrLen <= 0 || addrLen > maxUoTPayload {
return "", 0, fmt.Errorf("invalid address length: %d", addrLen)
}
if payloadLen < 0 || payloadLen > maxUoTPayload {
return "", 0, fmt.Errorf("invalid payload length: %d", payloadLen)
}
addrBuf := make([]byte, addrLen)
if _, err := io.ReadFull(r, addrBuf); err != nil {
return "", 0, err
}
addr, err := DecodeAddress(bytes.NewReader(addrBuf))
if err != nil {
return "", 0, fmt.Errorf("decode address: %w", err)
}
return addr, payloadLen, nil
}
func parseDatagramUDPAddr(addr string) (*net.UDPAddr, error) {
addrPort, err := netip.ParseAddrPort(addr)
if err != nil {
return nil, err
}
return net.UDPAddrFromAddrPort(netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())), nil
}
func discardBytes(r io.Reader, n int) error {
if n <= 0 {
return nil
}
_, err := io.CopyN(io.Discard, r, int64(n))
return err
}

View File

@@ -0,0 +1,19 @@
package sudoku
import "io"
func writeAllChunks(w io.Writer, chunks ...[]byte) error {
for _, chunk := range chunks {
for len(chunk) > 0 {
n, err := w.Write(chunk)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
chunk = chunk[n:]
}
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/binary"
"net"
"sync"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
@@ -26,6 +27,9 @@ type Service[K comparable] struct {
handler Handler
fallbackHandler N.TCPConnectionHandlerEx
logger logger.ContextLogger
conns map[K][]net.Conn
mu sync.RWMutex
}
func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandlerEx, logger logger.ContextLogger) *Service[K] {
@@ -35,6 +39,7 @@ func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHa
handler: handler,
fallbackHandler: fallbackHandler,
logger: logger,
conns: make(map[K][]net.Conn),
}
}
@@ -54,8 +59,20 @@ func (s *Service[K]) UpdateUsers(userList []K, passwordList []string) error {
users[user] = key
keys[key] = user
}
s.mu.Lock()
s.users = users
s.keys = keys
var closedConns []net.Conn
for user, conns := range s.conns {
if _, exists := users[user]; !exists {
closedConns = append(closedConns, conns...)
delete(s.conns, user)
}
}
s.mu.Unlock()
for _, conn := range closedConns {
conn.Close()
}
return nil
}
@@ -68,9 +85,30 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, source M.
return s.fallback(ctx, conn, source, key[:n], E.New("bad request size"), onClose)
}
s.mu.RLock()
if user, loaded := s.keys[key]; loaded {
s.mu.RUnlock()
ctx = auth.ContextWithUser(ctx, user)
s.mu.Lock()
s.conns[user] = append(s.conns[user], conn)
s.mu.Unlock()
originalOnClose := onClose
onClose = N.OnceClose(func(it error) {
s.mu.Lock()
conns := s.conns[user]
for i, c := range conns {
if c == conn {
s.conns[user] = append(conns[:i], conns[i+1:]...)
break
}
}
s.mu.Unlock()
if originalOnClose != nil {
originalOnClose(it)
}
})
} else {
s.mu.RUnlock()
return s.fallback(ctx, conn, source, key[:], E.New("bad request"), onClose)
}

View File

@@ -0,0 +1,323 @@
package trusttunnel
import (
"context"
stdtls "crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
qtls "github.com/sagernet/sing-quic"
"golang.org/x/net/http2"
)
var (
appName = "sing-box"
appVersion = C.Version
tcpUserAgent = runtime.GOOS + " " + appName + "/" + appVersion
udpUserAgent = runtime.GOOS + " " + UDPMagicAddress
icmpUserAgent = runtime.GOOS + " " + ICMPMagicAddress
)
type Dialer interface {
Dial(ctx context.Context, host string) (net.Conn, error)
ListenPacket(ctx context.Context) (net.PacketConn, error)
Close() error
}
type ClientOptions struct {
TLSDialer tls.Dialer
QUICDialer N.Dialer
QUICTLSConfig tls.Config
Server M.Socksaddr
Username string
Password string
QUIC bool
CongestionControl string
CWND int
BBRProfile string
HealthCheck bool
MaxConnections int
MinStreams int
MaxStreams int
}
type Client struct {
ctx context.Context
cancel context.CancelFunc
server M.Socksaddr
serverString string
auth string
roundTripper http.RoundTripper
startOnce sync.Once
healthCheck bool
healthCheckTimer *time.Timer
count atomic.Int64
}
func NewClient(ctx context.Context, options ClientOptions) (*Client, error) {
ctx, cancel := context.WithCancel(ctx)
client := &Client{
ctx: ctx,
cancel: cancel,
server: options.Server,
serverString: options.Server.String(),
auth: buildAuth(options.Username, options.Password),
healthCheck: options.HealthCheck,
}
if options.QUIC {
congestionControlFactory, err := NewCongestionControl(options.CongestionControl, options.CWND, options.BBRProfile, ntp.TimeFuncFromContext(ctx))
if err != nil {
cancel()
return nil, err
}
client.roundTripper = &http3.Transport{
QUICConfig: &quic.Config{
MaxIdleTimeout: DefaultSessionTimeout * 2,
KeepAlivePeriod: DefaultHealthCheckTimeout,
},
Dial: func(ctx context.Context, addr string, tlsCfg *stdtls.Config, cfg *quic.Config) (*quic.Conn, error) {
udpConn, err := options.QUICDialer.DialContext(ctx, N.NetworkUDP, client.server)
if err != nil {
return nil, err
}
conn, err := qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), options.QUICTLSConfig, cfg)
if err != nil {
return nil, err
}
conn.SetCongestionControl(congestionControlFactory(conn))
return conn, nil
},
}
} else {
client.roundTripper = &http2.Transport{
DialTLSContext: func(ctx context.Context, network, addr string, _ *stdtls.Config) (net.Conn, error) {
return options.TLSDialer.DialContext(ctx, network, client.server)
},
AllowHTTP: true,
}
}
return client, nil
}
func (c *Client) start() {
if c.healthCheck {
c.healthCheckTimer = time.NewTimer(DefaultHealthCheckTimeout)
go c.loopHealthCheck()
}
}
func (c *Client) loopHealthCheck() {
for {
select {
case <-c.healthCheckTimer.C:
case <-c.ctx.Done():
c.healthCheckTimer.Stop()
return
}
ctx, cancel := context.WithTimeout(c.ctx, DefaultHealthCheckTimeout)
_ = c.HealthCheck(ctx)
cancel()
}
}
func (c *Client) resetHealthCheckTimer() {
if c.healthCheckTimer == nil {
return
}
c.healthCheckTimer.Reset(DefaultHealthCheckTimeout)
}
func (c *Client) roundTrip(request *http.Request, conn *httpConn) {
c.startOnce.Do(c.start)
pipeReader, pipeWriter := io.Pipe()
request.Body = pipeReader
*conn = httpConn{writer: pipeWriter, created: make(chan struct{})}
c.count.Add(1)
conn.closeFn = sync.OnceFunc(func() { c.count.Add(-1) })
ctx, cancel := context.WithCancel(c.ctx)
conn.cancelFn = cancel
go func() {
timeout := time.AfterFunc(C.TCPTimeout, cancel)
defer timeout.Stop()
request = request.WithContext(ctx)
response, err := c.roundTripper.RoundTrip(request)
if err != nil {
_ = pipeWriter.CloseWithError(err)
_ = pipeReader.CloseWithError(err)
conn.setup(nil, err)
} else if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
err = fmt.Errorf("unexpected status code: %d", response.StatusCode)
_ = pipeWriter.CloseWithError(err)
_ = pipeReader.CloseWithError(err)
conn.setup(nil, err)
} else {
c.resetHealthCheckTimer()
conn.setup(response.Body, nil)
}
}()
}
func (c *Client) newConnectRequest(host, userAgent string) *http.Request {
return &http.Request{
Method: http.MethodConnect,
URL: &url.URL{Scheme: "https", Host: c.serverString},
Header: http.Header{
"User-Agent": {userAgent},
"Proxy-Authorization": {c.auth},
},
Host: host,
}
}
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
conn := &tcpConn{}
c.roundTrip(c.newConnectRequest(host, tcpUserAgent), &conn.httpConn)
return conn, nil
}
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
conn := &clientPacketConn{}
c.roundTrip(c.newConnectRequest(UDPMagicAddress, udpUserAgent), &conn.httpConn)
return conn, nil
}
func (c *Client) Close() error {
c.cancel()
if closer, ok := c.roundTripper.(io.Closer); ok {
_ = closer.Close()
}
if t, ok := c.roundTripper.(*http2.Transport); ok {
t.CloseIdleConnections()
}
if c.healthCheckTimer != nil {
c.healthCheckTimer.Stop()
}
return nil
}
func (c *Client) HealthCheck(ctx context.Context) error {
defer c.resetHealthCheckTimer()
response, err := c.roundTripper.RoundTrip(c.newConnectRequest(HealthCheckMagicAddress, runtime.GOOS).WithContext(ctx))
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
return nil
}
type MultiplexClient struct {
mutex sync.Mutex
maxConnections int
minStreams int
maxStreams int
ctx context.Context
options ClientOptions
clients []*Client
}
func NewMultiplexClient(ctx context.Context, options ClientOptions) (*MultiplexClient, error) {
maxConnections := options.MaxConnections
minStreams := options.MinStreams
maxStreams := options.MaxStreams
if maxConnections == 0 && minStreams == 0 && maxStreams == 0 {
maxConnections = 8
minStreams = 5
}
client, err := NewClient(ctx, options)
if err != nil {
return nil, err
}
return &MultiplexClient{
maxConnections: maxConnections,
minStreams: minStreams,
maxStreams: maxStreams,
ctx: ctx,
options: options,
clients: []*Client{client},
}, nil
}
func (c *MultiplexClient) Dial(ctx context.Context, host string) (net.Conn, error) {
t, err := c.getClient()
if err != nil {
return nil, err
}
return t.Dial(ctx, host)
}
func (c *MultiplexClient) ListenPacket(ctx context.Context) (net.PacketConn, error) {
t, err := c.getClient()
if err != nil {
return nil, err
}
return t.ListenPacket(ctx)
}
func (c *MultiplexClient) Close() error {
c.mutex.Lock()
defer c.mutex.Unlock()
var errs []error
for _, t := range c.clients {
if err := t.Close(); err != nil {
errs = append(errs, err)
}
}
c.clients = nil
return errors.Join(errs...)
}
func (c *MultiplexClient) getClient() (*Client, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
var transport *Client
for _, t := range c.clients {
if transport == nil || t.count.Load() < transport.count.Load() {
transport = t
}
}
if transport == nil {
return c.newClientLocked()
}
numStreams := int(transport.count.Load())
if numStreams == 0 {
return transport, nil
}
if c.maxConnections > 0 {
if len(c.clients) >= c.maxConnections || numStreams < c.minStreams {
return transport, nil
}
} else if c.maxStreams > 0 && numStreams < c.maxStreams {
return transport, nil
}
return c.newClientLocked()
}
func (c *MultiplexClient) newClientLocked() (*Client, error) {
t, err := NewClient(c.ctx, c.options)
if err != nil {
return nil, err
}
c.clients = append(c.clients, t)
return t, nil
}

View File

@@ -0,0 +1,62 @@
package trusttunnel
import (
"encoding/binary"
"net/netip"
"github.com/sagernet/sing/common/buf"
)
type IcmpConn struct {
httpConn
}
func (i *IcmpConn) WritePing(id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16) error {
request := buf.NewSize(2 + 16 + 2 + 1 + 2)
defer request.Release()
must(binary.Write(request, binary.BigEndian, id))
destinationAddress := buildPaddingIP(destination)
must1(request.Write(destinationAddress[:]))
must(binary.Write(request, binary.BigEndian, sequenceNumber))
must(binary.Write(request, binary.BigEndian, ttl))
must(binary.Write(request, binary.BigEndian, size))
_, err := i.writeFlush(request.Bytes())
return err
}
func (i *IcmpConn) ReadPing() (id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16, err error) {
err = i.waitCreated()
if err != nil {
return
}
response := buf.NewSize(2 + 16 + 1 + 1 + 2)
defer response.Release()
_, err = response.ReadFullFrom(i.body, response.FreeLen())
if err != nil {
return
}
must(binary.Read(response, binary.BigEndian, &id))
var sourceAddressBuffer [16]byte
must1(response.Read(sourceAddressBuffer[:]))
sourceAddress = parse16BytesIP(sourceAddressBuffer)
must(binary.Read(response, binary.BigEndian, &icmpType))
must(binary.Read(response, binary.BigEndian, &code))
must(binary.Read(response, binary.BigEndian, &sequenceNumber))
return
}
func (i *IcmpConn) Close() error {
return i.httpConn.Close()
}
func must(err error) {
if err != nil {
panic(err)
}
}
func must1[T any](_ T, err error) {
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,210 @@
package trusttunnel
import (
"encoding/binary"
"math"
"net"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
)
var (
_ N.NetPacketConn = (*clientPacketConn)(nil)
_ N.FrontHeadroom = (*clientPacketConn)(nil)
)
type clientPacketConn struct {
httpConn
}
func (u *clientPacketConn) FrontHeadroom() int {
return 4 + 16 + 2 + 16 + 2 + 1 + math.MaxUint8
}
func (u *clientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
err = u.waitCreated()
if err != nil {
return M.Socksaddr{}, err
}
return u.readPacketFromServer(buffer)
}
func (u *clientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
buffer := buf.With(p)
destination, err := u.ReadPacket(buffer)
if err != nil {
return 0, nil, err
}
return buffer.Len(), destination.UDPAddr(), nil
}
func (u *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return u.writePacketToServer(buffer, destination)
}
func (u *clientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
if err != nil {
return 0, err
}
return len(p), nil
}
func (u *clientPacketConn) readPacketFromServer(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
header := buf.NewSize(4 + 16 + 2 + 16 + 2)
defer header.Release()
_, err = header.ReadFullFrom(u.body, header.FreeLen())
if err != nil {
return
}
var length uint32
common.Must(binary.Read(header, binary.BigEndian, &length))
var sourceAddressBuffer [16]byte
common.Must1(header.Read(sourceAddressBuffer[:]))
destination.Addr = parse16BytesIP(sourceAddressBuffer)
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
common.Must(rw.SkipN(header, 16+2))
payloadLen := int(length) - (16 + 2 + 16 + 2)
if payloadLen < 0 {
return M.Socksaddr{}, E.New("invalid udp length: ", length)
}
_, err = buffer.ReadFullFrom(u.body, payloadLen)
return
}
func (u *clientPacketConn) writePacketToServer(buffer *buf.Buffer, source M.Socksaddr) error {
defer buffer.Release()
if !source.IsIP() {
return E.New("only support IP")
}
payloadLen := buffer.Len()
headerLen := 4 + 16 + 2 + 16 + 2 + 1 + len(appName)
lengthField := uint32(16 + 2 + 16 + 2 + 1 + len(appName) + payloadLen)
destinationAddress := buildPaddingIP(source.Addr)
header := buf.NewSize(headerLen)
defer header.Release()
common.Must(binary.Write(header, binary.BigEndian, lengthField))
common.Must(header.WriteZeroN(16 + 2))
common.Must1(header.Write(destinationAddress[:]))
common.Must(binary.Write(header, binary.BigEndian, source.Port))
common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName))))
common.Must1(header.WriteString(appName))
_, err := u.writer.Write(header.Bytes())
if err != nil {
return err
}
_, err = u.writer.Write(buffer.Bytes())
if err != nil {
return err
}
if u.flusher != nil {
u.flusher.Flush()
}
return nil
}
var (
_ N.NetPacketConn = (*serverPacketConn)(nil)
_ N.FrontHeadroom = (*serverPacketConn)(nil)
)
type serverPacketConn struct {
httpConn
}
func (u *serverPacketConn) FrontHeadroom() int {
return 4 + 16 + 2 + 16 + 2
}
func (u *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
err = u.waitCreated()
if err != nil {
return M.Socksaddr{}, err
}
return u.readPacketFromClient(buffer)
}
func (u *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
buffer := buf.With(p)
destination, err := u.ReadPacket(buffer)
if err != nil {
return 0, nil, err
}
return buffer.Len(), destination.UDPAddr(), nil
}
func (u *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return u.writePacketToClient(buffer, destination)
}
func (u *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
if err != nil {
return 0, err
}
return len(p), nil
}
func (u *serverPacketConn) readPacketFromClient(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
header := buf.NewSize(4 + 16 + 2 + 16 + 2 + 1)
defer header.Release()
_, err = header.ReadFullFrom(u.body, header.FreeLen())
if err != nil {
return
}
var length uint32
common.Must(binary.Read(header, binary.BigEndian, &length))
common.Must(rw.SkipN(header, 16+2))
var destinationAddressBuffer [16]byte
common.Must1(header.Read(destinationAddressBuffer[:]))
destination.Addr = parse16BytesIP(destinationAddressBuffer)
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
var appNameLen uint8
common.Must(binary.Read(header, binary.BigEndian, &appNameLen))
if appNameLen > 0 {
err = rw.SkipN(u.body, int(appNameLen))
if err != nil {
return M.Socksaddr{}, err
}
}
payloadLen := int(length) - (16 + 2 + 16 + 2 + 1 + int(appNameLen))
if payloadLen < 0 {
return M.Socksaddr{}, E.New("invalid udp length: ", length)
}
_, err = buffer.ReadFullFrom(u.body, payloadLen)
return
}
func (u *serverPacketConn) writePacketToClient(buffer *buf.Buffer, source M.Socksaddr) error {
defer buffer.Release()
if !source.IsIP() {
return E.New("only support IP")
}
payloadLen := buffer.Len()
headerLen := 4 + 16 + 2 + 16 + 2
lengthField := uint32(16 + 2 + 16 + 2 + payloadLen)
sourceAddress := buildPaddingIP(source.Addr)
header := buf.NewSize(headerLen)
defer header.Release()
common.Must(binary.Write(header, binary.BigEndian, lengthField))
common.Must1(header.Write(sourceAddress[:]))
common.Must(binary.Write(header, binary.BigEndian, source.Port))
common.Must(header.WriteZeroN(16 + 2))
_, err := u.writer.Write(header.Bytes())
if err != nil {
return err
}
_, err = u.writer.Write(buffer.Bytes())
if err != nil {
return err
}
if u.flusher != nil {
u.flusher.Flush()
}
return nil
}

View File

@@ -0,0 +1,174 @@
package trusttunnel
import (
"bytes"
"encoding/base64"
"io"
"net"
"net/http"
"net/netip"
"strings"
"sync"
"time"
)
const (
UDPMagicAddress = "_udp2"
ICMPMagicAddress = "_icmp"
HealthCheckMagicAddress = "_check"
DefaultConnectionTimeout = 30 * time.Second
DefaultHealthCheckTimeout = 7 * time.Second
DefaultSessionTimeout = 30 * time.Second
)
func buildAuth(username string, password string) string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
}
func parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return "", "", false
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return "", "", false
}
cs := string(c)
username, password, ok = strings.Cut(cs, ":")
return
}
func parse16BytesIP(buffer [16]byte) netip.Addr {
var zeroPrefix [12]byte
isIPv4 := bytes.HasPrefix(buffer[:], zeroPrefix[:])
isIPv4 = isIPv4 && !(buffer[12] == 0 && buffer[13] == 0 && buffer[14] == 0 && buffer[15] == 1)
if isIPv4 {
return netip.AddrFrom4([4]byte(buffer[12:16]))
}
return netip.AddrFrom16(buffer)
}
func buildPaddingIP(addr netip.Addr) (buffer [16]byte) {
if addr.Is6() {
return addr.As16()
}
ipv4 := addr.As4()
copy(buffer[12:16], ipv4[:])
return buffer
}
type httpConn struct {
writer io.Writer
flusher http.Flusher
body io.ReadCloser
setupOnce sync.Once
created chan struct{}
createErr error
cancelFn func()
closeFn func()
remoteAddr net.Addr
localAddr net.Addr
deadline *time.Timer
done chan struct{}
}
func (h *httpConn) setup(body io.ReadCloser, err error) {
h.setupOnce.Do(func() {
h.body = body
h.createErr = err
close(h.created)
})
if h.createErr != nil && body != nil {
_ = body.Close()
}
}
func (h *httpConn) waitCreated() error {
<-h.created
if h.body != nil {
return nil
}
return h.createErr
}
func (h *httpConn) Close() error {
h.setup(nil, net.ErrClosed)
if closer, ok := h.writer.(io.Closer); ok {
_ = closer.Close()
}
if h.body != nil {
_ = h.body.Close()
}
if h.cancelFn != nil {
h.cancelFn()
}
if h.closeFn != nil {
h.closeFn()
}
if h.done != nil {
select {
case <-h.done:
default:
close(h.done)
}
}
return nil
}
func (h *httpConn) writeFlush(p []byte) (n int, err error) {
n, err = h.writer.Write(p)
if h.flusher != nil {
h.flusher.Flush()
}
return n, err
}
func (h *httpConn) RemoteAddr() net.Addr {
if h.remoteAddr != nil {
return h.remoteAddr
}
return &net.TCPAddr{}
}
func (h *httpConn) LocalAddr() net.Addr {
if h.localAddr != nil {
return h.localAddr
}
return &net.TCPAddr{}
}
func (h *httpConn) SetDeadline(t time.Time) error {
if t.IsZero() {
if h.deadline != nil {
h.deadline.Stop()
h.deadline = nil
}
return nil
}
d := time.Until(t)
if h.deadline != nil {
h.deadline.Reset(d)
return nil
}
h.deadline = time.AfterFunc(d, func() { h.Close() })
return nil
}
func (h *httpConn) SetReadDeadline(t time.Time) error { return h.SetDeadline(t) }
func (h *httpConn) SetWriteDeadline(t time.Time) error { return h.SetDeadline(t) }
var _ net.Conn = (*tcpConn)(nil)
type tcpConn struct{ httpConn }
func (t *tcpConn) Read(b []byte) (n int, err error) {
if err = t.waitCreated(); err != nil {
return 0, err
}
return t.body.Read(b)
}
func (t *tcpConn) Write(b []byte) (int, error) {
return t.writeFlush(b)
}

View File

@@ -0,0 +1,140 @@
package trusttunnel
import (
"context"
"errors"
"net"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/congestion"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
qtls "github.com/sagernet/sing-quic"
"github.com/sagernet/sing-quic/congestion_bbr1"
"github.com/sagernet/sing-quic/congestion_bbr2"
congestion_meta1 "github.com/sagernet/sing-quic/congestion_meta1"
congestion_meta2 "github.com/sagernet/sing-quic/congestion_meta2"
"github.com/sagernet/sing/common/ntp"
)
func NewCongestionControl(name string, cwnd int, bbrProfile string, timeFunc func() time.Time) (func(conn *quic.Conn) congestion.CongestionControl, error) {
if timeFunc == nil {
timeFunc = time.Now
}
if cwnd == 0 {
cwnd = 32
}
switch name {
case "", "bbr":
return func(conn *quic.Conn) congestion.CongestionControl {
return congestion_meta2.NewBbrSender(
congestion_meta2.DefaultClock{TimeFunc: timeFunc},
congestion.ByteCount(conn.Config().InitialPacketSize),
congestion.ByteCount(cwnd)*congestion.ByteCount(conn.Config().InitialPacketSize),
)
}, nil
case "bbr_standard":
return func(conn *quic.Conn) congestion.CongestionControl {
return congestion_bbr1.NewBbrSender(
congestion_bbr1.DefaultClock{TimeFunc: timeFunc},
congestion.ByteCount(conn.Config().InitialPacketSize),
congestion_bbr1.InitialCongestionWindowPackets,
congestion_bbr1.MaxCongestionWindowPackets,
)
}, nil
case "bbr2":
return func(conn *quic.Conn) congestion.CongestionControl {
return congestion_bbr2.NewBBR2Sender(
congestion_bbr2.DefaultClock{TimeFunc: timeFunc},
congestion.ByteCount(conn.Config().InitialPacketSize),
0,
false,
)
}, nil
case "bbr2_variant":
return func(conn *quic.Conn) congestion.CongestionControl {
return congestion_bbr2.NewBBR2Sender(
congestion_bbr2.DefaultClock{TimeFunc: timeFunc},
congestion.ByteCount(conn.Config().InitialPacketSize),
32*congestion.ByteCount(conn.Config().InitialPacketSize),
true,
)
}, nil
case "cubic":
return func(conn *quic.Conn) congestion.CongestionControl {
return congestion_meta1.NewCubicSender(
congestion_meta1.DefaultClock{TimeFunc: timeFunc},
congestion.ByteCount(conn.Config().InitialPacketSize),
false,
)
}, nil
case "reno":
return func(conn *quic.Conn) congestion.CongestionControl {
return congestion_meta1.NewCubicSender(
congestion_meta1.DefaultClock{TimeFunc: timeFunc},
congestion.ByteCount(conn.Config().InitialPacketSize),
true,
)
}, nil
default:
return nil, E.New("unknown congestion control: ", name)
}
}
type QUICService struct {
service *Service
h3Server *http3.Server
udpConn net.PacketConn
congestionControl string
cwnd int
bbrProfile string
}
func NewQUICService(service *Service, congestionControl string, cwnd int, bbrProfile string) *QUICService {
return &QUICService{
service: service,
congestionControl: congestionControl,
cwnd: cwnd,
bbrProfile: bbrProfile,
}
}
func (s *QUICService) Start(ctx context.Context, udpConn net.PacketConn, tlsConfig tls.ServerConfig) error {
s.udpConn = udpConn
congestionControlFactory, err := NewCongestionControl(s.congestionControl, s.cwnd, s.bbrProfile, ntp.TimeFuncFromContext(ctx))
if err != nil {
return err
}
s.h3Server = &http3.Server{
Handler: s.service,
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
conn.SetCongestionControl(congestionControlFactory(conn))
return ctx
},
}
quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{
MaxIdleTimeout: DefaultSessionTimeout * 2,
MaxIncomingStreams: 1 << 60,
Allow0RTT: true,
})
if err != nil {
return err
}
go func() {
_ = s.h3Server.ServeListener(quicListener)
}()
return nil
}
func (s *QUICService) Close() error {
var errs []error
if s.h3Server != nil {
errs = append(errs, s.h3Server.Close())
}
if s.udpConn != nil {
errs = append(errs, s.udpConn.Close())
}
return errors.Join(errs...)
}

View File

@@ -0,0 +1,218 @@
package trusttunnel
import (
"context"
"io"
"net"
"net/http"
"sync"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type Handler interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
}
type ServiceOptions struct {
Ctx context.Context
Logger logger.ContextLogger
Handler Handler
}
type Service struct {
ctx context.Context
logger logger.ContextLogger
users map[string]string
handler Handler
conns map[string][]io.Closer
mu sync.RWMutex
}
func NewService(options ServiceOptions) *Service {
return &Service{
ctx: options.Ctx,
logger: options.Logger,
handler: options.Handler,
conns: make(map[string][]io.Closer),
}
}
func (s *Service) UpdateUsers(users map[string]string) {
s.mu.Lock()
s.users = users
var closedConns []io.Closer
for user, conns := range s.conns {
if _, exists := users[user]; !exists {
closedConns = append(closedConns, conns...)
delete(s.conns, user)
}
}
s.mu.Unlock()
for _, conn := range closedConns {
conn.Close()
}
}
func (s *Service) trackConn(username string, conn io.Closer) {
s.mu.Lock()
s.conns[username] = append(s.conns[username], conn)
s.mu.Unlock()
}
func (s *Service) untrackConn(username string, conn io.Closer) {
s.mu.Lock()
conns := s.conns[username]
for i, c := range conns {
if c == conn {
s.conns[username] = append(conns[:i], conns[i+1:]...)
break
}
}
s.mu.Unlock()
}
func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
authorization := request.Header.Get("Proxy-Authorization")
username, loaded := s.verify(authorization)
if !loaded {
writer.WriteHeader(http.StatusProxyAuthRequired)
s.badRequest(request.Context(), request, E.New("authorization failed"))
return
}
if request.Method != http.MethodConnect {
writer.WriteHeader(http.StatusMethodNotAllowed)
s.badRequest(request.Context(), request, E.New("unexpected HTTP method ", request.Method))
return
}
ctx := request.Context()
ctx = auth.ContextWithUser(ctx, username)
switch request.Host {
case UDPMagicAddress:
writer.WriteHeader(http.StatusOK)
flusher, isFlusher := writer.(http.Flusher)
if isFlusher {
flusher.Flush()
}
done := make(chan struct{})
conn := &serverPacketConn{
httpConn: httpConn{
writer: writer,
flusher: flusher,
created: make(chan struct{}),
done: done,
remoteAddr: parseRemoteAddr(request.RemoteAddr),
},
}
conn.setup(request.Body, nil)
firstPacket := buf.NewPacket()
destination, err := conn.ReadPacket(firstPacket)
if err != nil {
firstPacket.Release()
_ = conn.Close()
s.logger.ErrorContext(ctx, E.Cause(err, "read first packet from ", request.RemoteAddr))
return
}
destination = destination.Unwrap()
cachedConn := bufio.NewCachedPacketConn(conn, firstPacket, destination)
s.trackConn(username, conn)
_ = s.handler.NewPacketConnection(ctx, cachedConn, M.Metadata{
Protocol: "trusttunnel",
Source: M.ParseSocksaddr(request.RemoteAddr),
Destination: destination,
})
<-done
s.untrackConn(username, conn)
case HealthCheckMagicAddress:
writer.WriteHeader(http.StatusOK)
if flusher, isFlusher := writer.(http.Flusher); isFlusher {
flusher.Flush()
}
_ = request.Body.Close()
default:
writer.WriteHeader(http.StatusOK)
flusher, isFlusher := writer.(http.Flusher)
if isFlusher {
flusher.Flush()
}
done := make(chan struct{})
conn := &tcpConn{
httpConn{
writer: writer,
flusher: flusher,
created: make(chan struct{}),
done: done,
remoteAddr: parseRemoteAddr(request.RemoteAddr),
},
}
conn.setup(request.Body, nil)
wrapper := &h2ConnWrapper{Conn: conn}
s.trackConn(username, wrapper)
_ = s.handler.NewConnection(ctx, wrapper, M.Metadata{
Protocol: "trusttunnel",
Source: M.ParseSocksaddr(request.RemoteAddr),
Destination: M.ParseSocksaddr(request.Host).Unwrap(),
})
<-done
s.untrackConn(username, wrapper)
wrapper.CloseWrapper()
}
}
func (s *Service) verify(authorization string) (username string, loaded bool) {
username, password, loaded := parseBasicAuth(authorization)
if !loaded {
return "", false
}
s.mu.RLock()
recordedPassword, loaded := s.users[username]
s.mu.RUnlock()
if !loaded {
return "", false
}
if password != recordedPassword {
return "", false
}
return username, true
}
func (s *Service) badRequest(ctx context.Context, request *http.Request, err error) {
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
}
func parseRemoteAddr(addr string) net.Addr {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil
}
return tcpAddr
}
type h2ConnWrapper struct {
net.Conn
access sync.Mutex
closed bool
}
func (w *h2ConnWrapper) Write(p []byte) (n int, err error) {
w.access.Lock()
defer w.access.Unlock()
if w.closed {
return 0, net.ErrClosed
}
return w.Conn.Write(p)
}
func (w *h2ConnWrapper) CloseWrapper() {
w.access.Lock()
defer w.access.Unlock()
w.closed = true
}