Skip to content

Commit 99aa1b0

Browse files
committed
feat: inbound support shadow-tls
1 parent 52ad793 commit 99aa1b0

File tree

11 files changed

+270
-10
lines changed

11 files changed

+270
-10
lines changed

adapter/outbound/shadowsocks.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ type gostObfsOption struct {
8484
}
8585

8686
type shadowTLSOption struct {
87-
Password string `obfs:"password,omitempty"`
88-
Host string `obfs:"host"`
89-
Fingerprint string `obfs:"fingerprint,omitempty"`
90-
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
91-
Version int `obfs:"version,omitempty"`
87+
Password string `obfs:"password,omitempty"`
88+
Host string `obfs:"host"`
89+
Fingerprint string `obfs:"fingerprint,omitempty"`
90+
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
91+
Version int `obfs:"version,omitempty"`
92+
ALPN []string `obfs:"alpn,omitempty"`
9293
}
9394

9495
type restlsOption struct {
@@ -342,6 +343,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
342343
SkipCertVerify: opt.SkipCertVerify,
343344
Version: opt.Version,
344345
}
346+
347+
if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
348+
shadowTLSOpt.ALPN = opt.ALPN
349+
} else {
350+
shadowTLSOpt.ALPN = shadowtls.DefaultALPN
351+
}
345352
} else if option.Plugin == restls.Mode {
346353
obfsMode = restls.Mode
347354
restlsOpt := &restlsOption{}

docs/config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ proxies: # socks5
448448
host: "cloud.tencent.com"
449449
password: "shadow_tls_password"
450450
version: 2 # support 1/2/3
451+
# alpn: ["h2","http/1.1"]
451452

452453
- name: "ss5"
453454
type: ss
@@ -1179,6 +1180,15 @@ listeners:
11791180
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
11801181
password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=
11811182
cipher: 2022-blake3-aes-256-gcm
1183+
# shadow-tls:
1184+
# enable: false # 设置为true时开启
1185+
# version: 3 # 支持v1/v2/v3
1186+
# password: password # v2设置项
1187+
# users: # v3设置项
1188+
# - name: 1
1189+
# password: password
1190+
# handshake:
1191+
# dest: test.com:443
11821192

11831193
- name: vmess-in-1
11841194
type: vmess

listener/config/shadowsocks.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ShadowsocksServer struct {
1313
Cipher string
1414
Udp bool
1515
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
16+
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
1617
}
1718

1819
func (t ShadowsocksServer) String() string {

listener/config/shadowtls.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package config
2+
3+
type ShadowTLS struct {
4+
Enable bool
5+
Version int
6+
Password string
7+
Users []ShadowTLSUser
8+
Handshake ShadowTLSHandshakeOptions
9+
HandshakeForServerName map[string]ShadowTLSHandshakeOptions
10+
StrictMode bool
11+
WildcardSNI string
12+
}
13+
14+
type ShadowTLSUser struct {
15+
Name string
16+
Password string
17+
}
18+
19+
type ShadowTLSHandshakeOptions struct {
20+
Dest string
21+
Proxy string
22+
}

listener/inbound/common_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
N "github.com/metacubex/mihomo/common/net"
1818
"github.com/metacubex/mihomo/common/utils"
1919
"github.com/metacubex/mihomo/component/ca"
20+
"github.com/metacubex/mihomo/component/dialer"
2021
"github.com/metacubex/mihomo/component/generater"
2122
C "github.com/metacubex/mihomo/constant"
2223

@@ -36,6 +37,7 @@ var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "")
3637
var realityPrivateKey, realityPublickey string
3738
var realityDest = "itunes.apple.com"
3839
var realityShortid = "10f897e26c4b9478"
40+
var realityRealDial = false
3941

4042
func init() {
4143
rand.Read(httpData)
@@ -205,6 +207,14 @@ func NewHttpTestTunnel() *TestTunnel {
205207
if metadata.DstPort == 443 {
206208
tlsConn := tls.Server(c, tlsConfig.Clone())
207209
if metadata.Host == realityDest { // ignore the tls handshake error for realityDest
210+
if realityRealDial {
211+
rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
212+
if err != nil {
213+
panic(err)
214+
}
215+
N.Relay(rconn, tlsConn)
216+
return
217+
}
208218
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
209219
defer cancel()
210220
if err := tlsConn.HandshakeContext(ctx); err != nil {

listener/inbound/shadowsocks.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type ShadowSocksOption struct {
1515
Cipher string `inbound:"cipher"`
1616
UDP bool `inbound:"udp,omitempty"`
1717
MuxOption MuxOption `inbound:"mux-option,omitempty"`
18+
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
1819
}
1920

2021
func (o ShadowSocksOption) Equal(config C.InboundConfig) bool {
@@ -43,6 +44,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) {
4344
Cipher: options.Cipher,
4445
Udp: options.UDP,
4546
MuxOption: options.MuxOption.Build(),
47+
ShadowTLS: options.ShadowTLS.Build(),
4648
},
4749
}, nil
4850
}

listener/inbound/shadowsocks_test.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package inbound_test
33
import (
44
"crypto/rand"
55
"encoding/base64"
6+
"net"
67
"net/netip"
78
"strings"
89
"testing"
910

1011
"github.com/metacubex/mihomo/adapter/outbound"
1112
"github.com/metacubex/mihomo/listener/inbound"
13+
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
1214

1315
shadowsocks "github.com/metacubex/sing-shadowsocks"
1416
"github.com/metacubex/sing-shadowsocks/shadowaead"
@@ -18,22 +20,25 @@ import (
1820
)
1921

2022
var shadowsocksCipherList = []string{shadowsocks.MethodNone}
23+
var shadowsocksCipherListShort = []string{shadowsocks.MethodNone}
2124
var shadowsocksPassword32 string
2225
var shadowsocksPassword16 string
2326

2427
func init() {
2528
shadowsocksCipherList = append(shadowsocksCipherList, shadowaead.List...)
2629
shadowsocksCipherList = append(shadowsocksCipherList, shadowaead_2022.List...)
2730
shadowsocksCipherList = append(shadowsocksCipherList, shadowstream.List...)
31+
shadowsocksCipherListShort = append(shadowsocksCipherListShort, shadowaead.List[0])
32+
shadowsocksCipherListShort = append(shadowsocksCipherListShort, shadowaead_2022.List[0])
2833
passwordBytes := make([]byte, 32)
2934
rand.Read(passwordBytes)
3035
shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes)
3136
shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16])
3237
}
3338

34-
func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) {
39+
func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherList []string) {
3540
t.Parallel()
36-
for _, cipher := range shadowsocksCipherList {
41+
for _, cipher := range cipherList {
3742
cipher := cipher
3843
t.Run(cipher, func(t *testing.T) {
3944
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
@@ -94,5 +99,53 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt
9499
func TestInboundShadowSocks_Basic(t *testing.T) {
95100
inboundOptions := inbound.ShadowSocksOption{}
96101
outboundOptions := outbound.ShadowSocksOption{}
97-
testInboundShadowSocks(t, inboundOptions, outboundOptions)
102+
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherList)
103+
}
104+
105+
func TestInboundShadowSocks_ShadowTlsv1(t *testing.T) {
106+
inboundOptions := inbound.ShadowSocksOption{
107+
ShadowTLS: inbound.ShadowTLS{
108+
Enable: true,
109+
Version: 1,
110+
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
111+
},
112+
}
113+
outboundOptions := outbound.ShadowSocksOption{
114+
Plugin: shadowtls.Mode,
115+
PluginOpts: map[string]any{"host": realityDest, "fingerprint": tlsFingerprint, "version": 1},
116+
}
117+
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort)
118+
}
119+
120+
func TestInboundShadowSocks_ShadowTlsv2(t *testing.T) {
121+
inboundOptions := inbound.ShadowSocksOption{
122+
ShadowTLS: inbound.ShadowTLS{
123+
Enable: true,
124+
Version: 2,
125+
Password: shadowsocksPassword16,
126+
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
127+
},
128+
}
129+
outboundOptions := outbound.ShadowSocksOption{
130+
Plugin: shadowtls.Mode,
131+
PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 2},
132+
}
133+
outboundOptions.PluginOpts["alpn"] = []string{"http/1.1"} // shadowtls v2 work confuse with http/2 server, so we set alpn to http/1.1 to pass the test
134+
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort)
135+
}
136+
137+
func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) {
138+
inboundOptions := inbound.ShadowSocksOption{
139+
ShadowTLS: inbound.ShadowTLS{
140+
Enable: true,
141+
Version: 3,
142+
Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}},
143+
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
144+
},
145+
}
146+
outboundOptions := outbound.ShadowSocksOption{
147+
Plugin: shadowtls.Mode,
148+
PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3},
149+
}
150+
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort)
98151
}

listener/inbound/shadowtls.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package inbound
2+
3+
import (
4+
"github.com/metacubex/mihomo/common/utils"
5+
LC "github.com/metacubex/mihomo/listener/config"
6+
)
7+
8+
type ShadowTLS struct {
9+
Enable bool `inbound:"enable"`
10+
Version int `inbound:"version,omitempty"`
11+
Password string `inbound:"password,omitempty"`
12+
Users []ShadowTLSUser `inbound:"users,omitempty"`
13+
Handshake ShadowTLSHandshakeOptions `inbound:"handshake,omitempty"`
14+
HandshakeForServerName map[string]ShadowTLSHandshakeOptions `inbound:"handshake-for-server-name,omitempty"`
15+
StrictMode bool `inbound:"strict-mode,omitempty"`
16+
WildcardSNI string `inbound:"wildcard-sni,omitempty"`
17+
}
18+
19+
type ShadowTLSUser struct {
20+
Name string `inbound:"name,omitempty"`
21+
Password string `inbound:"password,omitempty"`
22+
}
23+
24+
type ShadowTLSHandshakeOptions struct {
25+
Dest string `inbound:"dest"`
26+
Proxy string `inbound:"proxy,omitempty"`
27+
}
28+
29+
func (c ShadowTLS) Build() LC.ShadowTLS {
30+
handshakeForServerName := make(map[string]LC.ShadowTLSHandshakeOptions)
31+
for k, v := range c.HandshakeForServerName {
32+
handshakeForServerName[k] = v.Build()
33+
}
34+
return LC.ShadowTLS{
35+
Enable: c.Enable,
36+
Version: c.Version,
37+
Password: c.Password,
38+
Users: utils.Map(c.Users, ShadowTLSUser.Build),
39+
Handshake: c.Handshake.Build(),
40+
HandshakeForServerName: handshakeForServerName,
41+
StrictMode: c.StrictMode,
42+
WildcardSNI: c.WildcardSNI,
43+
}
44+
}
45+
46+
func (c ShadowTLSUser) Build() LC.ShadowTLSUser {
47+
return LC.ShadowTLSUser{
48+
Name: c.Name,
49+
Password: c.Password,
50+
}
51+
}
52+
53+
func (c ShadowTLSHandshakeOptions) Build() LC.ShadowTLSHandshakeOptions {
54+
return LC.ShadowTLSHandshakeOptions{
55+
Dest: c.Dest,
56+
Proxy: c.Proxy,
57+
}
58+
}

listener/sing/dialer.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package sing
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
8+
C "github.com/metacubex/mihomo/constant"
9+
"github.com/metacubex/mihomo/listener/inner"
10+
11+
M "github.com/sagernet/sing/common/metadata"
12+
N "github.com/sagernet/sing/common/network"
13+
)
14+
15+
type Dialer struct {
16+
t C.Tunnel
17+
proxy string
18+
}
19+
20+
func (d Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
21+
if network != "tcp" && network != "tcp4" && network != "tcp6" {
22+
return nil, fmt.Errorf("unsupported network %s", network)
23+
}
24+
return inner.HandleTcp(d.t, destination.String(), d.proxy)
25+
}
26+
27+
func (d Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
28+
return nil, fmt.Errorf("unsupported ListenPacket")
29+
}
30+
31+
var _ N.Dialer = (*Dialer)(nil)
32+
33+
func NewDialer(t C.Tunnel, proxy string) (d *Dialer) {
34+
return &Dialer{t, proxy}
35+
}

0 commit comments

Comments
 (0)