mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-10-10 12:13:10 +08:00
feat: support jls for shadowsocks outbound and listener
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/ntp"
|
||||
gost "github.com/metacubex/mihomo/transport/gost"
|
||||
"github.com/metacubex/mihomo/transport/jls"
|
||||
"github.com/metacubex/mihomo/transport/kcptun"
|
||||
"github.com/metacubex/mihomo/transport/restls"
|
||||
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
|
||||
@@ -35,6 +36,7 @@ type ShadowSocks struct {
|
||||
gostOption *gost.Option
|
||||
shadowTLSOption *shadowtls.ShadowTLSOption
|
||||
restlsConfig *restls.Config
|
||||
jlsConfig *jls.ClientConfig
|
||||
kcptunClient *kcptun.Client
|
||||
}
|
||||
|
||||
@@ -113,6 +115,13 @@ type restlsOption struct {
|
||||
ForceTLS12 bool `obfs:"force-tls12,omitempty"` // for test
|
||||
}
|
||||
|
||||
type jlsOption struct {
|
||||
Host string `obfs:"host"`
|
||||
Username string `obfs:"username"`
|
||||
Password string `obfs:"password"`
|
||||
ALPN []string `obfs:"alpn,omitempty"`
|
||||
}
|
||||
|
||||
type kcpTunOption struct {
|
||||
Key string `obfs:"key,omitempty"`
|
||||
Crypt string `obfs:"crypt,omitempty"`
|
||||
@@ -173,6 +182,12 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
|
||||
return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err)
|
||||
}
|
||||
useEarly = true
|
||||
case jls.Mode:
|
||||
c, err = jls.NewClient(ctx, c, ss.jlsConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s (jls) connect error: %w", ss.addr, err)
|
||||
}
|
||||
useEarly = true
|
||||
}
|
||||
useEarly = useEarly || N.NeedHandshake(c)
|
||||
if !useEarly {
|
||||
@@ -298,6 +313,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
var obfsOption *simpleObfsOption
|
||||
var shadowTLSOpt *shadowtls.ShadowTLSOption
|
||||
var restlsConfig *restls.Config
|
||||
var jlsConfig *jls.ClientConfig
|
||||
var kcptunClient *kcptun.Client
|
||||
obfsMode := ""
|
||||
|
||||
@@ -424,6 +440,16 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
restls.SetNameCertVerify(restlsConfig, restlsOpt.NameCertVerify)
|
||||
}
|
||||
restlsConfig.ForceTLS12 = restlsOpt.ForceTLS12
|
||||
} else if option.Plugin == jls.Mode {
|
||||
obfsMode = jls.Mode
|
||||
jlsOpt := &jlsOption{}
|
||||
if err := decoder.Decode(option.PluginOpts, jlsOpt); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize jls-plugin error: %w", addr, err)
|
||||
}
|
||||
jlsConfig, err = jls.NewClientConfig(jlsOpt.Host, jlsOpt.Username, jlsOpt.Password, jlsOpt.ALPN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize jls-plugin error: %w", addr, err)
|
||||
}
|
||||
} else if option.Plugin == kcptun.Mode {
|
||||
obfsMode = kcptun.Mode
|
||||
kcptunOpt := &kcpTunOption{}
|
||||
@@ -490,6 +516,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
obfsOption: obfsOption,
|
||||
shadowTLSOption: shadowTLSOpt,
|
||||
restlsConfig: restlsConfig,
|
||||
jlsConfig: jlsConfig,
|
||||
kcptunClient: kcptunClient,
|
||||
}
|
||||
outbound.dialer = option.NewDialer(outbound.DialOptions())
|
||||
|
||||
@@ -536,6 +536,19 @@ proxies: # socks5
|
||||
# headers:
|
||||
# custom: value
|
||||
|
||||
- name: "ss-jls"
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "password"
|
||||
plugin: jls
|
||||
plugin-opts:
|
||||
host: "www.example.com"
|
||||
username: "jls-user"
|
||||
password: "jls-password"
|
||||
# alpn: [h2, http/1.1]
|
||||
|
||||
- name: "gost-relay-hop"
|
||||
type: gost-relay
|
||||
# Dynamic mode: relay connects to the target address requested by the upper proxy.
|
||||
@@ -1944,6 +1957,16 @@ listeners:
|
||||
# restls-script: ""
|
||||
# min-record-len: 0
|
||||
# proxy: ""
|
||||
# jls-config: # 仅封装 TCP;JLS 认证失败或普通 TLS 连接会透明回落到 dest
|
||||
# enable: false
|
||||
# users:
|
||||
# - username: jls-user
|
||||
# password: jls-password
|
||||
# dest: www.example.com:443
|
||||
# # sni: www.example.com # 留空时从 dest 推导
|
||||
# # alpn: [h2, http/1.1]
|
||||
# # proxy: ""
|
||||
# # rate-limit: 0 # 转发限速,单位 bit/s,0 表示不限速
|
||||
# kcp-tun:
|
||||
# enable: false
|
||||
# key: it's a secrect # pre-shared secret between client and server
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
github.com/metacubex/http v0.1.6
|
||||
github.com/metacubex/jls-quic-go v0.0.0-20260712113821-d34e6f9b4c7f
|
||||
github.com/metacubex/jls-tls v0.0.0-20260712085721-6ea48ea9f069
|
||||
github.com/metacubex/jls-tls v0.0.0-20260712171131-15870a03ac51
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
|
||||
github.com/metacubex/mhurl v0.1.0
|
||||
github.com/metacubex/mlkem v0.1.0
|
||||
|
||||
@@ -133,8 +133,8 @@ github.com/metacubex/http v0.1.6 h1:xvXuvXMCMxCWMF5nEJF4yiKvXL+p2atWMzs37e80m1I=
|
||||
github.com/metacubex/http v0.1.6/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
|
||||
github.com/metacubex/jls-quic-go v0.0.0-20260712113821-d34e6f9b4c7f h1:ywMFrpfRTU6oiRYhn2PGefm/RWdtdwsD13sDFmrJ6+s=
|
||||
github.com/metacubex/jls-quic-go v0.0.0-20260712113821-d34e6f9b4c7f/go.mod h1:fXVJbX1dv67OpH+jGaecl7DYRj6KDLwOya8OSUPBIxo=
|
||||
github.com/metacubex/jls-tls v0.0.0-20260712085721-6ea48ea9f069 h1:oZe46oTeFmoVsBSjGJRwSg7sD4k1EU3DnwoukKiu+bY=
|
||||
github.com/metacubex/jls-tls v0.0.0-20260712085721-6ea48ea9f069/go.mod h1:mmqs889W/TqPlfNRDa2UyJvRiLyiTJIEnWHkcj3SKB8=
|
||||
github.com/metacubex/jls-tls v0.0.0-20260712171131-15870a03ac51 h1:xTFfQ+fybBXCl8mkp6ojGp+tCrfIa2ZEqSrx+4unTVM=
|
||||
github.com/metacubex/jls-tls v0.0.0-20260712171131-15870a03ac51/go.mod h1:mmqs889W/TqPlfNRDa2UyJvRiLyiTJIEnWHkcj3SKB8=
|
||||
github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df h1:S0vBzqjXok24VopstOgPd1JdgglW9tXehrqvwpQWbQ8=
|
||||
github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df/go.mod h1:F4sVXat6QjPXkNsKRDyyG3BhSkxPFFnRPEIwmmyCgbg=
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package config
|
||||
|
||||
type JLS struct {
|
||||
Enable bool
|
||||
Users []JLSUser
|
||||
SNI string
|
||||
Dest string
|
||||
ALPN []string
|
||||
Proxy string
|
||||
RateLimit uint64
|
||||
}
|
||||
|
||||
type JLSUser struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type ShadowsocksServer struct {
|
||||
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
|
||||
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
|
||||
ResTLS ResTLS `yaml:"res-tls" json:"res-tls,omitempty"`
|
||||
JLSConfig JLS `yaml:"jls-config" json:"jls-config,omitempty"`
|
||||
KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"`
|
||||
SimpleObfs SimpleObfs `yaml:"simple-obfs" json:"simple-obfs,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
)
|
||||
|
||||
type JLS struct {
|
||||
Enable bool `inbound:"enable"`
|
||||
Users []JLSUser `inbound:"users"`
|
||||
SNI string `inbound:"sni,omitempty"`
|
||||
Dest string `inbound:"dest"`
|
||||
ALPN []string `inbound:"alpn,omitempty"`
|
||||
Proxy string `inbound:"proxy,omitempty"`
|
||||
RateLimit uint64 `inbound:"rate-limit,omitempty"`
|
||||
}
|
||||
|
||||
type JLSUser struct {
|
||||
Username string `inbound:"username"`
|
||||
Password string `inbound:"password"`
|
||||
}
|
||||
|
||||
func (j JLS) Build() LC.JLS {
|
||||
return LC.JLS{
|
||||
Enable: j.Enable,
|
||||
Users: utils.Map(j.Users, JLSUser.Build),
|
||||
SNI: j.SNI,
|
||||
Dest: j.Dest,
|
||||
ALPN: append([]string(nil), j.ALPN...),
|
||||
Proxy: j.Proxy,
|
||||
RateLimit: j.RateLimit,
|
||||
}
|
||||
}
|
||||
|
||||
func (u JLSUser) Build() LC.JLSUser {
|
||||
return LC.JLSUser{
|
||||
Username: u.Username,
|
||||
Password: u.Password,
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type ShadowSocksOption struct {
|
||||
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
||||
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
|
||||
ResTLS ResTLS `inbound:"res-tls,omitempty"`
|
||||
JLSConfig JLS `inbound:"jls-config,omitempty"`
|
||||
KcpTun KcpTun `inbound:"kcp-tun,omitempty"`
|
||||
SimpleObfs SimpleObfs `inbound:"simple-obfs,omitempty"`
|
||||
}
|
||||
@@ -61,6 +62,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) {
|
||||
MuxOption: options.MuxOption.Build(),
|
||||
ShadowTLS: options.ShadowTLS.Build(),
|
||||
ResTLS: options.ResTLS.Build(),
|
||||
JLSConfig: options.JLSConfig.Build(),
|
||||
KcpTun: options.KcpTun.Build(),
|
||||
SimpleObfs: options.SimpleObfs.Build(),
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/outbound"
|
||||
"github.com/metacubex/mihomo/listener/inbound"
|
||||
"github.com/metacubex/mihomo/transport/jls"
|
||||
"github.com/metacubex/mihomo/transport/kcptun"
|
||||
"github.com/metacubex/mihomo/transport/restls"
|
||||
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
|
||||
@@ -199,6 +200,28 @@ func TestInboundShadowSocks_Restls_tls13(t *testing.T) {
|
||||
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
|
||||
}
|
||||
|
||||
func TestInboundShadowSocks_JLS(t *testing.T) {
|
||||
username := "jls-user"
|
||||
password := "jls-password"
|
||||
inboundOptions := inbound.ShadowSocksOption{
|
||||
JLSConfig: inbound.JLS{
|
||||
Enable: true,
|
||||
Users: []inbound.JLSUser{{Username: username, Password: password}},
|
||||
SNI: realityDest,
|
||||
Dest: net.JoinHostPort(realityDest, "443"),
|
||||
},
|
||||
}
|
||||
outboundOptions := outbound.ShadowSocksOption{
|
||||
Plugin: jls.Mode,
|
||||
PluginOpts: map[string]any{
|
||||
"host": realityDest,
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
|
||||
}
|
||||
|
||||
func TestInboundShadowSocks_SimpleObfs_Http(t *testing.T) {
|
||||
inboundOptions := inbound.ShadowSocksOption{
|
||||
SimpleObfs: inbound.SimpleObfs{
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/inner"
|
||||
"github.com/metacubex/mihomo/listener/sing"
|
||||
"github.com/metacubex/mihomo/transport/jls"
|
||||
"github.com/metacubex/mihomo/transport/restls"
|
||||
"github.com/metacubex/mihomo/transport/shadowsocks/core"
|
||||
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
|
||||
@@ -26,6 +27,7 @@ type Listener struct {
|
||||
pickCipher core.Cipher
|
||||
handler *sing.ListenerHandler
|
||||
resTLS *restls.ServerConfig
|
||||
jls *jls.ServerConfig
|
||||
simpleObfs func(net.Conn) net.Conn
|
||||
}
|
||||
|
||||
@@ -62,6 +64,26 @@ func New(config LC.ShadowsocksServer, lc C.InboundListenConfig, tunnel C.Tunnel,
|
||||
}
|
||||
}
|
||||
|
||||
if config.JLSConfig.Enable {
|
||||
users := make([]jls.User, len(config.JLSConfig.Users))
|
||||
for i, user := range config.JLSConfig.Users {
|
||||
users[i] = jls.User{Username: user.Username, Password: user.Password}
|
||||
}
|
||||
sl.jls, err = jls.NewServerConfig(
|
||||
config.JLSConfig.SNI,
|
||||
config.JLSConfig.Dest,
|
||||
users,
|
||||
config.JLSConfig.ALPN,
|
||||
config.JLSConfig.RateLimit,
|
||||
func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return inner.HandleTcp(tunnel, address, config.JLSConfig.Proxy)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if config.SimpleObfs.Enable {
|
||||
switch config.SimpleObfs.Mode {
|
||||
case "http":
|
||||
@@ -141,6 +163,14 @@ func (l *Listener) AddrList() (addrList []net.Addr) {
|
||||
}
|
||||
|
||||
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
if l.jls != nil {
|
||||
c, err := jls.Server(context.TODO(), conn, l.jls)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
conn = c
|
||||
}
|
||||
if l.resTLS != nil {
|
||||
c, err := restls.Server(context.TODO(), conn, l.resTLS)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/metacubex/mihomo/listener/sing"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/ntp"
|
||||
"github.com/metacubex/mihomo/transport/jls"
|
||||
"github.com/metacubex/mihomo/transport/kcptun"
|
||||
"github.com/metacubex/mihomo/transport/restls"
|
||||
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
|
||||
@@ -38,6 +39,7 @@ type Listener struct {
|
||||
service shadowsocks.Service
|
||||
shadowTLS *shadowtls.Service
|
||||
resTLS *restls.ServerConfig
|
||||
jls *jls.ServerConfig
|
||||
simpleObfs func(net.Conn) net.Conn
|
||||
}
|
||||
|
||||
@@ -156,6 +158,24 @@ func New(config LC.ShadowsocksServer, lc C.InboundListenConfig, tunnel C.Tunnel,
|
||||
}
|
||||
}
|
||||
|
||||
if config.JLSConfig.Enable {
|
||||
sl.jls, err = jls.NewServerConfig(
|
||||
config.JLSConfig.SNI,
|
||||
config.JLSConfig.Dest,
|
||||
common.Map(config.JLSConfig.Users, func(user LC.JLSUser) jls.User {
|
||||
return jls.User{Username: user.Username, Password: user.Password}
|
||||
}),
|
||||
config.JLSConfig.ALPN,
|
||||
config.JLSConfig.RateLimit,
|
||||
func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return inner.HandleTcp(tunnel, address, config.JLSConfig.Proxy)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if config.SimpleObfs.Enable {
|
||||
switch config.SimpleObfs.Mode {
|
||||
case "http":
|
||||
@@ -296,6 +316,14 @@ func (l *Listener) AddrList() (addrList []net.Addr) {
|
||||
|
||||
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
ctx := sing.WithAdditions(context.TODO(), additions...)
|
||||
if l.jls != nil {
|
||||
c, err := jls.Server(context.TODO(), conn, l.jls)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
conn = c
|
||||
}
|
||||
if l.resTLS != nil {
|
||||
c, err := restls.Server(context.TODO(), conn, l.resTLS)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
package jls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
"github.com/metacubex/mihomo/ntp"
|
||||
|
||||
tls "github.com/metacubex/jls-tls"
|
||||
)
|
||||
|
||||
const (
|
||||
Mode = "jls"
|
||||
bitsPerByte = 8
|
||||
rateLimitCycle = 10 * time.Millisecond
|
||||
maxRateLimitBurstBytes = 64 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultALPN = []string{"h2", "http/1.1"}
|
||||
ErrFallbackCompleted = errors.New("jls: connection relayed to fallback")
|
||||
)
|
||||
|
||||
type User = tls.JLSUser
|
||||
|
||||
type ClientConfig struct {
|
||||
ServerName string
|
||||
User User
|
||||
ALPN []string
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
TLSConfig *tls.Config
|
||||
Dest string
|
||||
RateLimit uint64
|
||||
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
*tls.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn.NetConn()
|
||||
}
|
||||
|
||||
func NewClientConfig(serverName, username, password string, alpn []string) (*ClientConfig, error) {
|
||||
if serverName == "" {
|
||||
return nil, errors.New("jls: server name is required")
|
||||
}
|
||||
if username == "" {
|
||||
return nil, errors.New("jls: username is required")
|
||||
}
|
||||
if password == "" {
|
||||
return nil, errors.New("jls: password is required")
|
||||
}
|
||||
if alpn == nil {
|
||||
alpn = DefaultALPN
|
||||
}
|
||||
return &ClientConfig{
|
||||
ServerName: serverName,
|
||||
User: User{Username: username, Password: password},
|
||||
ALPN: append([]string(nil), alpn...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Conn, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("jls: nil client config")
|
||||
}
|
||||
tlsConn := tls.Client(conn, &tls.Config{
|
||||
ServerName: config.ServerName,
|
||||
NextProtos: append([]string(nil), config.ALPN...),
|
||||
RootCAs: ca.GetCertPool(),
|
||||
Time: ntp.Now,
|
||||
JLSConfig: &tls.JLSConfig{
|
||||
Enable: true,
|
||||
User: config.User,
|
||||
},
|
||||
})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !tlsConn.ConnectionState().JLS.Authenticated {
|
||||
return nil, tls.ErrJLSAuthFailed
|
||||
}
|
||||
return &Conn{Conn: tlsConn}, nil
|
||||
}
|
||||
|
||||
func NewServerConfig(sni, dest string, users []User, alpn []string, rateLimit uint64, dialContext func(context.Context, string, string) (net.Conn, error)) (*ServerConfig, error) {
|
||||
if dest == "" {
|
||||
return nil, errors.New("jls: dest is required")
|
||||
}
|
||||
destHost, _, err := net.SplitHostPort(dest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jls: invalid dest address: %w", err)
|
||||
}
|
||||
if sni == "" {
|
||||
sni = destHost
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return nil, errors.New("jls: at least one user is required")
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.Username == "" {
|
||||
return nil, errors.New("jls: username is required")
|
||||
}
|
||||
if user.Password == "" {
|
||||
return nil, errors.New("jls: password is required")
|
||||
}
|
||||
}
|
||||
if dialContext == nil {
|
||||
return nil, errors.New("jls: dial context is required")
|
||||
}
|
||||
if alpn == nil {
|
||||
alpn = DefaultALPN
|
||||
}
|
||||
// JLS authenticates the peer, so this generated certificate only carries the TLS handshake.
|
||||
certificatePEM, privateKeyPEM, _, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jls: generate TLS certificate: %w", err)
|
||||
}
|
||||
certificate, err := tls.X509KeyPair([]byte(certificatePEM), []byte(privateKeyPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jls: parse TLS certificate: %w", err)
|
||||
}
|
||||
return &ServerConfig{
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
NextProtos: append([]string(nil), alpn...),
|
||||
MinVersion: tls.VersionTLS13,
|
||||
Time: ntp.Now,
|
||||
JLSConfig: &tls.JLSConfig{
|
||||
Enable: true,
|
||||
Users: append([]User(nil), users...),
|
||||
ServerName: sni,
|
||||
},
|
||||
},
|
||||
Dest: dest,
|
||||
RateLimit: rateLimit,
|
||||
DialContext: dialContext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Server(ctx context.Context, conn net.Conn, config *ServerConfig) (net.Conn, error) {
|
||||
if config == nil || config.TLSConfig == nil {
|
||||
return nil, errors.New("jls: nil server config")
|
||||
}
|
||||
recorder := &handshakeRecorderConn{Conn: conn, recording: true}
|
||||
tlsConn := tls.Server(recorder, config.TLSConfig.Clone())
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
// Forwarding after authentication or a local write would mix two server handshakes.
|
||||
if tlsConn.ConnectionState().JLS.Authenticated || recorder.wroteToClient() {
|
||||
recorder.discard()
|
||||
return nil, err
|
||||
}
|
||||
return nil, relayFallback(ctx, conn, recorder.stop(), config)
|
||||
}
|
||||
recorder.discard()
|
||||
if !tlsConn.ConnectionState().JLS.Authenticated {
|
||||
return nil, tls.ErrJLSAuthFailed
|
||||
}
|
||||
return &Conn{Conn: tlsConn}, nil
|
||||
}
|
||||
|
||||
func relayFallback(ctx context.Context, inbound net.Conn, prefix []byte, config *ServerConfig) error {
|
||||
upstream, err := config.DialContext(ctx, "tcp", config.Dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inbound = N.NewCachedConn(inbound, prefix)
|
||||
upstream = newRateLimitedConn(upstream, config.RateLimit)
|
||||
N.Relay(inbound, upstream)
|
||||
return ErrFallbackCompleted
|
||||
}
|
||||
|
||||
type rateLimitedConn struct {
|
||||
net.Conn
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
readLimiter *bitRateLimiter
|
||||
writeLimiter *bitRateLimiter
|
||||
burst int
|
||||
}
|
||||
|
||||
func newRateLimitedConn(conn net.Conn, rateBps uint64) net.Conn {
|
||||
if rateBps == 0 {
|
||||
return conn
|
||||
}
|
||||
burst := rateBps / bitsPerByte / uint64(time.Second/rateLimitCycle)
|
||||
if burst == 0 {
|
||||
burst = 1
|
||||
} else if burst > maxRateLimitBurstBytes {
|
||||
burst = maxRateLimitBurstBytes
|
||||
}
|
||||
limitCtx, cancel := context.WithCancel(context.Background())
|
||||
return &rateLimitedConn{
|
||||
Conn: conn,
|
||||
ctx: limitCtx,
|
||||
cancel: cancel,
|
||||
readLimiter: &bitRateLimiter{rateBps: rateBps},
|
||||
writeLimiter: &bitRateLimiter{rateBps: rateBps},
|
||||
burst: int(burst),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rateLimitedConn) Read(p []byte) (n int, err error) {
|
||||
if len(p) > c.burst {
|
||||
p = p[:c.burst]
|
||||
}
|
||||
n, err = c.Conn.Read(p)
|
||||
if n > 0 {
|
||||
if limitErr := c.readLimiter.WaitN(c.ctx, n); err == nil {
|
||||
err = limitErr
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rateLimitedConn) Write(p []byte) (n int, err error) {
|
||||
for len(p) > 0 {
|
||||
chunkSize := len(p)
|
||||
if chunkSize > c.burst {
|
||||
chunkSize = c.burst
|
||||
}
|
||||
if err = c.writeLimiter.WaitN(c.ctx, chunkSize); err != nil {
|
||||
return n, err
|
||||
}
|
||||
var written int
|
||||
written, err = c.Conn.Write(p[:chunkSize])
|
||||
n += written
|
||||
p = p[written:]
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if written != chunkSize {
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *rateLimitedConn) Close() error {
|
||||
c.cancel()
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *rateLimitedConn) CloseWrite() error {
|
||||
if conn, ok := c.Conn.(interface{ CloseWrite() error }); ok {
|
||||
return conn.CloseWrite()
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
type bitRateLimiter struct {
|
||||
mu sync.Mutex
|
||||
rateBps uint64
|
||||
next time.Time
|
||||
}
|
||||
|
||||
func (l *bitRateLimiter) WaitN(ctx context.Context, n int) error {
|
||||
delay := l.reserveN(time.Now(), n)
|
||||
if delay <= 0 {
|
||||
return nil
|
||||
}
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *bitRateLimiter) reserveN(now time.Time, n int) time.Duration {
|
||||
interval := time.Duration(uint64(n) * bitsPerByte * uint64(time.Second) / l.rateBps)
|
||||
|
||||
l.mu.Lock()
|
||||
ready := l.next
|
||||
if ready.Before(now) {
|
||||
ready = now
|
||||
}
|
||||
l.next = ready.Add(interval)
|
||||
l.mu.Unlock()
|
||||
return ready.Sub(now)
|
||||
}
|
||||
|
||||
type handshakeRecorderConn struct {
|
||||
net.Conn
|
||||
buffer bytes.Buffer
|
||||
recording bool
|
||||
wrote bool
|
||||
}
|
||||
|
||||
func (c *handshakeRecorderConn) Read(p []byte) (int, error) {
|
||||
n, err := c.Conn.Read(p)
|
||||
if c.recording && n > 0 {
|
||||
_, _ = c.buffer.Write(p[:n])
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *handshakeRecorderConn) Write(p []byte) (int, error) {
|
||||
n, err := c.Conn.Write(p)
|
||||
if c.recording && n > 0 {
|
||||
c.wrote = true
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *handshakeRecorderConn) stop() []byte {
|
||||
c.recording = false
|
||||
data := append([]byte(nil), c.buffer.Bytes()...)
|
||||
c.buffer.Reset()
|
||||
return data
|
||||
}
|
||||
|
||||
func (c *handshakeRecorderConn) discard() {
|
||||
c.recording = false
|
||||
c.buffer.Reset()
|
||||
}
|
||||
|
||||
func (c *handshakeRecorderConn) wroteToClient() bool {
|
||||
return c.wrote
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package jls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
|
||||
tls "github.com/metacubex/jls-tls"
|
||||
)
|
||||
|
||||
func TestJLSClientServer(t *testing.T) {
|
||||
user := User{Username: "test-user", Password: "test-password"}
|
||||
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
|
||||
return nil, errors.New("authenticated JLS connection dialed fallback")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientConfig, err := NewClientConfig("camouflage.example", user.Username, user.Password, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serverSide, clientSide := net.Pipe()
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := Server(context.Background(), serverSide, serverConfig)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
state := conn.(*Conn).ConnectionState()
|
||||
if !state.JLS.Authenticated || state.JLS.User != user.Username {
|
||||
serverDone <- errors.New("server did not authenticate JLS user")
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(conn, conn)
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
client, err := NewClient(context.Background(), clientSide, clientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload := []byte("JLS over TCP")
|
||||
if _, err = client.Write(payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(client, response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(response, payload) {
|
||||
t.Fatalf("response = %q, want %q", response, payload)
|
||||
}
|
||||
_ = client.Close()
|
||||
if err = <-serverDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerConfigRequiresDialContext(t *testing.T) {
|
||||
_, err := NewServerConfig(
|
||||
"camouflage.example",
|
||||
"camouflage.example:443",
|
||||
[]User{{Username: "user", Password: "password"}},
|
||||
nil,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
if err == nil || err.Error() != "jls: dial context is required" {
|
||||
t.Fatalf("error = %v, want dial context required error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJLSServerFallback(t *testing.T) {
|
||||
for _, version := range []uint16{tls.VersionTLS13, tls.VersionTLS12} {
|
||||
t.Run(tls.VersionName(version), func(t *testing.T) {
|
||||
testJLSServerFallback(t, version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testJLSServerFallback(t *testing.T, version uint16) {
|
||||
upstreamConfig := newTestTLSServerConfig(t, version)
|
||||
upstreamClient, upstreamServer := net.Pipe()
|
||||
upstreamDone := make(chan error, 1)
|
||||
go func() {
|
||||
conn := tls.Server(upstreamServer, upstreamConfig)
|
||||
if err := conn.Handshake(); err != nil {
|
||||
upstreamDone <- err
|
||||
return
|
||||
}
|
||||
_, err := io.Copy(conn, conn)
|
||||
upstreamDone <- err
|
||||
}()
|
||||
|
||||
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{{Username: "user", Password: "password"}}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
|
||||
return upstreamClient, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverSide, clientSide := net.Pipe()
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverSide, serverConfig)
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
client := tls.Client(clientSide, &tls.Config{
|
||||
ServerName: "camouflage.example",
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: version,
|
||||
MaxVersion: version,
|
||||
})
|
||||
if err = client.Handshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload := []byte("ordinary TLS fallback")
|
||||
if _, err = client.Write(payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(client, response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(response, payload) {
|
||||
t.Fatalf("response = %q, want %q", response, payload)
|
||||
}
|
||||
_ = client.Close()
|
||||
if err = <-serverDone; !errors.Is(err, ErrFallbackCompleted) {
|
||||
t.Fatalf("server error = %v, want %v", err, ErrFallbackCompleted)
|
||||
}
|
||||
if err = <-upstreamDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJLSServerDoesNotFallbackAfterAuthentication(t *testing.T) {
|
||||
user := User{Username: "user", Password: "password"}
|
||||
fallbackDialed := false
|
||||
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
|
||||
fallbackDialed = true
|
||||
return nil, errors.New("fallback dialed")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverConfig.TLSConfig.Certificates = nil
|
||||
|
||||
clientConfig, err := NewClientConfig("camouflage.example", user.Username, user.Password, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverSide, clientSide := net.Pipe()
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverSide, serverConfig)
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
client, clientErr := NewClient(context.Background(), clientSide, clientConfig)
|
||||
if client != nil {
|
||||
_ = client.Close()
|
||||
}
|
||||
_ = clientSide.Close()
|
||||
if clientErr == nil {
|
||||
t.Fatal("client handshake unexpectedly succeeded")
|
||||
}
|
||||
if serverErr := <-serverDone; serverErr == nil {
|
||||
t.Fatal("server handshake unexpectedly succeeded")
|
||||
}
|
||||
if fallbackDialed {
|
||||
t.Fatal("authenticated handshake failure dialed fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJLSServerFallbackReplaysRejectedTLS(t *testing.T) {
|
||||
request := []byte{23, 3, 3, 0, 1, 0} // Application data before ClientHello.
|
||||
response := []byte("camouflage response")
|
||||
|
||||
upstreamClient, upstreamServer := net.Pipe()
|
||||
upstreamDone := make(chan error, 1)
|
||||
go func() {
|
||||
defer upstreamServer.Close()
|
||||
got := make([]byte, len(request))
|
||||
if _, err := io.ReadFull(upstreamServer, got); err != nil {
|
||||
upstreamDone <- err
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, request) {
|
||||
upstreamDone <- errors.New("fallback received modified handshake bytes")
|
||||
return
|
||||
}
|
||||
_, err := upstreamServer.Write(response)
|
||||
upstreamDone <- err
|
||||
}()
|
||||
|
||||
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{{Username: "user", Password: "password"}}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
|
||||
return upstreamClient, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverSide, clientSide := net.Pipe()
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := Server(context.Background(), serverSide, serverConfig)
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
if _, err = clientSide.Write(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(response))
|
||||
if _, err = io.ReadFull(clientSide, got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, response) {
|
||||
t.Fatalf("fallback response = %q, want %q", got, response)
|
||||
}
|
||||
_ = clientSide.Close()
|
||||
if err = <-serverDone; !errors.Is(err, ErrFallbackCompleted) {
|
||||
t.Fatalf("server error = %v, want %v", err, ErrFallbackCompleted)
|
||||
}
|
||||
if err = <-upstreamDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitRateLimiterReservations(t *testing.T) {
|
||||
limiter := &bitRateLimiter{rateBps: 800}
|
||||
now := time.Unix(0, 0)
|
||||
if delay := limiter.reserveN(now, 1); delay != 0 {
|
||||
t.Fatalf("initial reservation delay = %s, want 0", delay)
|
||||
}
|
||||
if delay := limiter.reserveN(now, 1); delay != 10*time.Millisecond {
|
||||
t.Fatalf("second reservation delay = %s, want 10ms", delay)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestTLSServerConfig(t *testing.T, version uint16) *tls.Config {
|
||||
t.Helper()
|
||||
certificatePEM, privateKeyPEM, _, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificate, err := tls.X509KeyPair([]byte(certificatePEM), []byte(privateKeyPEM))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
MinVersion: version,
|
||||
MaxVersion: version,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user