mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-30 02:26:46 +03:00
Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes
This commit is contained in:
@@ -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}
|
||||
}
|
||||
@@ -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
227
transport/openvpn/cipher.go
Normal 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
280
transport/openvpn/client.go
Normal 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
175
transport/openvpn/config.go
Normal 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)
|
||||
}
|
||||
475
transport/openvpn/control.go
Normal file
475
transport/openvpn/control.go
Normal 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
91
transport/openvpn/data.go
Normal 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
|
||||
}
|
||||
33
transport/openvpn/device.go
Normal file
33
transport/openvpn/device.go
Normal 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)
|
||||
}
|
||||
309
transport/openvpn/device_stack.go
Normal file
309
transport/openvpn/device_stack.go
Normal 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()) {
|
||||
}
|
||||
13
transport/openvpn/device_stack_stub.go
Normal file
13
transport/openvpn/device_stack_stub.go
Normal 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
|
||||
}
|
||||
250
transport/openvpn/keymethod.go
Normal file
250
transport/openvpn/keymethod.go
Normal 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
92
transport/openvpn/mux.go
Normal 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
254
transport/openvpn/packet.go
Normal 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
139
transport/openvpn/push.go
Normal 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
|
||||
}
|
||||
100
transport/openvpn/tlsauth.go
Normal file
100
transport/openvpn/tlsauth.go
Normal 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
|
||||
}
|
||||
128
transport/openvpn/tlscrypt.go
Normal file
128
transport/openvpn/tlscrypt.go
Normal 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
284
transport/openvpn/tunnel.go
Normal 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
100
transport/sudoku/address.go
Normal 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
212
transport/sudoku/config.go
Normal 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
|
||||
}
|
||||
150
transport/sudoku/crypto/aead.go
Normal file
150
transport/sudoku/crypto/aead.go
Normal 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)
|
||||
}
|
||||
116
transport/sudoku/crypto/ed25519.go
Normal file
116
transport/sudoku/crypto/ed25519.go
Normal 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())
|
||||
}
|
||||
452
transport/sudoku/crypto/record_conn.go
Normal file
452
transport/sudoku/crypto/record_conn.go
Normal 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
|
||||
}
|
||||
343
transport/sudoku/early_handshake.go
Normal file
343
transport/sudoku/early_handshake.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
511
transport/sudoku/handshake.go
Normal file
511
transport/sudoku/handshake.go
Normal 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)
|
||||
}
|
||||
}
|
||||
67
transport/sudoku/handshake_kip.go
Normal file
67
transport/sudoku/handshake_kip.go
Normal 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
|
||||
}
|
||||
155
transport/sudoku/httpmask_tunnel.go
Normal file
155
transport/sudoku/httpmask_tunnel.go
Normal 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
101
transport/sudoku/init.go
Normal 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
259
transport/sudoku/kip.go
Normal 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
|
||||
}
|
||||
130
transport/sudoku/multiplex.go
Normal file
130
transport/sudoku/multiplex.go
Normal 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()
|
||||
}
|
||||
493
transport/sudoku/multiplex/session.go
Normal file
493
transport/sudoku/multiplex/session.go
Normal 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 }
|
||||
19
transport/sudoku/multiplex/write_chunks.go
Normal file
19
transport/sudoku/multiplex/write_chunks.go
Normal 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
|
||||
}
|
||||
161
transport/sudoku/obfs/httpmask/auth.go
Normal file
161
transport/sudoku/obfs/httpmask/auth.go
Normal 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()
|
||||
}
|
||||
}
|
||||
174
transport/sudoku/obfs/httpmask/early_handshake.go
Normal file
174
transport/sudoku/obfs/httpmask/early_handshake.go
Normal 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
|
||||
}
|
||||
229
transport/sudoku/obfs/httpmask/halfpipe.go
Normal file
229
transport/sudoku/obfs/httpmask/halfpipe.go
Normal 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
|
||||
}
|
||||
252
transport/sudoku/obfs/httpmask/masker.go
Normal file
252
transport/sudoku/obfs/httpmask/masker.go
Normal 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: 4KB–10MB. 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
|
||||
}
|
||||
}
|
||||
}
|
||||
52
transport/sudoku/obfs/httpmask/pathroot.go
Normal file
52
transport/sudoku/obfs/httpmask/pathroot.go
Normal 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
|
||||
}
|
||||
2442
transport/sudoku/obfs/httpmask/tunnel.go
Normal file
2442
transport/sudoku/obfs/httpmask/tunnel.go
Normal file
File diff suppressed because it is too large
Load Diff
190
transport/sudoku/obfs/httpmask/tunnel_ws.go
Normal file
190
transport/sudoku/obfs/httpmask/tunnel_ws.go
Normal 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
|
||||
}
|
||||
109
transport/sudoku/obfs/httpmask/tunnel_ws_server.go
Normal file
109
transport/sudoku/obfs/httpmask/tunnel_ws_server.go
Normal 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
|
||||
}
|
||||
78
transport/sudoku/obfs/httpmask/ws_stream_conn.go
Normal file
78
transport/sudoku/obfs/httpmask/ws_stream_conn.go
Normal 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()
|
||||
}
|
||||
93
transport/sudoku/obfs/sudoku/ascii_mode.go
Normal file
93
transport/sudoku/obfs/sudoku/ascii_mode.go
Normal 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"
|
||||
}
|
||||
193
transport/sudoku/obfs/sudoku/conn.go
Normal file
193
transport/sudoku/obfs/sudoku/conn.go
Normal 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
|
||||
}
|
||||
36
transport/sudoku/obfs/sudoku/encode.go
Normal file
36
transport/sudoku/obfs/sudoku/encode.go
Normal 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
|
||||
}
|
||||
46
transport/sudoku/obfs/sudoku/grid.go
Normal file
46
transport/sudoku/obfs/sudoku/grid.go
Normal 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
|
||||
}
|
||||
255
transport/sudoku/obfs/sudoku/layout.go
Normal file
255
transport/sudoku/obfs/sudoku/layout.go
Normal 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
|
||||
}
|
||||
359
transport/sudoku/obfs/sudoku/packed.go
Normal file
359
transport/sudoku/obfs/sudoku/packed.go
Normal 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))]
|
||||
}
|
||||
42
transport/sudoku/obfs/sudoku/padding_prob.go
Normal file
42
transport/sudoku/obfs/sudoku/padding_prob.go
Normal 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
|
||||
}
|
||||
57
transport/sudoku/obfs/sudoku/pending.go
Normal file
57
transport/sudoku/obfs/sudoku/pending.go
Normal 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
|
||||
}
|
||||
56
transport/sudoku/obfs/sudoku/rand.go
Normal file
56
transport/sudoku/obfs/sudoku/rand.go
Normal 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)
|
||||
}
|
||||
214
transport/sudoku/obfs/sudoku/table.go
Normal file
214
transport/sudoku/obfs/sudoku/table.go
Normal 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])
|
||||
}
|
||||
38
transport/sudoku/obfs/sudoku/table_set.go
Normal file
38
transport/sudoku/obfs/sudoku/table_set.go
Normal 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
|
||||
}
|
||||
74
transport/sudoku/replay.go
Normal file
74
transport/sudoku/replay.go
Normal 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{}
|
||||
58
transport/sudoku/session_keys.go
Normal file
58
transport/sudoku/session_keys.go
Normal 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
|
||||
}
|
||||
164
transport/sudoku/table_probe.go
Normal file
164
transport/sudoku/table_probe.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
68
transport/sudoku/tables.go
Normal file
68
transport/sudoku/tables.go
Normal 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
165
transport/sudoku/uot.go
Normal 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
|
||||
}
|
||||
19
transport/sudoku/write_chunks.go
Normal file
19
transport/sudoku/write_chunks.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
323
transport/trusttunnel/client.go
Normal file
323
transport/trusttunnel/client.go
Normal 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
|
||||
}
|
||||
62
transport/trusttunnel/icmp.go
Normal file
62
transport/trusttunnel/icmp.go
Normal 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)
|
||||
}
|
||||
}
|
||||
210
transport/trusttunnel/packet.go
Normal file
210
transport/trusttunnel/packet.go
Normal 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
|
||||
}
|
||||
174
transport/trusttunnel/protocol.go
Normal file
174
transport/trusttunnel/protocol.go
Normal 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)
|
||||
}
|
||||
140
transport/trusttunnel/quic.go
Normal file
140
transport/trusttunnel/quic.go
Normal 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...)
|
||||
}
|
||||
218
transport/trusttunnel/service.go
Normal file
218
transport/trusttunnel/service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user