1
0
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:
wwqgtxx
2026-07-13 01:22:31 +08:00
parent a35045d9f1
commit 2b2bdf6d22
13 changed files with 792 additions and 3 deletions
+27
View File
@@ -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())
+23
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+16
View File
@@ -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
}
+1
View File
@@ -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"`
}
+40
View File
@@ -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,
}
}
+2
View File
@@ -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(),
},
+23
View File
@@ -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{
+30
View File
@@ -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 {
+28
View File
@@ -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 {
+334
View File
@@ -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
}
+265
View File
@@ -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,
}
}