mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 22:35:16 +03:00
222 lines
5.0 KiB
Go
222 lines
5.0 KiB
Go
package tunnel
|
|
|
|
import (
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
var vp8Keepalive = []byte{
|
|
0x30, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x10, 0x00,
|
|
0x10, 0x00, 0x00, 0x47, 0x08, 0x85, 0x85, 0x88,
|
|
0x99, 0x84, 0x88, 0xfc,
|
|
}
|
|
|
|
var vp8Interframe = []byte{
|
|
0xb1, 0x01, 0x00, 0x08, 0x11, 0x18, 0x00, 0x18,
|
|
0x00, 0x18, 0x58, 0x2f, 0xf4, 0x00, 0x08, 0x00,
|
|
0x00,
|
|
}
|
|
|
|
const (
|
|
vp8KeepaliveLen = 20
|
|
vp8InterframeLen = 17
|
|
epochFieldLen = 4
|
|
keepaliveHdrLen = vp8KeepaliveLen + epochFieldLen
|
|
interframeHdrLen = vp8InterframeLen + epochFieldLen
|
|
)
|
|
|
|
var ErrEmptySecret = errors.New("tunnel: obfuscator requires a non-empty secret")
|
|
|
|
type DecodeResult struct {
|
|
HasFrame bool
|
|
Keepalive bool
|
|
SelfEcho bool
|
|
PeerRestart bool
|
|
Payload []byte
|
|
PeerEpoch uint32
|
|
}
|
|
|
|
type TunnelObfuscator struct {
|
|
aead cipher.AEAD
|
|
localEpoch uint32
|
|
|
|
mu sync.Mutex
|
|
peerEpoch uint32
|
|
hasPeer bool
|
|
}
|
|
|
|
func DeriveSecretFromJoinLink(joinLink string) []byte {
|
|
token := extractJoinToken(joinLink)
|
|
if token == "" {
|
|
return nil
|
|
}
|
|
return []byte(token)
|
|
}
|
|
|
|
func NewTunnelObfuscator(secret []byte) (*TunnelObfuscator, error) {
|
|
if len(secret) == 0 {
|
|
return nil, ErrEmptySecret
|
|
}
|
|
keyHash := sha256.Sum256(secret)
|
|
aead, err := chacha20poly1305.NewX(keyHash[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var epochBytes [4]byte
|
|
if _, err := rand.Read(epochBytes[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
epoch := binary.BigEndian.Uint32(epochBytes[:])
|
|
if epoch == 0 {
|
|
epoch = 1
|
|
}
|
|
return &TunnelObfuscator{aead: aead, localEpoch: epoch}, nil
|
|
}
|
|
|
|
func (o *TunnelObfuscator) LocalEpoch() uint32 { return o.localEpoch }
|
|
|
|
func (o *TunnelObfuscator) EncodeKeepalive(padLen int) []byte {
|
|
hdr := o.keepaliveHeader()
|
|
if padLen <= 0 {
|
|
return hdr
|
|
}
|
|
out := make([]byte, keepaliveHdrLen+padLen)
|
|
copy(out, hdr)
|
|
if _, err := rand.Read(out[keepaliveHdrLen:]); err != nil {
|
|
return hdr
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (o *TunnelObfuscator) EncodeData(payload []byte) []byte {
|
|
hdr := o.dataHeader()
|
|
nonce := make([]byte, o.aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil
|
|
}
|
|
out := make([]byte, 0, len(hdr)+len(nonce)+len(payload)+o.aead.Overhead())
|
|
out = append(out, hdr...)
|
|
out = append(out, nonce...)
|
|
out = o.aead.Seal(out, nonce, payload, nil)
|
|
return out
|
|
}
|
|
|
|
func (o *TunnelObfuscator) EncryptPayload(plaintext []byte) []byte {
|
|
if o == nil {
|
|
return plaintext
|
|
}
|
|
nonce := make([]byte, o.aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil
|
|
}
|
|
out := make([]byte, 0, len(nonce)+len(plaintext)+o.aead.Overhead())
|
|
out = append(out, nonce...)
|
|
return o.aead.Seal(out, nonce, plaintext, nil)
|
|
}
|
|
|
|
func (o *TunnelObfuscator) DecryptPayload(data []byte) ([]byte, bool) {
|
|
if o == nil {
|
|
return data, true
|
|
}
|
|
nonceSize := o.aead.NonceSize()
|
|
if len(data) < nonceSize+o.aead.Overhead() {
|
|
return nil, false
|
|
}
|
|
nonce := data[:nonceSize]
|
|
ciphertext := data[nonceSize:]
|
|
plaintext, err := o.aead.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return plaintext, true
|
|
}
|
|
|
|
func (o *TunnelObfuscator) Decode(frame []byte) DecodeResult {
|
|
if len(frame) < 1 {
|
|
return DecodeResult{}
|
|
}
|
|
var hdrLen, epochOff int
|
|
isKeepaliveFrame := false
|
|
switch frame[0] {
|
|
case vp8Keepalive[0]:
|
|
hdrLen = keepaliveHdrLen
|
|
epochOff = vp8KeepaliveLen
|
|
isKeepaliveFrame = true
|
|
case vp8Interframe[0]:
|
|
hdrLen = interframeHdrLen
|
|
epochOff = vp8InterframeLen
|
|
default:
|
|
return DecodeResult{}
|
|
}
|
|
if len(frame) < hdrLen {
|
|
return DecodeResult{}
|
|
}
|
|
peerEpoch := binary.BigEndian.Uint32(frame[epochOff : epochOff+epochFieldLen])
|
|
if peerEpoch == o.localEpoch {
|
|
return DecodeResult{HasFrame: true, SelfEcho: true, PeerEpoch: peerEpoch}
|
|
}
|
|
res := DecodeResult{HasFrame: true, PeerEpoch: peerEpoch}
|
|
o.mu.Lock()
|
|
if !o.hasPeer {
|
|
o.peerEpoch = peerEpoch
|
|
o.hasPeer = true
|
|
} else if o.peerEpoch != peerEpoch {
|
|
o.peerEpoch = peerEpoch
|
|
res.PeerRestart = true
|
|
}
|
|
o.mu.Unlock()
|
|
if isKeepaliveFrame || len(frame) == hdrLen {
|
|
res.Keepalive = true
|
|
return res
|
|
}
|
|
body := frame[hdrLen:]
|
|
nonceSize := o.aead.NonceSize()
|
|
if len(body) < nonceSize+o.aead.Overhead() {
|
|
return DecodeResult{}
|
|
}
|
|
nonce := body[:nonceSize]
|
|
ciphertext := body[nonceSize:]
|
|
plaintext, err := o.aead.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return DecodeResult{}
|
|
}
|
|
res.Payload = plaintext
|
|
return res
|
|
}
|
|
|
|
func extractJoinToken(joinLink string) string {
|
|
s := strings.TrimSpace(joinLink)
|
|
s = strings.TrimRight(s, "/")
|
|
if i := strings.IndexByte(s, '?'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
if i := strings.IndexByte(s, '#'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
if i := strings.LastIndexByte(s, '/'); i >= 0 {
|
|
s = s[i+1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (o *TunnelObfuscator) keepaliveHeader() []byte {
|
|
hdr := make([]byte, keepaliveHdrLen)
|
|
copy(hdr, vp8Keepalive)
|
|
binary.BigEndian.PutUint32(hdr[vp8KeepaliveLen:], o.localEpoch)
|
|
return hdr
|
|
}
|
|
|
|
func (o *TunnelObfuscator) dataHeader() []byte {
|
|
hdr := make([]byte, interframeHdrLen)
|
|
copy(hdr, vp8Interframe)
|
|
binary.BigEndian.PutUint32(hdr[vp8InterframeLen:], o.localEpoch)
|
|
return hdr
|
|
}
|