Skip to content
Merged
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
27 changes: 26 additions & 1 deletion adapter/outbound/snell.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Snell struct {
restlsConfig *restls.Config
jlsConfig *jls.ClientConfig
version int
v6Mode snell.V6Mode
reuse bool
}

Expand All @@ -38,6 +39,7 @@ type SnellOption struct {
UDP bool `proxy:"udp,omitempty"`
Version int `proxy:"version,omitempty"`
Reuse bool `proxy:"reuse,omitempty"`
Mode string `proxy:"mode,omitempty"` // v6 only: default / unshaped / unsafe-raw
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
}
Expand Down Expand Up @@ -66,6 +68,9 @@ func (s *Snell) streamConnContext(ctx context.Context, c net.Conn) (*snell.Snell
return nil, err
}
}
if s.version == snell.Version6 {
return snell.StreamConnV6(c, s.psk, s.v6Mode), nil
}
return snell.StreamConn(c, s.psk, s.version), nil
}

Expand Down Expand Up @@ -249,16 +254,35 @@ func NewSnell(option SnellOption) (*Snell, error) {
// Snell v5 servers are backward-compatible with v4 clients.
option.Version = snell.Version4
}
reuse := option.Version == snell.Version2 || (option.Version == snell.Version4 && option.Reuse)
reuse := option.Version == snell.Version2 ||
((option.Version == snell.Version4 || option.Version == snell.Version6) && option.Reuse)
var v6Mode snell.V6Mode
switch option.Version {
case snell.Version1, snell.Version2:
if option.UDP {
return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
}
case snell.Version3, snell.Version4:
case snell.Version6:
if err := snell.ValidateV6PSK(psk); err != nil {
return nil, fmt.Errorf("snell %s %w", addr, err)
}
switch obfsOption.Mode {
case "tls", "http":
// v6 replaced the simple-obfs layer with PSK-derived traffic shaping
return nil, fmt.Errorf("snell %s version 6 does not support obfs mode %s", addr, obfsOption.Mode)
}
var err error
v6Mode, err = snell.ParseV6Mode(option.Mode)
if err != nil {
return nil, fmt.Errorf("snell %s %w", addr, err)
}
default:
return nil, fmt.Errorf("snell version error: %d", option.Version)
}
if option.Mode != "" && option.Version != snell.Version6 {
return nil, fmt.Errorf("snell %s mode is only supported on version 6", addr)
}

s := &Snell{
Base: NewBase(BaseOption{
Expand All @@ -280,6 +304,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
restlsConfig: restlsConfig,
jlsConfig: jlsConfig,
version: option.Version,
v6Mode: v6Mode,
reuse: reuse,
}
s.dialer = option.NewDialer(s.DialOptions())
Expand Down
24 changes: 13 additions & 11 deletions docs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -414,20 +414,21 @@ proxies: # socks5
server: server
port: 44046
psk: yourpsk
# version: 4 # support 1/2/3/4/5
# udp: true # support UDP on version 3/4/5
# reuse: false # v4/5 only
# version: 4 # support 1/2/3/4/5/6
# udp: true # support UDP on version 3/4/5/6
# reuse: false # v4/5/6 only
# mode: default # v6 only, default / unshaped / unsafe-raw, must match the server
# obfs-opts:
# mode: http # or tls/shadow-tls/restls/jls
# mode: http # or tls/shadow-tls/restls/jls (http/tls are not available on v6)

- name: "snell-shadow-tls"
type: snell
server: server
port: 44046
psk: yourpsk
# version: 4 # support 1/2/3/4/5
# udp: true # support UDP on version 3/4/5
# reuse: false # v4/5 only
# version: 4 # support 1/2/3/4/5/6
# udp: true # support UDP on version 3/4/5/6
# reuse: false # v4/5/6 only
# client-fingerprint: chrome
obfs-opts:
mode: shadow-tls
Expand All @@ -441,7 +442,7 @@ proxies: # socks5
server: server
port: 44046
psk: yourpsk
# version: 4 # support 1/2/3/4/5
# version: 4 # support 1/2/3/4/5/6
# client-fingerprint: chrome
obfs-opts:
mode: restls
Expand All @@ -454,7 +455,7 @@ proxies: # socks5
server: server
port: 44046
psk: yourpsk
# version: 4 # support 1/2/3/4/5
# version: 4 # support 1/2/3/4/5/6
# client-fingerprint: chrome
obfs-opts:
mode: jls
Expand Down Expand Up @@ -2195,10 +2196,11 @@ listeners:
listen: 0.0.0.0
# routing-mark: 0 # 为监听socket设置routing-mark(仅支持linux)
psk: your-password
version: 4 # 支持 1/2/3/4/5
version: 4 # 支持 1/2/3/4/5/6
udp: true # UDP over TCP tunnel,默认 true
# mode: default # 仅 v6:default / unshaped / unsafe-raw,需与客户端一致;v6 的 psk 长度须为 12 到 255 字节
# obfs-opts:
# mode: http # 可选:http / tls
# mode: http # 可选:http / tls(v6 不支持,配置时会报错)
# host: bing.com
# shadow-tls:
# enable: false # 设置为true时开启
Expand Down
1 change: 1 addition & 0 deletions listener/config/snell.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type SnellServer struct {
Listen string
Psk string
Version int
Mode string // v6 only: default / unshaped / unsafe-raw
UDP bool
ObfsMode string
ObfsHost string
Expand Down
4 changes: 3 additions & 1 deletion listener/inbound/snell.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type SnellOption struct {
BaseOption
Psk string `inbound:"psk"`
Version int `inbound:"version,omitempty"`
Mode string `inbound:"mode,omitempty"` // v6 only: default / unshaped / unsafe-raw
UDP bool `inbound:"udp,omitempty"`
ObfsOpts SnellObfsOption `inbound:"obfs-opts,omitempty"`
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
Expand Down Expand Up @@ -45,7 +46,7 @@ func NewSnell(options *SnellOption) (*Snell, error) {
if options.Version == 0 {
options.Version = 4
}
if options.Version < 1 || options.Version > 5 {
if options.Version < 1 || options.Version > 6 {
return nil, fmt.Errorf("snell inbound version %d is not supported", options.Version)
}

Expand All @@ -56,6 +57,7 @@ func NewSnell(options *SnellOption) (*Snell, error) {
Listen: base.RawAddress(),
Psk: options.Psk,
Version: options.Version,
Mode: options.Mode,
UDP: options.UDP,
ObfsMode: options.ObfsOpts.Mode,
ObfsHost: options.ObfsOpts.Host,
Expand Down
173 changes: 173 additions & 0 deletions listener/inbound/snell_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package inbound_test

import (
"bytes"
"context"
"crypto/rand"
"net"
"net/netip"
"testing"
"time"

"github.com/metacubex/mihomo/adapter/outbound"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/metacubex/mihomo/transport/jls"
"github.com/metacubex/mihomo/transport/restls"
Expand Down Expand Up @@ -157,3 +162,171 @@ func TestInboundSnell_JLS(t *testing.T) {
}
testInboundSnellUTLS(t, inboundOptions, outboundOptions)
}

func TestInboundSnell_V6(t *testing.T) {
for _, mode := range []string{"default", "unshaped", "unsafe-raw"} {
mode := mode
t.Run(mode, func(t *testing.T) {
t.Parallel()
t.Run("TCP", func(t *testing.T) {
inboundOptions := inbound.SnellOption{Version: 6, Mode: mode}
outboundOptions := outbound.SnellOption{Version: 6, Mode: mode}
testInboundSnell(t, inboundOptions, outboundOptions)
})
t.Run("TCPReuse", func(t *testing.T) {
inboundOptions := inbound.SnellOption{Version: 6, Mode: mode}
outboundOptions := outbound.SnellOption{Version: 6, Mode: mode, Reuse: true}
testInboundSnell(t, inboundOptions, outboundOptions)
})
t.Run("UDP", func(t *testing.T) {
testInboundSnellV6UDP(t, mode)
})
})
}
}

func TestInboundSnell_V6_ShadowTlsv3(t *testing.T) {
inboundOptions := inbound.SnellOption{
Version: 6,
ShadowTLS: inbound.ShadowTLS{
Enable: true,
Version: 3,
Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}},
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
},
}
outboundOptions := outbound.SnellOption{
Version: 6,
ObfsOpts: map[string]any{"mode": shadowtls.Mode, "host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3},
}
testInboundSnellUTLS(t, inboundOptions, outboundOptions)
}

// testInboundSnellV6UDP runs a UDP-over-TCP echo through the v6 inbound: the
// test tunnel reflects every datagram back to the source it came from.
func testInboundSnellV6UDP(t *testing.T, mode string) {
t.Parallel()
inboundOptions := inbound.SnellOption{
BaseOption: inbound.BaseOption{
NameStr: "snell_inbound_udp",
Listen: "127.0.0.1",
Port: "0",
},
Psk: userUUID,
Version: 6,
Mode: mode,
UDP: true,
}
in, err := inbound.NewSnell(&inboundOptions)
if !assert.NoError(t, err) {
return
}

tunnel := NewHttpTestTunnel()
defer tunnel.Close()
tunnel.HandleUDPPacketFn = func(packet C.UDPPacket, metadata *C.Metadata) {
defer packet.Drop()
if !assert.Equal(t, C.SNELL, metadata.Type) {
return
}
_, err := packet.WriteBack(packet.Data(), metadata.UDPAddr())
assert.NoError(t, err)
}

err = in.Listen(tunnel)
if !assert.NoError(t, err) {
return
}
defer in.Close()

addrPort, err := netip.ParseAddrPort(in.Address())
if !assert.NoError(t, err) {
return
}

outboundOptions := outbound.SnellOption{
Name: "snell_outbound_udp",
Server: addrPort.Addr().String(),
Port: int(addrPort.Port()),
Psk: userUUID,
Version: 6,
Mode: mode,
UDP: true,
}
outboundOptions.DialerForAPI = tunnel.NewDialer()
outboundOptions.TunnelForAPI = tunnel
out, err := outbound.NewSnell(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
target := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53}
pc, err := out.ListenPacketContext(ctx, &C.Metadata{
NetWork: C.UDP,
DstIP: netip.MustParseAddr("127.0.0.1"),
DstPort: 53,
})
if !assert.NoError(t, err) {
return
}
defer pc.Close()

for round := 0; round < 5; round++ {
message := make([]byte, 1200)
_, err = rand.Read(message)
if !assert.NoError(t, err) {
return
}
_, err = pc.WriteTo(message, target)
if !assert.NoError(t, err) {
return
}

received := make([]byte, 2048)
if !assert.NoError(t, pc.SetReadDeadline(time.Now().Add(10*time.Second))) {
return
}
n, from, err := pc.ReadFrom(received)
if !assert.NoError(t, err, "round %d", round) {
return
}
assert.Equal(t, target.String(), from.String())
assert.True(t, bytes.Equal(message, received[:n]), "round %d payload mismatch", round)
}
}

func TestInboundSnell_V6_Validation(t *testing.T) {
t.Parallel()
newInbound := func(options inbound.SnellOption) error {
options.BaseOption = inbound.BaseOption{
NameStr: "snell_inbound_validation",
Listen: "127.0.0.1",
Port: "0",
}
if options.Psk == "" {
options.Psk = userUUID
}
in, err := inbound.NewSnell(&options)
if err != nil {
return err
}
tunnel := NewHttpTestTunnel()
defer tunnel.Close()
err = in.Listen(tunnel)
if err == nil {
_ = in.Close()
}
return err
}

assert.Error(t, newInbound(inbound.SnellOption{Version: 6, Psk: "short"}), "psk shorter than 12 bytes must be rejected")
assert.Error(t, newInbound(inbound.SnellOption{Version: 6, ObfsOpts: inbound.SnellObfsOption{Mode: "http"}}), "v6 has no simple-obfs layer")
assert.Error(t, newInbound(inbound.SnellOption{Version: 6, ObfsOpts: inbound.SnellObfsOption{Mode: "tls"}}), "v6 has no simple-obfs layer")
assert.Error(t, newInbound(inbound.SnellOption{Version: 6, Mode: "bogus"}), "unknown mode must be rejected")
assert.Error(t, newInbound(inbound.SnellOption{Version: 4, Mode: "unshaped"}), "mode is v6 only")
assert.Error(t, newInbound(inbound.SnellOption{Version: 7}), "unknown version must be rejected")
assert.NoError(t, newInbound(inbound.SnellOption{Version: 6, Mode: "unshaped"}))
}
Loading
Loading