mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-03 12:17:31 +00:00
Compare commits
17 Commits
v1.19.18
...
dev-conn-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c4110b57 | ||
|
|
1f8bee9710 | ||
|
|
eb30d3f331 | ||
|
|
10f4bebdfa | ||
|
|
06387d5045 | ||
|
|
c393e917eb | ||
|
|
4f0a6fa117 | ||
|
|
4f9bfd216f | ||
|
|
498f81aad3 | ||
|
|
9168bee6b7 | ||
|
|
e6c0e3b19c | ||
|
|
287f9e5185 | ||
|
|
c456370f4f | ||
|
|
10ef29f5cd | ||
|
|
85ba7f6a0a | ||
|
|
7daf37bc15 | ||
|
|
64015b7634 |
@@ -8,6 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/component/keepalive"
|
"github.com/metacubex/mihomo/component/keepalive"
|
||||||
|
"github.com/metacubex/mihomo/component/mptcp"
|
||||||
|
|
||||||
"github.com/metacubex/tfo-go"
|
"github.com/metacubex/tfo-go"
|
||||||
)
|
)
|
||||||
@@ -34,13 +35,13 @@ func Tfo() bool {
|
|||||||
func SetMPTCP(open bool) {
|
func SetMPTCP(open bool) {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
setMultiPathTCP(&lc.ListenConfig, open)
|
mptcp.SetNetListenConfig(&lc.ListenConfig, open)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MPTCP() bool {
|
func MPTCP() bool {
|
||||||
mutex.RLock()
|
mutex.RLock()
|
||||||
defer mutex.RUnlock()
|
defer mutex.RUnlock()
|
||||||
return getMultiPathTCP(&lc.ListenConfig)
|
return mptcp.GetNetListenConfig(&lc.ListenConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func preResolve(network, address string) (string, error) {
|
func preResolve(network, address string) (string, error) {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
//go:build !go1.21
|
|
||||||
|
|
||||||
package inbound
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
const multipathTCPAvailable = false
|
|
||||||
|
|
||||||
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//go:build go1.21
|
|
||||||
|
|
||||||
package inbound
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
const multipathTCPAvailable = true
|
|
||||||
|
|
||||||
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
|
|
||||||
listenConfig.SetMultipathTCP(open)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
|
|
||||||
return listenConfig.MultipathTCP()
|
|
||||||
}
|
|
||||||
@@ -34,7 +34,10 @@ func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
|||||||
if err := r.ResolveUDP(ctx, metadata); err != nil {
|
if err := r.ResolveUDP(ctx, metadata); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return newPacketConn(&nopPacketConn{}, r), nil
|
if r.drop {
|
||||||
|
return newPacketConn(dropPacketConn{}, r), nil
|
||||||
|
}
|
||||||
|
return newPacketConn(nopPacketConn{}, r), C.ErrResetByRule
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||||
@@ -90,12 +93,10 @@ func NewPass() *Reject {
|
|||||||
|
|
||||||
type nopConn struct{}
|
type nopConn struct{}
|
||||||
|
|
||||||
func (rw nopConn) Read(b []byte) (int, error) { return 0, io.EOF }
|
func (rw nopConn) Read(b []byte) (int, error) { return 0, C.ErrResetByRule }
|
||||||
|
func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { return C.ErrResetByRule }
|
||||||
func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { return io.EOF }
|
func (rw nopConn) Write(b []byte) (int, error) { return 0, C.ErrResetByRule }
|
||||||
|
func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error { return C.ErrResetByRule }
|
||||||
func (rw nopConn) Write(b []byte) (int, error) { return 0, io.EOF }
|
|
||||||
func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error { return io.EOF }
|
|
||||||
func (rw nopConn) Close() error { return nil }
|
func (rw nopConn) Close() error { return nil }
|
||||||
func (rw nopConn) LocalAddr() net.Addr { return nil }
|
func (rw nopConn) LocalAddr() net.Addr { return nil }
|
||||||
func (rw nopConn) RemoteAddr() net.Addr { return nil }
|
func (rw nopConn) RemoteAddr() net.Addr { return nil }
|
||||||
@@ -110,11 +111,9 @@ type nopPacketConn struct{}
|
|||||||
func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||||
return len(b), nil
|
return len(b), nil
|
||||||
}
|
}
|
||||||
func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, C.ErrResetByRule }
|
||||||
return 0, nil, io.EOF
|
|
||||||
}
|
|
||||||
func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
|
func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
|
||||||
return nil, nil, nil, io.EOF
|
return nil, nil, nil, C.ErrResetByRule
|
||||||
}
|
}
|
||||||
func (npc nopPacketConn) Close() error { return nil }
|
func (npc nopPacketConn) Close() error { return nil }
|
||||||
func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified }
|
func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified }
|
||||||
@@ -137,3 +136,18 @@ func (rw dropConn) RemoteAddr() net.Addr { return nil }
|
|||||||
func (rw dropConn) SetDeadline(time.Time) error { return nil }
|
func (rw dropConn) SetDeadline(time.Time) error { return nil }
|
||||||
func (rw dropConn) SetReadDeadline(time.Time) error { return nil }
|
func (rw dropConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
func (rw dropConn) SetWriteDeadline(time.Time) error { return nil }
|
func (rw dropConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
type dropPacketConn struct{}
|
||||||
|
|
||||||
|
func (npc dropPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
func (npc dropPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
|
||||||
|
func (npc dropPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
|
||||||
|
return nil, nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
func (npc dropPacketConn) Close() error { return nil }
|
||||||
|
func (npc dropPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified }
|
||||||
|
func (npc dropPacketConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (npc dropPacketConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (npc dropPacketConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ type SudokuOption struct {
|
|||||||
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
||||||
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
|
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
|
||||||
HTTPMask bool `proxy:"http-mask,omitempty"`
|
HTTPMask bool `proxy:"http-mask,omitempty"`
|
||||||
|
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
||||||
|
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
|
||||||
|
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
|
||||||
HTTPMaskStrategy string `proxy:"http-mask-strategy,omitempty"` // "random" (default), "post", "websocket"
|
HTTPMaskStrategy string `proxy:"http-mask-strategy,omitempty"` // "random" (default), "post", "websocket"
|
||||||
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||||
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
|
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
|
||||||
@@ -42,7 +45,16 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
|
var c net.Conn
|
||||||
|
if !cfg.DisableHTTPMask {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||||
|
case "stream", "poll", "auto":
|
||||||
|
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c == nil && err == nil {
|
||||||
|
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||||
}
|
}
|
||||||
@@ -56,9 +68,14 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
|||||||
defer done(&err)
|
defer done(&err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
|
handshakeCfg := *cfg
|
||||||
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
|
if !handshakeCfg.DisableHTTPMask {
|
||||||
})
|
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
|
||||||
|
case "stream", "poll", "auto":
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -87,7 +104,16 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
|
var c net.Conn
|
||||||
|
if !cfg.DisableHTTPMask {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||||
|
case "stream", "poll", "auto":
|
||||||
|
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c == nil && err == nil {
|
||||||
|
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||||
}
|
}
|
||||||
@@ -101,9 +127,14 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
|||||||
defer done(&err)
|
defer done(&err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
|
handshakeCfg := *cfg
|
||||||
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
|
if !handshakeCfg.DisableHTTPMask {
|
||||||
})
|
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
|
||||||
|
case "stream", "poll", "auto":
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -190,6 +221,12 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
|||||||
EnablePureDownlink: enablePureDownlink,
|
EnablePureDownlink: enablePureDownlink,
|
||||||
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
||||||
DisableHTTPMask: !option.HTTPMask,
|
DisableHTTPMask: !option.HTTPMask,
|
||||||
|
HTTPMaskMode: defaultConf.HTTPMaskMode,
|
||||||
|
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
|
||||||
|
HTTPMaskHost: option.HTTPMaskHost,
|
||||||
|
}
|
||||||
|
if option.HTTPMaskMode != "" {
|
||||||
|
baseConf.HTTPMaskMode = option.HTTPMaskMode
|
||||||
}
|
}
|
||||||
tables, err := sudoku.NewTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
|
tables, err := sudoku.NewTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ type WriterWithUpstream = network.WriterWithUpstream
|
|||||||
type WithUpstreamWriter = network.WithUpstreamWriter
|
type WithUpstreamWriter = network.WithUpstreamWriter
|
||||||
type WithUpstream = common.WithUpstream
|
type WithUpstream = common.WithUpstream
|
||||||
|
|
||||||
|
type HandshakeSuccess = network.HandshakeSuccess
|
||||||
|
type HandshakeFailure = network.HandshakeFailure
|
||||||
|
|
||||||
var UnwrapReader = network.UnwrapReader
|
var UnwrapReader = network.UnwrapReader
|
||||||
var UnwrapWriter = network.UnwrapWriter
|
var UnwrapWriter = network.UnwrapWriter
|
||||||
|
|
||||||
|
|||||||
31
common/utils/cast.go
Normal file
31
common/utils/cast.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WithUpstream interface {
|
||||||
|
Upstream() any
|
||||||
|
}
|
||||||
|
|
||||||
|
type stdWithUpstreamNetConn interface {
|
||||||
|
NetConn() net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cast[T any](obj any) (_ T, _ bool) {
|
||||||
|
if c, ok := obj.(T); ok {
|
||||||
|
fmt.Printf("Got 1: %T\n", obj) // TODO
|
||||||
|
return c, true
|
||||||
|
}
|
||||||
|
if u, ok := obj.(WithUpstream); ok {
|
||||||
|
fmt.Printf("Upstream 2: %T\n", obj) // TODO
|
||||||
|
return Cast[T](u.Upstream())
|
||||||
|
}
|
||||||
|
if u, ok := obj.(stdWithUpstreamNetConn); ok {
|
||||||
|
fmt.Printf("Std 3: %T\n", obj) // TODO
|
||||||
|
return Cast[T](u.NetConn())
|
||||||
|
}
|
||||||
|
fmt.Printf("Failed: %T\n", obj) // TODO
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ func NewTLSKeyPairLoader(certificate, privateKey string) (func() (*tls.Certifica
|
|||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
||||||
}
|
}
|
||||||
gcFlag := new(os.File)
|
gcFlag := new(os.File) // tiny (on the order of 16 bytes or less) and pointer-free objects may never run the finalizer, so we choose new an os.File
|
||||||
updateMutex := sync.RWMutex{}
|
updateMutex := sync.RWMutex{}
|
||||||
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) {
|
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) {
|
||||||
updateMutex.Lock()
|
updateMutex.Lock()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/component/keepalive"
|
"github.com/metacubex/mihomo/component/keepalive"
|
||||||
|
"github.com/metacubex/mihomo/component/mptcp"
|
||||||
"github.com/metacubex/mihomo/component/resolver"
|
"github.com/metacubex/mihomo/component/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,9 +141,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
|||||||
|
|
||||||
dialer := netDialer.(*net.Dialer)
|
dialer := netDialer.(*net.Dialer)
|
||||||
keepalive.SetNetDialer(dialer)
|
keepalive.SetNetDialer(dialer)
|
||||||
if opt.mpTcp {
|
mptcp.SetNetDialer(dialer, opt.mpTcp)
|
||||||
setMultiPathTCP(dialer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
||||||
socketHookToToDialer(dialer)
|
socketHookToToDialer(dialer)
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !go1.21
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
const multipathTCPAvailable = false
|
|
||||||
|
|
||||||
func setMultiPathTCP(dialer *net.Dialer) {
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
//go:build go1.21
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
const multipathTCPAvailable = true
|
|
||||||
|
|
||||||
func setMultiPathTCP(dialer *net.Dialer) {
|
|
||||||
dialer.SetMultipathTCP(true)
|
|
||||||
}
|
|
||||||
@@ -132,7 +132,7 @@ func LoadECHKey(key string, tlsConfig *tls.Config) error {
|
|||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
||||||
}
|
}
|
||||||
gcFlag := new(os.File)
|
gcFlag := new(os.File) // tiny (on the order of 16 bytes or less) and pointer-free objects may never run the finalizer, so we choose new an os.File
|
||||||
updateMutex := sync.RWMutex{}
|
updateMutex := sync.RWMutex{}
|
||||||
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) {
|
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) {
|
||||||
updateMutex.Lock()
|
updateMutex.Lock()
|
||||||
|
|||||||
@@ -4,13 +4,29 @@ import (
|
|||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UseFakeIP = "fake-ip"
|
||||||
|
UseRealIP = "real-ip"
|
||||||
|
)
|
||||||
|
|
||||||
type Skipper struct {
|
type Skipper struct {
|
||||||
|
Rules []C.Rule
|
||||||
Host []C.DomainMatcher
|
Host []C.DomainMatcher
|
||||||
Mode C.FilterMode
|
Mode C.FilterMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldSkipped return if domain should be skipped
|
// ShouldSkipped return if domain should be skipped
|
||||||
func (p *Skipper) ShouldSkipped(domain string) bool {
|
func (p *Skipper) ShouldSkipped(domain string) bool {
|
||||||
|
if len(p.Rules) > 0 {
|
||||||
|
metadata := &C.Metadata{Host: domain}
|
||||||
|
for _, rule := range p.Rules {
|
||||||
|
if matched, action := rule.Match(metadata, C.RuleMatchHelper{}); matched {
|
||||||
|
return action == UseRealIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
should := p.shouldSkipped(domain)
|
should := p.shouldSkipped(domain)
|
||||||
if p.Mode == C.FilterWhiteList {
|
if p.Mode == C.FilterWhiteList {
|
||||||
return !should
|
return !should
|
||||||
|
|||||||
23
component/mptcp/mptcp_go120.go
Normal file
23
component/mptcp/mptcp_go120.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//go:build !go1.21
|
||||||
|
|
||||||
|
package mptcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MultipathTCPAvailable = false
|
||||||
|
|
||||||
|
func SetNetDialer(dialer *net.Dialer, open bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNetDialer(dialer *net.Dialer) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetNetListenConfig(listenConfig *net.ListenConfig, open bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNetListenConfig(listenConfig *net.ListenConfig) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
23
component/mptcp/mptcp_go121.go
Normal file
23
component/mptcp/mptcp_go121.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//go:build go1.21
|
||||||
|
|
||||||
|
package mptcp
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
const MultipathTCPAvailable = true
|
||||||
|
|
||||||
|
func SetNetDialer(dialer *net.Dialer, open bool) {
|
||||||
|
dialer.SetMultipathTCP(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNetDialer(dialer *net.Dialer) bool {
|
||||||
|
return dialer.MultipathTCP()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetNetListenConfig(listenConfig *net.ListenConfig, open bool) {
|
||||||
|
listenConfig.SetMultipathTCP(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNetListenConfig(listenConfig *net.ListenConfig) bool {
|
||||||
|
return listenConfig.MultipathTCP()
|
||||||
|
}
|
||||||
@@ -1450,16 +1450,22 @@ func parseDNS(rawCfg *RawConfig, ruleProviders map[string]P.RuleProvider) (*DNS,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fake ip skip host filter
|
skipper := &fakeip.Skipper{Mode: cfg.FakeIPFilterMode}
|
||||||
|
|
||||||
|
if cfg.FakeIPFilterMode == C.FilterRule {
|
||||||
|
rules, err := parseFakeIPRules(cfg.FakeIPFilter, ruleProviders)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
skipper.Rules = rules
|
||||||
|
} else {
|
||||||
host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders)
|
host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
skipper.Host = host
|
||||||
skipper := &fakeip.Skipper{
|
|
||||||
Host: host,
|
|
||||||
Mode: cfg.FakeIPFilterMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsCfg.FakeIPSkipper = skipper
|
dnsCfg.FakeIPSkipper = skipper
|
||||||
dnsCfg.FakeIPTTL = cfg.FakeIPTTL
|
dnsCfg.FakeIPTTL = cfg.FakeIPTTL
|
||||||
|
|
||||||
@@ -1541,6 +1547,55 @@ func parseDNS(rawCfg *RawConfig, ruleProviders map[string]P.RuleProvider) (*DNS,
|
|||||||
return dnsCfg, nil
|
return dnsCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseFakeIPRules(rawRules []string, ruleProviders map[string]P.RuleProvider) ([]C.Rule, error) {
|
||||||
|
var rules []C.Rule
|
||||||
|
|
||||||
|
for idx, line := range rawRules {
|
||||||
|
tp, payload, action, params := RC.ParseRulePayload(line, true)
|
||||||
|
|
||||||
|
action = strings.ToLower(action)
|
||||||
|
if action != fakeip.UseFakeIP && action != fakeip.UseRealIP {
|
||||||
|
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: invalid action '%s', must be 'fake-ip' or 'real-ip'", idx, line, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tp == "RULE-SET" {
|
||||||
|
if rp, ok := ruleProviders[payload]; !ok {
|
||||||
|
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule-set '%s' not found", idx, line, payload)
|
||||||
|
} else {
|
||||||
|
switch rp.Behavior() {
|
||||||
|
case P.IPCIDR:
|
||||||
|
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule-set behavior is %s, must be domain or classical", idx, line, rp.Behavior())
|
||||||
|
case P.Classical:
|
||||||
|
log.Warnln("%s provider is %s, only matching domain rules in fake-ip-filter", rp.Name(), rp.Behavior())
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := R.ParseRule(tp, payload, action, params, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: %w", idx, line, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDomainRule(parsed.RuleType()) && parsed.RuleType() != C.MATCH {
|
||||||
|
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule type '%s' not supported, only domain-based rules allowed", idx, line, tp)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDomainRule(rt C.RuleType) bool {
|
||||||
|
switch rt {
|
||||||
|
case C.Domain, C.DomainSuffix, C.DomainKeyword, C.DomainRegex, C.DomainWildcard, C.GEOSITE, C.RuleSet:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseAuthentication(rawRecords []string) []auth.AuthUser {
|
func parseAuthentication(rawRecords []string) []auth.AuthUser {
|
||||||
var users []auth.AuthUser
|
var users []auth.AuthUser
|
||||||
for _, line := range rawRecords {
|
for _, line := range rawRecords {
|
||||||
|
|||||||
@@ -274,6 +274,10 @@ func (s *packetAdapter) Key() string {
|
|||||||
return s.key
|
return s.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *packetAdapter) Upstream() any {
|
||||||
|
return s.UDPPacket
|
||||||
|
}
|
||||||
|
|
||||||
func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter {
|
func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter {
|
||||||
return &packetAdapter{
|
return &packetAdapter{
|
||||||
packet,
|
packet,
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ func (d *DNSPrefer) UnmarshalText(data []byte) error {
|
|||||||
var FilterModeMapping = map[string]FilterMode{
|
var FilterModeMapping = map[string]FilterMode{
|
||||||
FilterBlackList.String(): FilterBlackList,
|
FilterBlackList.String(): FilterBlackList,
|
||||||
FilterWhiteList.String(): FilterWhiteList,
|
FilterWhiteList.String(): FilterWhiteList,
|
||||||
|
FilterRule.String(): FilterRule,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterMode int
|
type FilterMode int
|
||||||
@@ -110,6 +111,7 @@ type FilterMode int
|
|||||||
const (
|
const (
|
||||||
FilterBlackList FilterMode = iota
|
FilterBlackList FilterMode = iota
|
||||||
FilterWhiteList
|
FilterWhiteList
|
||||||
|
FilterRule
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e FilterMode) String() string {
|
func (e FilterMode) String() string {
|
||||||
@@ -118,6 +120,8 @@ func (e FilterMode) String() string {
|
|||||||
return "blacklist"
|
return "blacklist"
|
||||||
case FilterWhiteList:
|
case FilterWhiteList:
|
||||||
return "whitelist"
|
return "whitelist"
|
||||||
|
case FilterRule:
|
||||||
|
return "rule"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/metacubex/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
// Rule Type
|
// Rule Type
|
||||||
const (
|
const (
|
||||||
Domain RuleType = iota
|
Domain RuleType = iota
|
||||||
@@ -129,3 +135,7 @@ type RuleGroup interface {
|
|||||||
Rule
|
Rule
|
||||||
GetRecodeSize() int
|
GetRecodeSize() int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrResetByRule = exceptions.Cause(io.EOF, "reset by rule") // TODO: replace function from sing
|
||||||
|
)
|
||||||
|
|||||||
@@ -272,8 +272,20 @@ dns:
|
|||||||
- rule-set:fakeip-filter
|
- rule-set:fakeip-filter
|
||||||
# fakeip-filter 为 geosite 中名为 fakeip-filter 的分类(需要自行保证该分类存在)
|
# fakeip-filter 为 geosite 中名为 fakeip-filter 的分类(需要自行保证该分类存在)
|
||||||
- geosite:fakeip-filter
|
- geosite:fakeip-filter
|
||||||
|
|
||||||
|
# 当 fake-ip-filter-mode: rule 时开启规则模式
|
||||||
|
# fake-ip 与路由 rules 匹配逻辑一致(自上而下),语法也一致,支持GEOSITE、RuleSet、DOMAIN*、MATCH
|
||||||
|
- RULE-SET,reject-domain,fake-ip # 自定义 RuleSet behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则
|
||||||
|
- RULE-SET,proxy-domain,fake-ip
|
||||||
|
- GEOSITE,gfw,fake-ip
|
||||||
|
- DOMAIN,www.baidu.com,real-ip
|
||||||
|
- DOMAIN-SUFFIX,qq.com,real-ip
|
||||||
|
- DOMAIN-SUFFIX,jd.com,fake-ip
|
||||||
|
- MATCH,fake-ip # 最后 fake-ip or real-ip
|
||||||
|
|
||||||
# 配置fake-ip-filter的匹配模式,默认为blacklist,即如果匹配成功不返回fake-ip
|
# 配置fake-ip-filter的匹配模式,默认为blacklist,即如果匹配成功不返回fake-ip
|
||||||
# 可设置为whitelist,即只有匹配成功才返回fake-ip
|
# 可设置为whitelist,即只有匹配成功才返回fake-ip
|
||||||
|
# 也可配置为rule,规则模式语法见fake-ip-filter说明
|
||||||
fake-ip-filter-mode: blacklist
|
fake-ip-filter-mode: blacklist
|
||||||
# 配置fakeip查询返回的TTL,非必要情况下请勿修改
|
# 配置fakeip查询返回的TTL,非必要情况下请勿修改
|
||||||
fake-ip-ttl: 1
|
fake-ip-ttl: 1
|
||||||
@@ -1041,7 +1053,7 @@ proxies: # socks5
|
|||||||
# sudoku
|
# sudoku
|
||||||
- name: sudoku
|
- name: sudoku
|
||||||
type: sudoku
|
type: sudoku
|
||||||
server: serverip # 1.2.3.4
|
server: server_ip/domain # 1.2.3.4 or domain
|
||||||
port: 443
|
port: 443
|
||||||
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
|
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
|
||||||
aead-method: chacha20-poly1305 # 可选值:chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
|
aead-method: chacha20-poly1305 # 可选值:chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
|
||||||
@@ -1051,7 +1063,10 @@ proxies: # socks5
|
|||||||
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
|
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
|
||||||
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
||||||
http-mask: true # 是否启用http掩码
|
http-mask: true # 是否启用http掩码
|
||||||
# http-mask-strategy: random # 可选:random(默认)、post、websocket;仅在 http-mask=true 时生效
|
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、auto;stream/poll/auto 支持走 CDN/反代
|
||||||
|
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 http(不会根据端口自动推断)
|
||||||
|
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
|
||||||
|
# http-mask-strategy: random # 可选:random(默认)、post、websocket;仅 legacy 下生效
|
||||||
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与服务端端保持相同(如果此处为false,则要求aead不可为none)
|
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与服务端端保持相同(如果此处为false,则要求aead不可为none)
|
||||||
|
|
||||||
# anytls
|
# anytls
|
||||||
@@ -1596,6 +1611,8 @@ listeners:
|
|||||||
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
||||||
handshake-timeout: 5 # optional
|
handshake-timeout: 5 # optional
|
||||||
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与客户端保持相同(如果此处为false,则要求aead不可为none)
|
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与客户端保持相同(如果此处为false,则要求aead不可为none)
|
||||||
|
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false)
|
||||||
|
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、auto;stream/poll/auto 支持走 CDN/反代
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -3,6 +3,7 @@ module github.com/metacubex/mihomo
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0
|
||||||
github.com/bahlo/generic-list-go v0.2.0
|
github.com/bahlo/generic-list-go v0.2.0
|
||||||
github.com/coreos/go-iptables v0.8.0
|
github.com/coreos/go-iptables v0.8.0
|
||||||
github.com/dlclark/regexp2 v1.11.5
|
github.com/dlclark/regexp2 v1.11.5
|
||||||
@@ -25,7 +26,7 @@ require (
|
|||||||
github.com/metacubex/http v0.1.0
|
github.com/metacubex/http v0.1.0
|
||||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9
|
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9
|
||||||
github.com/metacubex/mlkem v0.1.0
|
github.com/metacubex/mlkem v0.1.0
|
||||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72
|
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec
|
||||||
github.com/metacubex/randv2 v0.2.0
|
github.com/metacubex/randv2 v0.2.0
|
||||||
github.com/metacubex/restls-client-go v0.1.7
|
github.com/metacubex/restls-client-go v0.1.7
|
||||||
github.com/metacubex/sing v0.5.6
|
github.com/metacubex/sing v0.5.6
|
||||||
@@ -34,7 +35,7 @@ require (
|
|||||||
github.com/metacubex/sing-shadowsocks v0.2.12
|
github.com/metacubex/sing-shadowsocks v0.2.12
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.2.7
|
github.com/metacubex/sing-shadowsocks2 v0.2.7
|
||||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
|
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
|
||||||
github.com/metacubex/sing-tun v0.4.11
|
github.com/metacubex/sing-tun v0.4.12-0.20251231220427-f11396db2fa1
|
||||||
github.com/metacubex/sing-vmess v0.2.4
|
github.com/metacubex/sing-vmess v0.2.4
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
|
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
|
||||||
github.com/metacubex/smux v0.0.0-20251111013112-03f8d12dafc1
|
github.com/metacubex/smux v0.0.0-20251111013112-03f8d12dafc1
|
||||||
@@ -46,7 +47,6 @@ require (
|
|||||||
github.com/mroth/weightedrand/v2 v2.1.0
|
github.com/mroth/weightedrand/v2 v2.1.0
|
||||||
github.com/openacid/low v0.1.21
|
github.com/openacid/low v0.1.21
|
||||||
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
|
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
|
||||||
github.com/saba-futai/sudoku v0.0.2-d
|
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
||||||
github.com/samber/lo v1.52.0
|
github.com/samber/lo v1.52.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
@@ -66,7 +66,6 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
|
||||||
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
|
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
|
||||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
@@ -92,7 +91,7 @@ require (
|
|||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/metacubex/ascon v0.1.0 // indirect
|
github.com/metacubex/ascon v0.1.0 // indirect
|
||||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 // indirect
|
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
|
||||||
github.com/metacubex/hkdf v0.1.0 // indirect
|
github.com/metacubex/hkdf v0.1.0 // indirect
|
||||||
github.com/metacubex/hpke v0.1.0 // indirect
|
github.com/metacubex/hpke v0.1.0 // indirect
|
||||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
|
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -98,8 +98,8 @@ github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQux
|
|||||||
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ=
|
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ=
|
||||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||||
github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
|
github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
|
||||||
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
|
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
|
||||||
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
|
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
|
||||||
@@ -114,8 +114,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy
|
|||||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||||
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
||||||
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
||||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 h1:kNlYHZ75itJwkerDiySpixX+dKsv/K0TYQsKvuxogNM=
|
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec h1:5ePGO2Xht06fpwjNIzfY5XS+82xwDHHx4xGbqgLbxjA=
|
||||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72/go.mod h1:N071X2oW2+kIhLlHW3mfcD2QP+zWu2bEs1EEAm66bvI=
|
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
|
||||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||||
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
||||||
@@ -133,8 +133,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w
|
|||||||
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
|
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
|
||||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
||||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
||||||
github.com/metacubex/sing-tun v0.4.11 h1:NG5zpvYPbBXf+9GSUmDaGCDwl3hZXV677tbRAw0QtCM=
|
github.com/metacubex/sing-tun v0.4.12-0.20251231220427-f11396db2fa1 h1:eaQCkCI2STxaGiVK35huV9TjUu3QDo3LiS/LKO5n1Z8=
|
||||||
github.com/metacubex/sing-tun v0.4.11/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
github.com/metacubex/sing-tun v0.4.12-0.20251231220427-f11396db2fa1/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
||||||
github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkAaus6I=
|
github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkAaus6I=
|
||||||
github.com/metacubex/sing-vmess v0.2.4/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
|
github.com/metacubex/sing-vmess v0.2.4/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
||||||
@@ -170,8 +170,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/saba-futai/sudoku v0.0.2-d h1:HW/gIyNUFcDchpMN+ZhluM86U/HGkWkkRV+9Km6WZM8=
|
|
||||||
github.com/saba-futai/sudoku v0.0.2-d/go.mod h1:Rvggsoprp7HQM7bMIZUd1M27bPj8THRsZdY1dGbIAvo=
|
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type SudokuServer struct {
|
|||||||
EnablePureDownlink *bool `json:"enable-pure-downlink,omitempty"`
|
EnablePureDownlink *bool `json:"enable-pure-downlink,omitempty"`
|
||||||
CustomTable string `json:"custom-table,omitempty"`
|
CustomTable string `json:"custom-table,omitempty"`
|
||||||
CustomTables []string `json:"custom-tables,omitempty"`
|
CustomTables []string `json:"custom-tables,omitempty"`
|
||||||
|
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
|
||||||
|
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
|
||||||
|
|
||||||
// mihomo private extension (not the part of standard Sudoku protocol)
|
// mihomo private extension (not the part of standard Sudoku protocol)
|
||||||
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
|
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package inbound_test
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
"github.com/metacubex/mihomo/adapter/outbound"
|
||||||
@@ -149,6 +151,9 @@ func TestNewMieru(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInboundMieru(t *testing.T) {
|
func TestInboundMieru(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.26") {
|
||||||
|
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
|
||||||
|
}
|
||||||
t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) {
|
t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) {
|
||||||
testInboundMieruTCP(t, "HANDSHAKE_STANDARD")
|
testInboundMieruTCP(t, "HANDSHAKE_STANDARD")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type SudokuOption struct {
|
|||||||
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
|
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
|
||||||
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||||
CustomTables []string `inbound:"custom-tables,omitempty"`
|
CustomTables []string `inbound:"custom-tables,omitempty"`
|
||||||
|
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
|
||||||
|
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
||||||
|
|
||||||
// mihomo private extension (not the part of standard Sudoku protocol)
|
// mihomo private extension (not the part of standard Sudoku protocol)
|
||||||
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
||||||
@@ -59,6 +61,8 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
|
|||||||
EnablePureDownlink: options.EnablePureDownlink,
|
EnablePureDownlink: options.EnablePureDownlink,
|
||||||
CustomTable: options.CustomTable,
|
CustomTable: options.CustomTable,
|
||||||
CustomTables: options.CustomTables,
|
CustomTables: options.CustomTables,
|
||||||
|
DisableHTTPMask: options.DisableHTTPMask,
|
||||||
|
HTTPMaskMode: options.HTTPMaskMode,
|
||||||
}
|
}
|
||||||
serverConf.MuxOption = options.MuxOption.Build()
|
serverConf.MuxOption = options.MuxOption.Build()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package inbound_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/outbound"
|
"github.com/metacubex/mihomo/adapter/outbound"
|
||||||
@@ -164,3 +165,27 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
|
|||||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := "test_key_http_mask_mode"
|
||||||
|
|
||||||
|
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {
|
||||||
|
mode := mode
|
||||||
|
t.Run(mode, func(t *testing.T) {
|
||||||
|
inboundOptions := inbound.SudokuOption{
|
||||||
|
Key: key,
|
||||||
|
HTTPMaskMode: mode,
|
||||||
|
}
|
||||||
|
outboundOptions := outbound.SudokuOption{
|
||||||
|
Key: key,
|
||||||
|
HTTPMask: true,
|
||||||
|
HTTPMaskMode: mode,
|
||||||
|
}
|
||||||
|
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -296,8 +296,19 @@ func (c *packet) LocalAddr() net.Addr {
|
|||||||
|
|
||||||
func (c *packet) Drop() {
|
func (c *packet) Drop() {
|
||||||
c.buff.Release()
|
c.buff.Release()
|
||||||
|
// always try to report success to ensure that the memory is freed up
|
||||||
|
if handshake, isHandshake := common.Cast[network.HandshakeSuccess](c); isHandshake {
|
||||||
|
_ = handshake.HandshakeSuccess()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *packet) InAddr() net.Addr {
|
func (c *packet) InAddr() net.Addr {
|
||||||
return c.lAddr
|
return c.lAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *packet) Upstream() any {
|
||||||
|
if c.writer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *c.writer
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Listener struct {
|
|||||||
addr string
|
addr string
|
||||||
closed bool
|
closed bool
|
||||||
protoConf sudoku.ProtocolConfig
|
protoConf sudoku.ProtocolConfig
|
||||||
|
tunnelSrv *sudoku.HTTPMaskTunnelServer
|
||||||
handler *sing.ListenerHandler
|
handler *sing.ListenerHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,11 +47,33 @@ func (l *Listener) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||||
session, err := sudoku.ServerHandshake(conn, &l.protoConf)
|
handshakeConn := conn
|
||||||
|
handshakeCfg := &l.protoConf
|
||||||
|
if l.tunnelSrv != nil {
|
||||||
|
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
|
handshakeConn = c
|
||||||
|
}
|
||||||
|
if cfg != nil {
|
||||||
|
handshakeCfg = cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
_ = handshakeConn.Close()
|
||||||
|
if handshakeConn != conn {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch session.Type {
|
switch session.Type {
|
||||||
case sudoku.SessionTypeUoT:
|
case sudoku.SessionTypeUoT:
|
||||||
@@ -184,6 +207,8 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
|
|||||||
PaddingMax: paddingMax,
|
PaddingMax: paddingMax,
|
||||||
EnablePureDownlink: enablePureDownlink,
|
EnablePureDownlink: enablePureDownlink,
|
||||||
HandshakeTimeoutSeconds: handshakeTimeout,
|
HandshakeTimeoutSeconds: handshakeTimeout,
|
||||||
|
DisableHTTPMask: config.DisableHTTPMask,
|
||||||
|
HTTPMaskMode: config.HTTPMaskMode,
|
||||||
}
|
}
|
||||||
if len(tables) == 1 {
|
if len(tables) == 1 {
|
||||||
protoConf.Table = tables[0]
|
protoConf.Table = tables[0]
|
||||||
@@ -200,6 +225,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
|
|||||||
protoConf: protoConf,
|
protoConf: protoConf,
|
||||||
handler: h,
|
handler: h,
|
||||||
}
|
}
|
||||||
|
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package tproxy
|
package tproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/inbound"
|
"github.com/metacubex/mihomo/adapter/inbound"
|
||||||
"github.com/metacubex/mihomo/component/keepalive"
|
"github.com/metacubex/mihomo/component/keepalive"
|
||||||
|
"github.com/metacubex/mihomo/component/mptcp"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/transport/socks5"
|
"github.com/metacubex/mihomo/transport/socks5"
|
||||||
)
|
)
|
||||||
@@ -46,7 +48,11 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
|
|||||||
inbound.WithSpecialRules(""),
|
inbound.WithSpecialRules(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
l, err := net.Listen("tcp", addr)
|
// Golang will then enable mptcp support for listeners by default when the major version of go.mod is 1.24 or higher.
|
||||||
|
// This can cause tproxy to malfunction on certain Linux kernel versions, so we force to disable mptcp for tproxy.
|
||||||
|
lc := net.ListenConfig{}
|
||||||
|
mptcp.SetNetListenConfig(&lc, false)
|
||||||
|
l, err := lc.Listen(context.Background(), "tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if g.country == "lan" {
|
||||||
|
return !g.isLan(ip)
|
||||||
|
}
|
||||||
|
|
||||||
if geodata.GeodataMode() {
|
if geodata.GeodataMode() {
|
||||||
matcher, err := g.getIPMatcher()
|
matcher, err := g.getIPMatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -186,6 +190,11 @@ func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GEOIP) GetRecodeSize() int {
|
func (g *GEOIP) GetRecodeSize() int {
|
||||||
|
// skip pseudorule lan
|
||||||
|
if g.country == "lan" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
if matcher, err := g.GetIPMatcher(); err == nil {
|
if matcher, err := g.GetIPMatcher(); err == nil {
|
||||||
return matcher.Count()
|
return matcher.Count()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type ruleProviderSchema struct {
|
|||||||
Interval int `provider:"interval,omitempty"`
|
Interval int `provider:"interval,omitempty"`
|
||||||
SizeLimit int64 `provider:"size-limit,omitempty"`
|
SizeLimit int64 `provider:"size-limit,omitempty"`
|
||||||
Payload []string `provider:"payload,omitempty"`
|
Payload []string `provider:"payload,omitempty"`
|
||||||
|
Header map[string][]string `provider:"header,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRuleFunc) (P.RuleProvider, error) {
|
func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRuleFunc) (P.RuleProvider, error) {
|
||||||
@@ -54,7 +55,7 @@ func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRu
|
|||||||
return nil, C.Path.ErrNotSafePath(path)
|
return nil, C.Path.ErrNotSafePath(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, nil, resource.DefaultHttpTimeout, schema.SizeLimit)
|
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
|
||||||
case "inline":
|
case "inline":
|
||||||
return NewInlineProvider(name, behavior, schema.Payload, parse), nil
|
return NewInlineProvider(name, behavior, schema.Payload, parse), nil
|
||||||
default:
|
default:
|
||||||
|
|||||||
144
transport/sudoku/config.go
Normal file
144
transport/sudoku/config.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/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 toggles the bandwidth-optimized downlink mode.
|
||||||
|
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 (stream-one or split), CDN-compatible
|
||||||
|
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
|
||||||
|
// - "auto": try stream then fall back to poll
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.EnablePureDownlink && c.AEADMethod == "none" {
|
||||||
|
return fmt.Errorf("bandwidth optimized downlink requires AEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
130
transport/sudoku/crypto/aead.go
Normal file
130
transport/sudoku/crypto/aead.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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 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
|
||||||
|
|
||||||
|
} else 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())
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
)
|
)
|
||||||
|
|
||||||
type discardConn struct{}
|
type discardConn struct{}
|
||||||
|
|||||||
@@ -11,18 +11,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/saba-futai/sudoku/apis"
|
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||||
"github.com/saba-futai/sudoku/pkg/crypto"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtocolConfig = apis.ProtocolConfig
|
|
||||||
|
|
||||||
func DefaultConfig() *ProtocolConfig { return apis.DefaultConfig() }
|
|
||||||
|
|
||||||
type SessionType int
|
type SessionType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -105,14 +100,14 @@ const (
|
|||||||
downlinkModePacked byte = 0x02
|
downlinkModePacked byte = 0x02
|
||||||
)
|
)
|
||||||
|
|
||||||
func downlinkMode(cfg *apis.ProtocolConfig) byte {
|
func downlinkMode(cfg *ProtocolConfig) byte {
|
||||||
if cfg.EnablePureDownlink {
|
if cfg.EnablePureDownlink {
|
||||||
return downlinkModePure
|
return downlinkModePure
|
||||||
}
|
}
|
||||||
return downlinkModePacked
|
return downlinkModePacked
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table) net.Conn {
|
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
|
||||||
baseReader := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
|
baseReader := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
|
||||||
baseWriter := newSudokuObfsWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax)
|
baseWriter := newSudokuObfsWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax)
|
||||||
if cfg.EnablePureDownlink {
|
if cfg.EnablePureDownlink {
|
||||||
@@ -130,7 +125,7 @@ func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildServerObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
|
func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
|
||||||
uplink := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
|
uplink := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
|
||||||
if cfg.EnablePureDownlink {
|
if cfg.EnablePureDownlink {
|
||||||
downlink := &directionalConn{
|
downlink := &directionalConn{
|
||||||
@@ -189,12 +184,12 @@ type ClientHandshakeOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ClientHandshake performs the client-side Sudoku handshake (without sending target address).
|
// ClientHandshake performs the client-side Sudoku handshake (without sending target address).
|
||||||
func ClientHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (net.Conn, error) {
|
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
|
||||||
return ClientHandshakeWithOptions(rawConn, cfg, ClientHandshakeOptions{})
|
return ClientHandshakeWithOptions(rawConn, cfg, ClientHandshakeOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientHandshakeWithOptions performs the client-side Sudoku handshake (without sending target address).
|
// ClientHandshakeWithOptions performs the client-side Sudoku handshake (without sending target address).
|
||||||
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
|
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("config is required")
|
return nil, fmt.Errorf("config is required")
|
||||||
}
|
}
|
||||||
@@ -220,7 +215,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
handshake := buildHandshakePayload(cfg.Key)
|
handshake := buildHandshakePayload(cfg.Key)
|
||||||
if len(tableCandidates(cfg)) > 1 {
|
if len(cfg.tableCandidates()) > 1 {
|
||||||
handshake[15] = tableID
|
handshake[15] = tableID
|
||||||
}
|
}
|
||||||
if _, err := cConn.Write(handshake[:]); err != nil {
|
if _, err := cConn.Write(handshake[:]); err != nil {
|
||||||
@@ -236,7 +231,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
|
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
|
||||||
func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession, error) {
|
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("config is required")
|
return nil, fmt.Errorf("config is required")
|
||||||
}
|
}
|
||||||
@@ -260,7 +255,7 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, tableCandidates(cfg))
|
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/saba-futai/sudoku/apis"
|
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
|
func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
|
||||||
@@ -67,8 +66,8 @@ func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPackedConfig(table *sudokuobfs.Table) *apis.ProtocolConfig {
|
func newPackedConfig(table *sudokuobfs.Table) *ProtocolConfig {
|
||||||
cfg := apis.DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
cfg.Key = "sudoku-test-key"
|
cfg.Key = "sudoku-test-key"
|
||||||
cfg.Table = table
|
cfg.Table = table
|
||||||
cfg.PaddingMin = 10
|
cfg.PaddingMin = 10
|
||||||
@@ -118,7 +117,7 @@ func TestPackedDownlinkSoak(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
|
func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
||||||
serverConn, clientConn := net.Pipe()
|
serverConn, clientConn := net.Pipe()
|
||||||
target := fmt.Sprintf("1.1.1.%d:80", (id%200)+1)
|
target := fmt.Sprintf("1.1.1.%d:80", (id%200)+1)
|
||||||
payload := []byte{0x42, byte(id)}
|
payload := []byte{0x42, byte(id)}
|
||||||
@@ -176,7 +175,7 @@ func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPackedUoTSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
|
func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
||||||
serverConn, clientConn := net.Pipe()
|
serverConn, clientConn := net.Pipe()
|
||||||
target := "8.8.8.8:53"
|
target := "8.8.8.8:53"
|
||||||
payload := []byte{0xaa, byte(id)}
|
payload := []byte{0xaa, byte(id)}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
88
transport/sudoku/httpmask_tunnel.go
Normal file
88
transport/sudoku/httpmask_tunnel.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPMaskTunnelServer struct {
|
||||||
|
cfg *ProtocolConfig
|
||||||
|
ts *httpmask.TunnelServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
|
||||||
|
if cfg == nil {
|
||||||
|
return &HTTPMaskTunnelServer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ts *httpmask.TunnelServer
|
||||||
|
if !cfg.DisableHTTPMask {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||||
|
case "stream", "poll", "auto":
|
||||||
|
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{Mode: cfg.HTTPMaskMode})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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) and returns a stream carrying raw Sudoku bytes.
|
||||||
|
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (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":
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
|
||||||
|
}
|
||||||
|
return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{
|
||||||
|
Mode: cfg.HTTPMaskMode,
|
||||||
|
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||||
|
HostOverride: cfg.HTTPMaskHost,
|
||||||
|
DialContext: dial,
|
||||||
|
})
|
||||||
|
}
|
||||||
445
transport/sudoku/httpmask_tunnel_test.go
Normal file
445
transport/sudoku/httpmask_tunnel_test.go
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSession) error) (addr string, stop func(), errCh <-chan error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errC := make(chan error, 128)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
tunnelSrv := NewHTTPMaskTunnelServer(cfg)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var stopOnce sync.Once
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for {
|
||||||
|
c, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
close(done)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
handshakeConn, handshakeCfg, handled, err := tunnelSrv.WrapConn(conn)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if handshakeConn == nil || handshakeCfg == nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
errC <- fmt.Errorf("wrap conn returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := ServerHandshake(handshakeConn, handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
_ = handshakeConn.Close()
|
||||||
|
if handshakeConn != conn {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer session.Conn.Close()
|
||||||
|
|
||||||
|
if handleErr := handle(session); handleErr != nil {
|
||||||
|
errC <- handleErr
|
||||||
|
}
|
||||||
|
}(c)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
stop = func() {
|
||||||
|
stopOnce.Do(func() {
|
||||||
|
_ = ln.Close()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("server did not stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatalf("server goroutines did not exit")
|
||||||
|
}
|
||||||
|
close(errC)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ln.Addr().String(), stop, errC
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTunnelTestTable(t *testing.T, key string) *ProtocolConfig {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tables, err := NewTablesWithCustomPatterns(ClientAEADSeed(key), "prefer_ascii", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("build tables: %v", err)
|
||||||
|
}
|
||||||
|
if len(tables) != 1 {
|
||||||
|
t.Fatalf("unexpected tables: %d", len(tables))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.Key = key
|
||||||
|
cfg.AEADMethod = "chacha20-poly1305"
|
||||||
|
cfg.Table = tables[0]
|
||||||
|
cfg.PaddingMin = 0
|
||||||
|
cfg.PaddingMax = 0
|
||||||
|
cfg.HandshakeTimeoutSeconds = 5
|
||||||
|
cfg.EnablePureDownlink = true
|
||||||
|
cfg.DisableHTTPMask = false
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
|
||||||
|
key := "tunnel-stream-key"
|
||||||
|
target := "1.1.1.1:80"
|
||||||
|
|
||||||
|
serverCfg := newTunnelTestTable(t, key)
|
||||||
|
serverCfg.HTTPMaskMode = "stream"
|
||||||
|
|
||||||
|
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||||
|
if s.Type != SessionTypeTCP {
|
||||||
|
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||||
|
}
|
||||||
|
if s.Target != target {
|
||||||
|
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||||
|
}
|
||||||
|
_, _ = s.Conn.Write([]byte("ok"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
clientCfg := *serverCfg
|
||||||
|
clientCfg.ServerAddress = addr
|
||||||
|
clientCfg.HTTPMaskHost = "example.com"
|
||||||
|
|
||||||
|
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial tunnel: %v", err)
|
||||||
|
}
|
||||||
|
defer tunnelConn.Close()
|
||||||
|
|
||||||
|
handshakeCfg := clientCfg
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client handshake: %v", err)
|
||||||
|
}
|
||||||
|
defer cConn.Close()
|
||||||
|
|
||||||
|
addrBuf, err := EncodeAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode addr: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := cConn.Write(addrBuf); err != nil {
|
||||||
|
t.Fatalf("write addr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(cConn, buf); err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
if string(buf) != "ok" {
|
||||||
|
t.Fatalf("unexpected payload: %q", buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop()
|
||||||
|
for err := range errCh {
|
||||||
|
t.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
|
||||||
|
key := "tunnel-poll-key"
|
||||||
|
target := "8.8.8.8:53"
|
||||||
|
payload := []byte{0xaa, 0xbb, 0xcc, 0xdd}
|
||||||
|
|
||||||
|
serverCfg := newTunnelTestTable(t, key)
|
||||||
|
serverCfg.HTTPMaskMode = "poll"
|
||||||
|
|
||||||
|
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||||
|
if s.Type != SessionTypeUoT {
|
||||||
|
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||||
|
}
|
||||||
|
gotAddr, gotPayload, err := ReadDatagram(s.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server read datagram: %w", err)
|
||||||
|
}
|
||||||
|
if gotAddr != target {
|
||||||
|
return fmt.Errorf("uot target mismatch: %s", gotAddr)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(gotPayload, payload) {
|
||||||
|
return fmt.Errorf("uot payload mismatch: %x", gotPayload)
|
||||||
|
}
|
||||||
|
if err := WriteDatagram(s.Conn, gotAddr, gotPayload); err != nil {
|
||||||
|
return fmt.Errorf("server write datagram: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
clientCfg := *serverCfg
|
||||||
|
clientCfg.ServerAddress = addr
|
||||||
|
|
||||||
|
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial tunnel: %v", err)
|
||||||
|
}
|
||||||
|
defer tunnelConn.Close()
|
||||||
|
|
||||||
|
handshakeCfg := clientCfg
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client handshake: %v", err)
|
||||||
|
}
|
||||||
|
defer cConn.Close()
|
||||||
|
|
||||||
|
if err := WritePreface(cConn); err != nil {
|
||||||
|
t.Fatalf("write preface: %v", err)
|
||||||
|
}
|
||||||
|
if err := WriteDatagram(cConn, target, payload); err != nil {
|
||||||
|
t.Fatalf("write datagram: %v", err)
|
||||||
|
}
|
||||||
|
gotAddr, gotPayload, err := ReadDatagram(cConn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read datagram: %v", err)
|
||||||
|
}
|
||||||
|
if gotAddr != target {
|
||||||
|
t.Fatalf("uot target mismatch: %s", gotAddr)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(gotPayload, payload) {
|
||||||
|
t.Fatalf("uot payload mismatch: %x", gotPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop()
|
||||||
|
for err := range errCh {
|
||||||
|
t.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
|
||||||
|
key := "tunnel-auto-key"
|
||||||
|
target := "9.9.9.9:443"
|
||||||
|
|
||||||
|
serverCfg := newTunnelTestTable(t, key)
|
||||||
|
serverCfg.HTTPMaskMode = "auto"
|
||||||
|
|
||||||
|
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||||
|
if s.Type != SessionTypeTCP {
|
||||||
|
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||||
|
}
|
||||||
|
if s.Target != target {
|
||||||
|
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||||
|
}
|
||||||
|
_, _ = s.Conn.Write([]byte("ok"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
clientCfg := *serverCfg
|
||||||
|
clientCfg.ServerAddress = addr
|
||||||
|
|
||||||
|
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial tunnel: %v", err)
|
||||||
|
}
|
||||||
|
defer tunnelConn.Close()
|
||||||
|
|
||||||
|
handshakeCfg := clientCfg
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client handshake: %v", err)
|
||||||
|
}
|
||||||
|
defer cConn.Close()
|
||||||
|
|
||||||
|
addrBuf, err := EncodeAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode addr: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := cConn.Write(addrBuf); err != nil {
|
||||||
|
t.Fatalf("write addr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(cConn, buf); err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
if string(buf) != "ok" {
|
||||||
|
t.Fatalf("unexpected payload: %q", buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop()
|
||||||
|
for err := range errCh {
|
||||||
|
t.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPMaskTunnel_Validation(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.Key = "k"
|
||||||
|
cfg.Table = NewTable("seed", "prefer_ascii")
|
||||||
|
cfg.ServerAddress = "127.0.0.1:1"
|
||||||
|
|
||||||
|
cfg.DisableHTTPMask = true
|
||||||
|
cfg.HTTPMaskMode = "stream"
|
||||||
|
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
|
||||||
|
t.Fatalf("expected error for disabled http mask")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.DisableHTTPMask = false
|
||||||
|
cfg.HTTPMaskMode = "legacy"
|
||||||
|
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
|
||||||
|
t.Fatalf("expected error for legacy mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
|
||||||
|
key := "tunnel-soak-key"
|
||||||
|
target := "1.0.0.1:80"
|
||||||
|
|
||||||
|
serverCfg := newTunnelTestTable(t, key)
|
||||||
|
serverCfg.HTTPMaskMode = "stream"
|
||||||
|
serverCfg.EnablePureDownlink = false
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessions = 8
|
||||||
|
payloadLen = 64 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||||
|
if s.Type != SessionTypeTCP {
|
||||||
|
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||||
|
}
|
||||||
|
if s.Target != target {
|
||||||
|
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||||
|
}
|
||||||
|
buf := make([]byte, payloadLen)
|
||||||
|
if _, err := io.ReadFull(s.Conn, buf); err != nil {
|
||||||
|
return fmt.Errorf("server read payload: %w", err)
|
||||||
|
}
|
||||||
|
_, err := s.Conn.Write(buf)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
runErr := make(chan error, sessions)
|
||||||
|
|
||||||
|
for i := 0; i < sessions; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
clientCfg := *serverCfg
|
||||||
|
clientCfg.ServerAddress = addr
|
||||||
|
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
|
||||||
|
|
||||||
|
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
|
||||||
|
if err != nil {
|
||||||
|
runErr <- fmt.Errorf("dial: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tunnelConn.Close()
|
||||||
|
|
||||||
|
handshakeCfg := clientCfg
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
runErr <- fmt.Errorf("handshake: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cConn.Close()
|
||||||
|
|
||||||
|
addrBuf, err := EncodeAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
runErr <- fmt.Errorf("encode addr: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := cConn.Write(addrBuf); err != nil {
|
||||||
|
runErr <- fmt.Errorf("write addr: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := bytes.Repeat([]byte{byte(id)}, payloadLen)
|
||||||
|
if _, err := cConn.Write(payload); err != nil {
|
||||||
|
runErr <- fmt.Errorf("write payload: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
echo := make([]byte, payloadLen)
|
||||||
|
if _, err := io.ReadFull(cConn, echo); err != nil {
|
||||||
|
runErr <- fmt.Errorf("read echo: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !bytes.Equal(echo, payload) {
|
||||||
|
runErr <- fmt.Errorf("echo mismatch")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runErr <- nil
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(runErr)
|
||||||
|
|
||||||
|
for err := range runErr {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("soak: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop()
|
||||||
|
for err := range errCh {
|
||||||
|
t.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
246
transport/sudoku/obfs/httpmask/masker.go
Normal file
246
transport/sudoku/obfs/httpmask/masker.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
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 {
|
||||||
|
// Get RNG from pool
|
||||||
|
r := rngPool.Get().(*rand.Rand)
|
||||||
|
defer rngPool.Put(r)
|
||||||
|
|
||||||
|
path := 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1691
transport/sudoku/obfs/httpmask/tunnel.go
Normal file
1691
transport/sudoku/obfs/httpmask/tunnel.go
Normal file
File diff suppressed because it is too large
Load Diff
212
transport/sudoku/obfs/sudoku/conn.go
Normal file
212
transport/sudoku/obfs/sudoku/conn.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
crypto_rand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 bool
|
||||||
|
recordLock sync.Mutex
|
||||||
|
|
||||||
|
rawBuf []byte
|
||||||
|
pendingData []byte
|
||||||
|
hintBuf []byte
|
||||||
|
|
||||||
|
rng *rand.Rand
|
||||||
|
paddingRate float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
|
||||||
|
var seedBytes [8]byte
|
||||||
|
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
|
||||||
|
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
|
||||||
|
}
|
||||||
|
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
|
||||||
|
localRng := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
|
min := float32(pMin) / 100.0
|
||||||
|
rng := float32(pMax-pMin) / 100.0
|
||||||
|
rate := min + localRng.Float32()*rng
|
||||||
|
|
||||||
|
sc := &Conn{
|
||||||
|
Conn: c,
|
||||||
|
table: table,
|
||||||
|
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||||
|
rawBuf: make([]byte, IOBufferSize),
|
||||||
|
pendingData: make([]byte, 0, 4096),
|
||||||
|
hintBuf: make([]byte, 0, 4),
|
||||||
|
rng: localRng,
|
||||||
|
paddingRate: rate,
|
||||||
|
}
|
||||||
|
if record {
|
||||||
|
sc.recorder = new(bytes.Buffer)
|
||||||
|
sc.recording = true
|
||||||
|
}
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *Conn) StopRecording() {
|
||||||
|
sc.recordLock.Lock()
|
||||||
|
sc.recording = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
outCapacity := len(p) * 6
|
||||||
|
out := make([]byte, 0, outCapacity)
|
||||||
|
pads := sc.table.PaddingPool
|
||||||
|
padLen := len(pads)
|
||||||
|
|
||||||
|
for _, b := range p {
|
||||||
|
if sc.rng.Float32() < sc.paddingRate {
|
||||||
|
out = append(out, pads[sc.rng.Intn(padLen)])
|
||||||
|
}
|
||||||
|
|
||||||
|
puzzles := sc.table.EncodeTable[b]
|
||||||
|
puzzle := puzzles[sc.rng.Intn(len(puzzles))]
|
||||||
|
|
||||||
|
perm := perm4[sc.rng.Intn(len(perm4))]
|
||||||
|
for _, idx := range perm {
|
||||||
|
if sc.rng.Float32() < sc.paddingRate {
|
||||||
|
out = append(out, pads[sc.rng.Intn(padLen)])
|
||||||
|
}
|
||||||
|
out = append(out, puzzle[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc.rng.Float32() < sc.paddingRate {
|
||||||
|
out = append(out, pads[sc.rng.Intn(padLen)])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = sc.Conn.Write(out)
|
||||||
|
return len(p), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *Conn) Read(p []byte) (n int, err error) {
|
||||||
|
if len(sc.pendingData) > 0 {
|
||||||
|
n = copy(p, sc.pendingData)
|
||||||
|
if n == len(sc.pendingData) {
|
||||||
|
sc.pendingData = sc.pendingData[:0]
|
||||||
|
} else {
|
||||||
|
sc.pendingData = sc.pendingData[n:]
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if len(sc.pendingData) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nr, rErr := sc.reader.Read(sc.rawBuf)
|
||||||
|
if nr > 0 {
|
||||||
|
chunk := sc.rawBuf[:nr]
|
||||||
|
sc.recordLock.Lock()
|
||||||
|
if sc.recording {
|
||||||
|
sc.recorder.Write(chunk)
|
||||||
|
}
|
||||||
|
sc.recordLock.Unlock()
|
||||||
|
|
||||||
|
for _, b := range chunk {
|
||||||
|
if !sc.table.layout.isHint(b) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.hintBuf = append(sc.hintBuf, b)
|
||||||
|
if len(sc.hintBuf) == 4 {
|
||||||
|
key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]})
|
||||||
|
val, ok := sc.table.DecodeMap[key]
|
||||||
|
if !ok {
|
||||||
|
return 0, errors.New("INVALID_SUDOKU_MAP_MISS")
|
||||||
|
}
|
||||||
|
sc.pendingData = append(sc.pendingData, val)
|
||||||
|
sc.hintBuf = sc.hintBuf[:0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rErr != nil {
|
||||||
|
return 0, rErr
|
||||||
|
}
|
||||||
|
if len(sc.pendingData) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n = copy(p, sc.pendingData)
|
||||||
|
if n == len(sc.pendingData) {
|
||||||
|
sc.pendingData = sc.pendingData[:0]
|
||||||
|
} else {
|
||||||
|
sc.pendingData = sc.pendingData[n:]
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
204
transport/sudoku/obfs/sudoku/layout.go
Normal file
204
transport/sudoku/obfs/sudoku/layout.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/bits"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type byteLayout struct {
|
||||||
|
name string
|
||||||
|
hintMask byte
|
||||||
|
hintValue byte
|
||||||
|
padMarker byte
|
||||||
|
paddingPool []byte
|
||||||
|
|
||||||
|
encodeHint func(val, pos byte) byte
|
||||||
|
encodeGroup func(group byte) byte
|
||||||
|
decodeGroup func(b byte) (byte, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *byteLayout) isHint(b byte) bool {
|
||||||
|
return (b & l.hintMask) == l.hintValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
return &byteLayout{
|
||||||
|
name: "ascii",
|
||||||
|
hintMask: 0x40,
|
||||||
|
hintValue: 0x40,
|
||||||
|
padMarker: 0x3F,
|
||||||
|
paddingPool: padding,
|
||||||
|
encodeHint: func(val, pos byte) byte {
|
||||||
|
return 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
|
||||||
|
},
|
||||||
|
encodeGroup: func(group byte) byte {
|
||||||
|
return 0x40 | (group & 0x3F)
|
||||||
|
},
|
||||||
|
decodeGroup: func(b byte) (byte, bool) {
|
||||||
|
if (b & 0x40) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return b & 0x3F, true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
return &byteLayout{
|
||||||
|
name: "entropy",
|
||||||
|
hintMask: 0x90,
|
||||||
|
hintValue: 0x00,
|
||||||
|
padMarker: 0x80,
|
||||||
|
paddingPool: padding,
|
||||||
|
encodeHint: func(val, pos byte) byte {
|
||||||
|
return ((val & 0x03) << 5) | (pos & 0x0F)
|
||||||
|
},
|
||||||
|
encodeGroup: func(group byte) byte {
|
||||||
|
v := group & 0x3F
|
||||||
|
return ((v & 0x30) << 1) | (v & 0x0F)
|
||||||
|
},
|
||||||
|
decodeGroup: func(b byte) (byte, bool) {
|
||||||
|
if (b & 0x90) != 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return ((b >> 1) & 0x30) | (b & 0x0F), true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeGroup := func(b byte) (byte, bool) {
|
||||||
|
if (b & xMask) != xMask {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
var val, pos byte
|
||||||
|
if b&(1<<pBits[0]) != 0 {
|
||||||
|
val |= 0x02
|
||||||
|
}
|
||||||
|
if b&(1<<pBits[1]) != 0 {
|
||||||
|
val |= 0x01
|
||||||
|
}
|
||||||
|
for i, bit := range vBits {
|
||||||
|
if b&(1<<bit) != 0 {
|
||||||
|
pos |= 1 << (3 - uint8(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group := (val << 4) | (pos & 0x0F)
|
||||||
|
return group, true
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &byteLayout{
|
||||||
|
name: fmt.Sprintf("custom(%s)", cleaned),
|
||||||
|
hintMask: xMask,
|
||||||
|
hintValue: xMask,
|
||||||
|
padMarker: padding[0],
|
||||||
|
paddingPool: padding,
|
||||||
|
encodeHint: func(val, pos byte) byte {
|
||||||
|
return encodeBits(val, pos, -1)
|
||||||
|
},
|
||||||
|
encodeGroup: func(group byte) byte {
|
||||||
|
val := (group >> 4) & 0x03
|
||||||
|
pos := group & 0x0F
|
||||||
|
return encodeBits(val, pos, -1)
|
||||||
|
},
|
||||||
|
decodeGroup: decodeGroup,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
332
transport/sudoku/obfs/sudoku/packed.go
Normal file
332
transport/sudoku/obfs/sudoku/packed.go
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
crypto_rand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 每次从 RNG 获取批量随机数的缓存大小,减少 RNG 函数调用开销
|
||||||
|
RngBatchSize = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1. 使用 12字节->16组 的块处理优化 Write (减少循环开销)
|
||||||
|
// 2. 使用浮点随机概率判断 Padding,与纯 Sudoku 保持流量特征一致
|
||||||
|
// 3. Read 使用 copy 移动避免底层数组泄漏
|
||||||
|
type PackedConn struct {
|
||||||
|
net.Conn
|
||||||
|
table *Table
|
||||||
|
reader *bufio.Reader
|
||||||
|
|
||||||
|
// 读缓冲
|
||||||
|
rawBuf []byte
|
||||||
|
pendingData []byte // 解码后尚未被 Read 取走的字节
|
||||||
|
|
||||||
|
// 写缓冲与状态
|
||||||
|
writeMu sync.Mutex
|
||||||
|
writeBuf []byte
|
||||||
|
bitBuf uint64 // 暂存的位数据
|
||||||
|
bitCount int // 暂存的位数
|
||||||
|
|
||||||
|
// 读状态
|
||||||
|
readBitBuf uint64
|
||||||
|
readBits int
|
||||||
|
|
||||||
|
// 随机数与填充控制 - 使用浮点随机,与 Conn 一致
|
||||||
|
rng *rand.Rand
|
||||||
|
paddingRate float32 // 与 Conn 保持一致的随机概率模型
|
||||||
|
padMarker byte
|
||||||
|
padPool []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
|
||||||
|
var seedBytes [8]byte
|
||||||
|
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
|
||||||
|
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
|
||||||
|
}
|
||||||
|
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
|
||||||
|
localRng := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
|
// 与 Conn 保持一致的 padding 概率计算
|
||||||
|
min := float32(pMin) / 100.0
|
||||||
|
rng := float32(pMax-pMin) / 100.0
|
||||||
|
rate := min + localRng.Float32()*rng
|
||||||
|
|
||||||
|
pc := &PackedConn{
|
||||||
|
Conn: c,
|
||||||
|
table: table,
|
||||||
|
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||||
|
rawBuf: make([]byte, IOBufferSize),
|
||||||
|
pendingData: make([]byte, 0, 4096),
|
||||||
|
writeBuf: make([]byte, 0, 4096),
|
||||||
|
rng: localRng,
|
||||||
|
paddingRate: rate,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeAddPadding 内联辅助:根据浮点概率插入 padding
|
||||||
|
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
|
||||||
|
if pc.rng.Float32() < pc.paddingRate {
|
||||||
|
out = append(out, pc.getPaddingByte())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write 极致优化版 - 批量处理 12 字节
|
||||||
|
func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.writeMu.Lock()
|
||||||
|
defer pc.writeMu.Unlock()
|
||||||
|
|
||||||
|
// 1. 预分配内存,避免 append 导致的多次扩容
|
||||||
|
// 预估:原数据 * 1.5 (4/3 + padding 余量)
|
||||||
|
needed := len(p)*3/2 + 32
|
||||||
|
if cap(pc.writeBuf) < needed {
|
||||||
|
pc.writeBuf = make([]byte, 0, needed)
|
||||||
|
}
|
||||||
|
out := pc.writeBuf[:0]
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
n := len(p)
|
||||||
|
|
||||||
|
// 2. 头部对齐处理 (Slow Path)
|
||||||
|
for pc.bitCount > 0 && i < n {
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
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.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(group&0x3F))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 极速批量处理 (Fast Path) - 每次处理 12 字节 → 生成 16 个编码组
|
||||||
|
for i+11 < n {
|
||||||
|
// 处理 4 组,每组 3 字节
|
||||||
|
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
|
||||||
|
|
||||||
|
// 每个组之前都有概率插入 padding
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g1))
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g2))
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g3))
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 处理剩余的 3 字节块
|
||||||
|
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.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g1))
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g2))
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g3))
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(g4))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 尾部处理 (Tail Path) - 处理剩余的 1 或 2 个字节
|
||||||
|
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.maybeAddPadding(out)
|
||||||
|
out = append(out, pc.encodeGroup(group&0x3F))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 处理残留位
|
||||||
|
if pc.bitCount > 0 {
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
group := byte(pc.bitBuf << (6 - pc.bitCount))
|
||||||
|
pc.bitBuf = 0
|
||||||
|
pc.bitCount = 0
|
||||||
|
out = append(out, pc.encodeGroup(group&0x3F))
|
||||||
|
out = append(out, pc.padMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尾部可能添加 padding
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
|
||||||
|
// 发送数据
|
||||||
|
if len(out) > 0 {
|
||||||
|
_, err := pc.Conn.Write(out)
|
||||||
|
pc.writeBuf = out[:0]
|
||||||
|
return len(p), err
|
||||||
|
}
|
||||||
|
pc.writeBuf = out[:0]
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush 处理最后不足 6 bit 的情况
|
||||||
|
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.encodeGroup(group&0x3F))
|
||||||
|
out = append(out, pc.padMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尾部随机添加 padding
|
||||||
|
out = pc.maybeAddPadding(out)
|
||||||
|
|
||||||
|
if len(out) > 0 {
|
||||||
|
_, err := pc.Conn.Write(out)
|
||||||
|
pc.writeBuf = out[:0]
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read 优化版:减少切片操作,避免内存泄漏
|
||||||
|
func (pc *PackedConn) Read(p []byte) (int, error) {
|
||||||
|
// 1. 优先返回待处理区的数据
|
||||||
|
if len(pc.pendingData) > 0 {
|
||||||
|
n := copy(p, pc.pendingData)
|
||||||
|
if n == len(pc.pendingData) {
|
||||||
|
pc.pendingData = pc.pendingData[:0]
|
||||||
|
} else {
|
||||||
|
// 优化:移动剩余数据到数组头部,避免切片指向中间导致内存泄漏
|
||||||
|
remaining := len(pc.pendingData) - n
|
||||||
|
copy(pc.pendingData, pc.pendingData[n:])
|
||||||
|
pc.pendingData = pc.pendingData[:remaining]
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 循环读取直到解出数据或出错
|
||||||
|
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.isHint(b) {
|
||||||
|
if b == padMarker {
|
||||||
|
rBuf = 0
|
||||||
|
rBits = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
group, ok := layout.decodeGroup(b)
|
||||||
|
if !ok {
|
||||||
|
return 0, ErrInvalidSudokuMapMiss
|
||||||
|
}
|
||||||
|
|
||||||
|
rBuf = (rBuf << 6) | uint64(group)
|
||||||
|
rBits += 6
|
||||||
|
|
||||||
|
if rBits >= 8 {
|
||||||
|
rBits -= 8
|
||||||
|
val := byte(rBuf >> rBits)
|
||||||
|
pc.pendingData = append(pc.pendingData, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.readBitBuf = rBuf
|
||||||
|
pc.readBits = rBits
|
||||||
|
}
|
||||||
|
|
||||||
|
if rErr != nil {
|
||||||
|
if rErr == io.EOF {
|
||||||
|
pc.readBitBuf = 0
|
||||||
|
pc.readBits = 0
|
||||||
|
}
|
||||||
|
if len(pc.pendingData) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return 0, rErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pc.pendingData) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 返回解码后的数据 - 优化:避免底层数组泄漏
|
||||||
|
n := copy(p, pc.pendingData)
|
||||||
|
if n == len(pc.pendingData) {
|
||||||
|
pc.pendingData = pc.pendingData[:0]
|
||||||
|
} else {
|
||||||
|
remaining := len(pc.pendingData) - n
|
||||||
|
copy(pc.pendingData, pc.pendingData[n:])
|
||||||
|
pc.pendingData = pc.pendingData[:remaining]
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPaddingByte 从 Pool 中随机取 Padding 字节
|
||||||
|
func (pc *PackedConn) getPaddingByte() byte {
|
||||||
|
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeGroup 编码 6-bit 组
|
||||||
|
func (pc *PackedConn) encodeGroup(group byte) byte {
|
||||||
|
return pc.table.layout.encodeGroup(group)
|
||||||
|
}
|
||||||
153
transport/sudoku/obfs/sudoku/table.go
Normal file
153
transport/sudoku/obfs/sudoku/table.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
log.Panicf("failed to build table: %v", err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTableWithCustom initializes obfuscation tables using either predefined or custom layouts.
|
||||||
|
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
|
||||||
|
// 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) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
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.encodeHint(p.val-1, p.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
|
||||||
|
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
|
||||||
|
key := packHintsToKey(currentHints)
|
||||||
|
t.DecodeMap[key] = byte(byteVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("[Init] Sudoku Tables initialized (%s) in %v", layout.name, time.Since(start))
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
)
|
)
|
||||||
|
|
||||||
// perm4 matches github.com/saba-futai/sudoku/pkg/obfs/sudoku perm4.
|
// perm4 matches github.com/saba-futai/sudoku/pkg/obfs/sudoku perm4.
|
||||||
|
|||||||
@@ -10,26 +10,12 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/saba-futai/sudoku/apis"
|
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||||
"github.com/saba-futai/sudoku/pkg/crypto"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func tableCandidates(cfg *apis.ProtocolConfig) []*sudoku.Table {
|
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, byte, error) {
|
||||||
if cfg == nil {
|
candidates := cfg.tableCandidates()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(cfg.Tables) > 0 {
|
|
||||||
return cfg.Tables
|
|
||||||
}
|
|
||||||
if cfg.Table != nil {
|
|
||||||
return []*sudoku.Table{cfg.Table}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickClientTable(cfg *apis.ProtocolConfig) (*sudoku.Table, byte, error) {
|
|
||||||
candidates := tableCandidates(cfg)
|
|
||||||
if len(candidates) == 0 {
|
if len(candidates) == 0 {
|
||||||
return nil, 0, fmt.Errorf("no table configured")
|
return nil, 0, fmt.Errorf("no table configured")
|
||||||
}
|
}
|
||||||
@@ -62,7 +48,7 @@ func drainBuffered(r *bufio.Reader) ([]byte, error) {
|
|||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.Table) error {
|
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
|
||||||
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
|
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
|
||||||
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
|
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
|
||||||
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
|
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
|
||||||
@@ -90,7 +76,7 @@ func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.T
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectTableByProbe(r *bufio.Reader, cfg *apis.ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
|
func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
|
||||||
const (
|
const (
|
||||||
maxProbeBytes = 64 * 1024
|
maxProbeBytes = 64 * 1024
|
||||||
readChunk = 4 * 1024
|
readChunk = 4 * 1024
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package sudoku
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
|
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
|
||||||
|
|||||||
@@ -299,7 +299,8 @@ func preHandleMetadata(metadata *C.Metadata) error {
|
|||||||
if exist {
|
if exist {
|
||||||
metadata.Host = host
|
metadata.Host = host
|
||||||
metadata.DNSMode = C.DNSMapping
|
metadata.DNSMode = C.DNSMapping
|
||||||
if resolver.FakeIPEnabled() {
|
if resolver.IsFakeIP(metadata.DstIP) {
|
||||||
|
// only clear dstIP if it is confirmed to be a fake IP
|
||||||
metadata.DstIP = netip.Addr{}
|
metadata.DstIP = netip.Addr{}
|
||||||
metadata.DNSMode = C.DNSFakeIP
|
metadata.DNSMode = C.DNSFakeIP
|
||||||
} else if node, ok := resolver.DefaultHosts.Search(host, false); ok {
|
} else if node, ok := resolver.DefaultHosts.Search(host, false); ok {
|
||||||
@@ -462,9 +463,17 @@ func handleUDPConn(packet C.PacketAdapter) {
|
|||||||
rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) {
|
rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) {
|
||||||
return proxy.ListenPacketContext(ctx, dialMetadata)
|
return proxy.ListenPacketContext(ctx, dialMetadata)
|
||||||
}, func(err error) {
|
}, func(err error) {
|
||||||
|
if !errors.Is(err, C.ErrResetByRule) {
|
||||||
logMetadataErr(metadata, rule, proxy, err)
|
logMetadataErr(metadata, rule, proxy, err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, C.ErrResetByRule) {
|
||||||
|
logMetadata(metadata, rule, rawPc)
|
||||||
|
if handshake, isHandshake := utils.Cast[N.HandshakeFailure](packet); isHandshake {
|
||||||
|
_ = handshake.HandshakeFailure(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
logMetadata(metadata, rule, rawPc)
|
logMetadata(metadata, rule, rawPc)
|
||||||
@@ -489,7 +498,7 @@ func handleUDPConn(packet C.PacketAdapter) {
|
|||||||
sender.Process(pc, proxy)
|
sender.Process(pc, proxy)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
sender.Send(packet) // nonblocking
|
sender.Send(packet) // will not block
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTCPConn(connCtx C.ConnContext) {
|
func handleTCPConn(connCtx C.ConnContext) {
|
||||||
@@ -699,6 +708,9 @@ func shouldStopRetry(err error) bool {
|
|||||||
if errors.Is(err, resolver.ErrIPv6Disabled) {
|
if errors.Is(err, resolver.ErrIPv6Disabled) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, C.ErrResetByRule) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if errors.Is(err, loopback.ErrReject) {
|
if errors.Is(err, loopback.ErrReject) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user