feat: inbound support shadow-tls

This commit is contained in:
wwqgtxx 2025-04-22 20:49:54 +08:00
parent 52ad793d11
commit 99aa1b0de1
11 changed files with 270 additions and 10 deletions

View File

@ -84,11 +84,12 @@ type gostObfsOption struct {
}
type shadowTLSOption struct {
Password string `obfs:"password,omitempty"`
Host string `obfs:"host"`
Fingerprint string `obfs:"fingerprint,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Version int `obfs:"version,omitempty"`
Password string `obfs:"password,omitempty"`
Host string `obfs:"host"`
Fingerprint string `obfs:"fingerprint,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Version int `obfs:"version,omitempty"`
ALPN []string `obfs:"alpn,omitempty"`
}
type restlsOption struct {
@ -342,6 +343,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
SkipCertVerify: opt.SkipCertVerify,
Version: opt.Version,
}
if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
shadowTLSOpt.ALPN = opt.ALPN
} else {
shadowTLSOpt.ALPN = shadowtls.DefaultALPN
}
} else if option.Plugin == restls.Mode {
obfsMode = restls.Mode
restlsOpt := &restlsOption{}

View File

@ -448,6 +448,7 @@ proxies: # socks5
host: "cloud.tencent.com"
password: "shadow_tls_password"
version: 2 # support 1/2/3
# alpn: ["h2","http/1.1"]
- name: "ss5"
type: ss
@ -1179,6 +1180,15 @@ listeners:
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=
cipher: 2022-blake3-aes-256-gcm
# shadow-tls:
# enable: false # 设置为true时开启
# version: 3 # 支持v1/v2/v3
# password: password # v2设置项
# users: # v3设置项
# - name: 1
# password: password
# handshake:
# dest: test.com:443
- name: vmess-in-1
type: vmess

View File

@ -13,6 +13,7 @@ type ShadowsocksServer struct {
Cipher string
Udp bool
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
}
func (t ShadowsocksServer) String() string {

View File

@ -0,0 +1,22 @@
package config
type ShadowTLS struct {
Enable bool
Version int
Password string
Users []ShadowTLSUser
Handshake ShadowTLSHandshakeOptions
HandshakeForServerName map[string]ShadowTLSHandshakeOptions
StrictMode bool
WildcardSNI string
}
type ShadowTLSUser struct {
Name string
Password string
}
type ShadowTLSHandshakeOptions struct {
Dest string
Proxy string
}

View File

@ -17,6 +17,7 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/generater"
C "github.com/metacubex/mihomo/constant"
@ -36,6 +37,7 @@ var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "")
var realityPrivateKey, realityPublickey string
var realityDest = "itunes.apple.com"
var realityShortid = "10f897e26c4b9478"
var realityRealDial = false
func init() {
rand.Read(httpData)
@ -205,6 +207,14 @@ func NewHttpTestTunnel() *TestTunnel {
if metadata.DstPort == 443 {
tlsConn := tls.Server(c, tlsConfig.Clone())
if metadata.Host == realityDest { // ignore the tls handshake error for realityDest
if realityRealDial {
rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
if err != nil {
panic(err)
}
N.Relay(rconn, tlsConn)
return
}
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(ctx); err != nil {

View File

@ -15,6 +15,7 @@ type ShadowSocksOption struct {
Cipher string `inbound:"cipher"`
UDP bool `inbound:"udp,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
}
func (o ShadowSocksOption) Equal(config C.InboundConfig) bool {
@ -43,6 +44,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) {
Cipher: options.Cipher,
Udp: options.UDP,
MuxOption: options.MuxOption.Build(),
ShadowTLS: options.ShadowTLS.Build(),
},
}, nil
}

View File

@ -3,12 +3,14 @@ package inbound_test
import (
"crypto/rand"
"encoding/base64"
"net"
"net/netip"
"strings"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
shadowsocks "github.com/metacubex/sing-shadowsocks"
"github.com/metacubex/sing-shadowsocks/shadowaead"
@ -18,6 +20,7 @@ import (
)
var shadowsocksCipherList = []string{shadowsocks.MethodNone}
var shadowsocksCipherListShort = []string{shadowsocks.MethodNone}
var shadowsocksPassword32 string
var shadowsocksPassword16 string
@ -25,15 +28,17 @@ func init() {
shadowsocksCipherList = append(shadowsocksCipherList, shadowaead.List...)
shadowsocksCipherList = append(shadowsocksCipherList, shadowaead_2022.List...)
shadowsocksCipherList = append(shadowsocksCipherList, shadowstream.List...)
shadowsocksCipherListShort = append(shadowsocksCipherListShort, shadowaead.List[0])
shadowsocksCipherListShort = append(shadowsocksCipherListShort, shadowaead_2022.List[0])
passwordBytes := make([]byte, 32)
rand.Read(passwordBytes)
shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes)
shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16])
}
func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) {
func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherList []string) {
t.Parallel()
for _, cipher := range shadowsocksCipherList {
for _, cipher := range cipherList {
cipher := cipher
t.Run(cipher, func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
@ -94,5 +99,53 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt
func TestInboundShadowSocks_Basic(t *testing.T) {
inboundOptions := inbound.ShadowSocksOption{}
outboundOptions := outbound.ShadowSocksOption{}
testInboundShadowSocks(t, inboundOptions, outboundOptions)
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherList)
}
func TestInboundShadowSocks_ShadowTlsv1(t *testing.T) {
inboundOptions := inbound.ShadowSocksOption{
ShadowTLS: inbound.ShadowTLS{
Enable: true,
Version: 1,
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
},
}
outboundOptions := outbound.ShadowSocksOption{
Plugin: shadowtls.Mode,
PluginOpts: map[string]any{"host": realityDest, "fingerprint": tlsFingerprint, "version": 1},
}
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort)
}
func TestInboundShadowSocks_ShadowTlsv2(t *testing.T) {
inboundOptions := inbound.ShadowSocksOption{
ShadowTLS: inbound.ShadowTLS{
Enable: true,
Version: 2,
Password: shadowsocksPassword16,
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
},
}
outboundOptions := outbound.ShadowSocksOption{
Plugin: shadowtls.Mode,
PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 2},
}
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
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort)
}
func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) {
inboundOptions := inbound.ShadowSocksOption{
ShadowTLS: inbound.ShadowTLS{
Enable: true,
Version: 3,
Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}},
Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")},
},
}
outboundOptions := outbound.ShadowSocksOption{
Plugin: shadowtls.Mode,
PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3},
}
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort)
}

View File

@ -0,0 +1,58 @@
package inbound
import (
"github.com/metacubex/mihomo/common/utils"
LC "github.com/metacubex/mihomo/listener/config"
)
type ShadowTLS struct {
Enable bool `inbound:"enable"`
Version int `inbound:"version,omitempty"`
Password string `inbound:"password,omitempty"`
Users []ShadowTLSUser `inbound:"users,omitempty"`
Handshake ShadowTLSHandshakeOptions `inbound:"handshake,omitempty"`
HandshakeForServerName map[string]ShadowTLSHandshakeOptions `inbound:"handshake-for-server-name,omitempty"`
StrictMode bool `inbound:"strict-mode,omitempty"`
WildcardSNI string `inbound:"wildcard-sni,omitempty"`
}
type ShadowTLSUser struct {
Name string `inbound:"name,omitempty"`
Password string `inbound:"password,omitempty"`
}
type ShadowTLSHandshakeOptions struct {
Dest string `inbound:"dest"`
Proxy string `inbound:"proxy,omitempty"`
}
func (c ShadowTLS) Build() LC.ShadowTLS {
handshakeForServerName := make(map[string]LC.ShadowTLSHandshakeOptions)
for k, v := range c.HandshakeForServerName {
handshakeForServerName[k] = v.Build()
}
return LC.ShadowTLS{
Enable: c.Enable,
Version: c.Version,
Password: c.Password,
Users: utils.Map(c.Users, ShadowTLSUser.Build),
Handshake: c.Handshake.Build(),
HandshakeForServerName: handshakeForServerName,
StrictMode: c.StrictMode,
WildcardSNI: c.WildcardSNI,
}
}
func (c ShadowTLSUser) Build() LC.ShadowTLSUser {
return LC.ShadowTLSUser{
Name: c.Name,
Password: c.Password,
}
}
func (c ShadowTLSHandshakeOptions) Build() LC.ShadowTLSHandshakeOptions {
return LC.ShadowTLSHandshakeOptions{
Dest: c.Dest,
Proxy: c.Proxy,
}
}

35
listener/sing/dialer.go Normal file
View File

@ -0,0 +1,35 @@
package sing
import (
"context"
"fmt"
"net"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inner"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type Dialer struct {
t C.Tunnel
proxy string
}
func (d Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != "tcp" && network != "tcp4" && network != "tcp6" {
return nil, fmt.Errorf("unsupported network %s", network)
}
return inner.HandleTcp(d.t, destination.String(), d.proxy)
}
func (d Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, fmt.Errorf("unsupported ListenPacket")
}
var _ N.Dialer = (*Dialer)(nil)
func NewDialer(t C.Tunnel, proxy string) (d *Dialer) {
return &Dialer{t, proxy}
}

View File

@ -18,6 +18,7 @@ import (
shadowsocks "github.com/metacubex/sing-shadowsocks"
"github.com/metacubex/sing-shadowsocks/shadowaead"
"github.com/metacubex/sing-shadowsocks/shadowaead_2022"
shadowtls "github.com/metacubex/sing-shadowtls"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
@ -31,10 +32,24 @@ type Listener struct {
listeners []net.Listener
udpListeners []net.PacketConn
service shadowsocks.Service
shadowTLS *shadowtls.Service
}
var _listener *Listener
// shadowTLSService is a wrapper for shadowsocks.Service to support shadowTLS.
type shadowTLSService struct {
shadowsocks.Service
shadowTLS *shadowtls.Service
}
func (s *shadowTLSService) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
if s.shadowTLS != nil {
return s.shadowTLS.NewConnection(ctx, conn, metadata)
}
return s.Service.NewConnection(ctx, conn, metadata)
}
func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) {
var sl *Listener
var err error
@ -60,7 +75,8 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
return nil, err
}
sl = &Listener{false, config, nil, nil, nil}
sl = &Listener{}
sl.config = config
switch {
case config.Cipher == shadowsocks.MethodNone:
@ -77,6 +93,51 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
return nil, err
}
if config.ShadowTLS.Enable {
buildHandshake := func(handshake LC.ShadowTLSHandshakeOptions) (handshakeConfig shadowtls.HandshakeConfig) {
handshakeConfig.Server = M.ParseSocksaddr(handshake.Dest)
handshakeConfig.Dialer = sing.NewDialer(tunnel, handshake.Proxy)
return
}
var handshakeForServerName map[string]shadowtls.HandshakeConfig
if config.ShadowTLS.Version > 1 {
handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
for serverName, serverOptions := range config.ShadowTLS.HandshakeForServerName {
handshakeForServerName[serverName] = buildHandshake(serverOptions)
}
}
var wildcardSNI shadowtls.WildcardSNI
switch config.ShadowTLS.WildcardSNI {
case "authed":
wildcardSNI = shadowtls.WildcardSNIAuthed
case "all":
wildcardSNI = shadowtls.WildcardSNIAll
default:
wildcardSNI = shadowtls.WildcardSNIOff
}
var shadowTLS *shadowtls.Service
shadowTLS, err = shadowtls.NewService(shadowtls.ServiceConfig{
Version: config.ShadowTLS.Version,
Password: config.ShadowTLS.Password,
Users: common.Map(config.ShadowTLS.Users, func(it LC.ShadowTLSUser) shadowtls.User {
return shadowtls.User{Name: it.Name, Password: it.Password}
}),
Handshake: buildHandshake(config.ShadowTLS.Handshake),
HandshakeForServerName: handshakeForServerName,
StrictMode: config.ShadowTLS.StrictMode,
WildcardSNI: wildcardSNI,
Handler: sl.service,
Logger: log.SingLogger,
})
if err != nil {
return nil, err
}
sl.service = &shadowTLSService{
Service: sl.service,
shadowTLS: shadowTLS,
}
}
for _, addr := range strings.Split(config.Listen, ",") {
addr := addr

View File

@ -28,11 +28,12 @@ type ShadowTLSOption struct {
ClientFingerprint string
SkipCertVerify bool
Version int
ALPN []string
}
func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) {
tlsConfig := &tls.Config{
NextProtos: DefaultALPN,
NextProtos: option.ALPN,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: option.SkipCertVerify,
ServerName: option.Host,