Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions adapter/outbound/vmess.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/mkcp"
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"

"github.com/metacubex/http"
Expand All @@ -36,6 +37,9 @@ type Vmess struct {
// for gun mux
gunClient *gun.Client

// for mkcp transport
mkcpConfig *mkcp.Config

realityConfig *tlsC.RealityConfig
echConfig *ech.Config
}
Expand Down Expand Up @@ -63,6 +67,7 @@ type VmessOption struct {
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
KCPOpts KCPOptions `proxy:"kcp-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
Expand Down Expand Up @@ -91,6 +96,40 @@ type GrpcOptions struct {
MaxStreams int `proxy:"max-streams,omitempty"`
}

// KCPOptions configures the mKCP transport (Xray-compatible). Field names and
// semantics mirror Xray's kcpSettings. Buffer sizes are in MB like Xray's JSON.
type KCPOptions struct {
MTU int `proxy:"mtu,omitempty"`
TTI int `proxy:"tti,omitempty"`
UplinkCapacity int `proxy:"uplink-capacity,omitempty"`
DownlinkCapacity int `proxy:"downlink-capacity,omitempty"`
Congestion bool `proxy:"congestion,omitempty"`
ReadBufferSize int `proxy:"read-buffer-size,omitempty"`
WriteBufferSize int `proxy:"write-buffer-size,omitempty"`
Header string `proxy:"header,omitempty"`
Seed string `proxy:"seed,omitempty"`
}

func (o KCPOptions) Build() *mkcp.Config {
cfg := &mkcp.Config{
Mtu: uint32(o.MTU),
Tti: uint32(o.TTI),
UplinkCapacity: uint32(o.UplinkCapacity),
DownlinkCapacity: uint32(o.DownlinkCapacity),
Congestion: o.Congestion,
Header: o.Header,
Seed: o.Seed,
}
// Xray expresses these buffers in MB; convert to bytes (0 keeps the default).
if o.ReadBufferSize > 0 {
cfg.ReadBufferSize = uint32(o.ReadBufferSize) * 1024 * 1024
}
if o.WriteBufferSize > 0 {
cfg.WriteBufferSize = uint32(o.WriteBufferSize) * 1024 * 1024
}
return cfg
}

type WSOptions struct {
Path string `proxy:"path,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
Expand Down Expand Up @@ -278,11 +317,35 @@ func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
return v.gunClient.Dial()
case "kcp": // mkcp transport over udp
pc, addr, err := v.listenPacketContext(ctx)
if err != nil {
return nil, err
}
c, err := mkcp.Dial(pc, addr, v.mkcpConfig)
if err != nil {
_ = pc.Close()
return nil, err
}
return c, nil
default:
}
return v.dialer.DialContext(ctx, "tcp", v.addr)
}

func (v *Vmess) listenPacketContext(ctx context.Context) (net.PacketConn, net.Addr, error) {
addr, err := resolveUDPAddr(ctx, "udp", v.addr, v.prefer)
if err != nil {
return nil, nil, err
}

pc, err := v.dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
if err != nil {
return nil, nil, err
}
return pc, addr, nil
}

// DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := v.dialContext(ctx)
Expand Down Expand Up @@ -407,6 +470,8 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if len(option.HTTP2Opts.Host) == 0 {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
}
case "kcp":
v.mkcpConfig = option.KCPOpts.Build()
case "grpc":
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
c, err := v.dialer.DialContext(ctx, "tcp", v.addr)
Expand Down
24 changes: 24 additions & 0 deletions component/sniffer/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
continue
}

// Protocol detected but no domain extracted (e.g. STUN)
if host == "" {
metadata.SniffProtocol = current.Protocol()
log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] protocol [%s] detected (no domain)",
metadata.NetWork,
metadata.SourceDetail(),
metadata.RemoteAddress(),
current.Protocol())
return packetSender
}

replaceDomain(metadata, host)
return packetSender
}
Expand Down Expand Up @@ -285,6 +296,17 @@ func NewDispatcher(snifferConfig *Config) (*Dispatcher, error) {
dispatcher.sniffers[s] = config
}

// Auto-register STUN sniffer when sniffer is enabled but STUN was not explicitly configured.
if snifferConfig.Enable {
if _, exists := snifferConfig.Sniffers[sniffer.STUN]; !exists {
s, err := NewSTUNSniffer(SnifferConfig{})
if err == nil {
dispatcher.sniffers[s] = SnifferConfig{}
log.Infoln("[Sniffer] STUN sniffer auto-enabled")
}
}
}

return &dispatcher, nil
}

Expand All @@ -296,6 +318,8 @@ func NewSniffer(name sniffer.Type, snifferConfig SnifferConfig) (sniffer.Sniffer
return NewHTTPSniffer(snifferConfig)
case sniffer.QUIC:
return NewQuicSniffer(snifferConfig)
case sniffer.STUN:
return NewSTUNSniffer(snifferConfig)
default:
return nil, ErrorUnsupportedSniffer
}
Expand Down
113 changes: 113 additions & 0 deletions component/sniffer/sniff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,119 @@ func TestQuicHeaders(t *testing.T) {
}
}

func TestSTUNSniffer(t *testing.T) {
sniffer, err := NewSTUNSniffer(SnifferConfig{})
assert.NoError(t, err)
assert.Equal(t, "stun", sniffer.Protocol())
assert.Equal(t, constant.UDP, sniffer.SupportNetwork())

cases := []struct {
name string
input []byte
isSTUN bool
}{
{
name: "valid STUN Binding Request",
input: []byte{
0x00, 0x01,
0x00, 0x00,
0x21, 0x12, 0xA4, 0x42,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c,
},
isSTUN: true,
},
{
name: "valid STUN Binding Response with attributes",
input: []byte{
0x01, 0x01,
0x00, 0x0c,
0x21, 0x12, 0xA4, 0x42,
0xaa, 0xbb, 0xcc, 0xdd,
0xee, 0xff, 0x00, 0x11,
0x22, 0x33, 0x44, 0x55,
0x00, 0x20,
0x00, 0x08,
0x00, 0x01,
0x63, 0x46,
0x73, 0x92, 0xa5, 0x46,
},
isSTUN: true,
},
{
name: "too short packet",
input: []byte{0x00, 0x01, 0x00, 0x00},
isSTUN: false,
},
{
name: "wrong magic cookie",
input: []byte{
0x00, 0x01,
0x00, 0x00,
0xDE, 0xAD, 0xBE, 0xEF,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c,
},
isSTUN: false,
},
{
name: "first 2 bits not zero (DTLS or other)",
input: []byte{
0x80, 0x01,
0x00, 0x00,
0x21, 0x12, 0xA4, 0x42,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c,
},
isSTUN: false,
},
{
name: "message length not multiple of 4",
input: []byte{
0x00, 0x01,
0x00, 0x03,
0x21, 0x12, 0xA4, 0x42,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c,
},
isSTUN: false,
},
{
name: "TLS handshake packet (not STUN)",
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00,
0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe,
0xb4, 0x96, 0x04, 0x5b,
},
isSTUN: false,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
host, err := sniffer.SniffData(tc.input)
if tc.isSTUN {
assert.NoError(t, err)
assert.Equal(t, "", host)
} else {
assert.Error(t, err)
}
})
}

// Default: no port restriction (all ports match)
assert.True(t, sniffer.SupportPort(3478))
assert.True(t, sniffer.SupportPort(5349))
assert.True(t, sniffer.SupportPort(19302))
assert.True(t, sniffer.SupportPort(443))
assert.True(t, sniffer.SupportPort(80))
assert.True(t, sniffer.SupportPort(12345))
}

func TestTLSHeaders(t *testing.T) {
cases := []struct {
input []byte
Expand Down
68 changes: 68 additions & 0 deletions component/sniffer/stun_sniffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package sniffer

import (
"encoding/binary"
"errors"

C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/sniffer"
)

// https://datatracker.ietf.org/doc/html/rfc8489
const (
stunHeaderSize = 20
stunMagicCookie = 0x2112A442
)

var (
errNotSTUN = errors.New("not STUN message")
)

var _ sniffer.Sniffer = (*STUNSniffer)(nil)

type STUNSniffer struct {
*BaseSniffer
}

func NewSTUNSniffer(snifferConfig SnifferConfig) (*STUNSniffer, error) {
return &STUNSniffer{
BaseSniffer: NewBaseSniffer(snifferConfig.Ports, C.UDP),
}, nil
}

func (s *STUNSniffer) Protocol() string {
return "stun"
}

func (s *STUNSniffer) SupportNetwork() C.NetWork {
return C.UDP
}

func (s *STUNSniffer) SniffData(b []byte) (string, error) {
if err := detectSTUN(b); err != nil {
return "", err
}

return "", nil
}

func detectSTUN(b []byte) error {
if len(b) < stunHeaderSize {
return errNotSTUN
}

if b[0]&0xC0 != 0x00 {
return errNotSTUN
}

if binary.BigEndian.Uint32(b[4:8]) != stunMagicCookie {
return errNotSTUN
}

msgLen := binary.BigEndian.Uint16(b[2:4])
if msgLen%4 != 0 {
return errNotSTUN
}

return nil
}
3 changes: 2 additions & 1 deletion constant/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ type Metadata struct {
RawSrcAddr net.Addr `json:"-"`
RawDstAddr net.Addr `json:"-"`
// Only domain rule
SniffHost string `json:"sniffHost"`
SniffHost string `json:"sniffHost"`
SniffProtocol string `json:"sniffProtocol,omitempty"`
}

func (m *Metadata) RemoteAddress() string {
Expand Down
3 changes: 3 additions & 0 deletions constant/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
ProcessPathWildcard
RuleSet
Network
SniffProtocol
Uid
SubRules
MATCH
Expand Down Expand Up @@ -103,6 +104,8 @@ func (rt RuleType) String() string {
return "RuleSet"
case Network:
return "Network"
case SniffProtocol:
return "SniffProtocol"
case DSCP:
return "DSCP"
case Uid:
Expand Down
5 changes: 4 additions & 1 deletion constant/sniffer/sniffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ const (
TLS Type = iota
HTTP
QUIC
STUN
)

var (
List = []Type{TLS, HTTP, QUIC}
List = []Type{TLS, HTTP, QUIC, STUN}
)

type Type int
Expand All @@ -36,6 +37,8 @@ func (rt Type) String() string {
return "HTTP"
case QUIC:
return "QUIC"
case STUN:
return "STUN"
default:
return "Unknown"
}
Expand Down
Loading