mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-05 21:35:15 +03:00
Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user