mirror of
https://gitclone.com/github.com/MetaCubeX/Clash.Meta
synced 2025-05-17 23:48:10 +08:00
feat: inbound support shadow-tls
This commit is contained in:
parent
52ad793d11
commit
99aa1b0de1
@ -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{}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
22
listener/config/shadowtls.go
Normal file
22
listener/config/shadowtls.go
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
58
listener/inbound/shadowtls.go
Normal file
58
listener/inbound/shadowtls.go
Normal 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
35
listener/sing/dialer.go
Normal 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}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user