1
0
mirror of https://github.com/MetaCubeX/mihomo.git synced 2026-10-10 04:03:11 +08:00

feat: support tlsmirror for vmess

This commit is contained in:
wwqgtxx
2026-07-07 10:48:02 +08:00
parent 3b85577f20
commit 2f510e86ad
25 changed files with 4527 additions and 48 deletions
+1
View File
@@ -221,6 +221,7 @@ jobs:
if: ${{ matrix.jobs.test == 'test' }}
run: |
export SKIP_CONCURRENT_TEST=1
export SKIP_INTEROP_TEST=1
go test ./...
echo "---test with_gvisor---"
go test ./... -tags "with_gvisor" -count=1
+2
View File
@@ -42,6 +42,8 @@ jobs:
GOTOOLCHAIN: local
# Fix mingw trying to be smart and converting paths https://github.com/moby/moby/issues/24029#issuecomment-250412919
MSYS_NO_PATHCONV: true
# Skip mihomo inbound interop test
SKIP_INTEROP_TEST: 1
steps:
- uses: actions/checkout@v6
+142
View File
@@ -0,0 +1,142 @@
package outbound
import "github.com/metacubex/mihomo/transport/tlsmirror"
type TLSMirrorOptions struct {
PrimaryKey string `proxy:"primary-key,omitempty"`
ExplicitNonceCipherSuites []uint16 `proxy:"explicit-nonce-ciphersuites,omitempty"`
DeferInstanceDerivedWriteTime TLSMirrorTimeSpec `proxy:"defer-instance-derived-write-time,omitempty"`
TransportLayerPadding TLSMirrorTransportLayerPadding `proxy:"transport-layer-padding,omitempty"`
ConnectionEnrolment *TLSMirrorConnectionEnrolment `proxy:"connection-enrolment,omitempty"`
EmbeddedTrafficGenerator TLSMirrorTrafficGenerator `proxy:"embedded-traffic-generator,omitempty"`
SequenceWatermarkingEnabled bool `proxy:"sequence-watermarking-enabled,omitempty"`
}
type TLSMirrorConnectionEnrolment struct {
PrimaryIngressOutbound string `proxy:"primary-ingress-outbound,omitempty"`
PrimaryEgressOutbound string `proxy:"primary-egress-outbound,omitempty"`
}
func (e *TLSMirrorConnectionEnrolment) Build() *tlsmirror.ConnectionEnrolment {
if e == nil {
return nil
}
return &tlsmirror.ConnectionEnrolment{
PrimaryIngressOutbound: e.PrimaryIngressOutbound,
PrimaryEgressOutbound: e.PrimaryEgressOutbound,
}
}
type TLSMirrorTimeSpec struct {
BaseNanoseconds uint64 `proxy:"base-nanoseconds,omitempty"`
UniformRandomMultiplierNanoseconds uint64 `proxy:"uniform-random-multiplier-nanoseconds,omitempty"`
}
func (s TLSMirrorTimeSpec) Build() tlsmirror.TimeSpec {
return tlsmirror.TimeSpec{
BaseNanoseconds: s.BaseNanoseconds,
UniformRandomMultiplierNanoseconds: s.UniformRandomMultiplierNanoseconds,
}
}
type TLSMirrorTransportLayerPadding struct {
Enabled bool `proxy:"enabled,omitempty"`
}
func (p TLSMirrorTransportLayerPadding) Build() tlsmirror.TransportLayerPadding {
return tlsmirror.TransportLayerPadding{
Enabled: p.Enabled,
}
}
type TLSMirrorTrafficGenerator struct {
Steps []TLSMirrorTrafficStep `proxy:"steps,omitempty"`
}
func (g TLSMirrorTrafficGenerator) Build() *tlsmirror.TrafficGenerator {
if len(g.Steps) == 0 {
return nil
}
steps := make([]tlsmirror.TrafficStep, 0, len(g.Steps))
for _, step := range g.Steps {
steps = append(steps, step.Build())
}
return &tlsmirror.TrafficGenerator{Steps: steps}
}
type TLSMirrorTrafficStep struct {
Name string `proxy:"name,omitempty"`
Host string `proxy:"host,omitempty"`
Path string `proxy:"path,omitempty"`
Method string `proxy:"method,omitempty"`
Headers []TLSMirrorTrafficHeader `proxy:"headers,omitempty"`
NextStep []TLSMirrorTrafficTransferCandidate `proxy:"next-step,omitempty"`
ConnectionReady bool `proxy:"connection-ready,omitempty"`
ConnectionRecallExit bool `proxy:"connection-recall-exit,omitempty"`
WaitTime TLSMirrorTimeSpec `proxy:"wait-time,omitempty"`
H2DoNotWaitForDownloadFinish bool `proxy:"h2-do-not-wait-for-download-finish,omitempty"`
}
func (s TLSMirrorTrafficStep) Build() tlsmirror.TrafficStep {
headers := make([]tlsmirror.TrafficHeader, 0, len(s.Headers))
for _, header := range s.Headers {
headers = append(headers, header.Build())
}
nextStep := make([]tlsmirror.TrafficTransferCandidate, 0, len(s.NextStep))
for _, candidate := range s.NextStep {
nextStep = append(nextStep, candidate.Build())
}
return tlsmirror.TrafficStep{
Name: s.Name,
Host: s.Host,
Path: s.Path,
Method: s.Method,
Headers: headers,
NextStep: nextStep,
ConnectionReady: s.ConnectionReady,
ConnectionRecallExit: s.ConnectionRecallExit,
WaitTime: s.WaitTime.Build(),
H2DoNotWaitForDownloadFinish: s.H2DoNotWaitForDownloadFinish,
}
}
type TLSMirrorTrafficHeader struct {
Name string `proxy:"name,omitempty"`
Value string `proxy:"value,omitempty"`
Values []string `proxy:"values,omitempty"`
}
func (h TLSMirrorTrafficHeader) Build() tlsmirror.TrafficHeader {
return tlsmirror.TrafficHeader{
Name: h.Name,
Value: h.Value,
Values: h.Values,
}
}
type TLSMirrorTrafficTransferCandidate struct {
Weight int32 `proxy:"weight,omitempty"`
GotoLocation int `proxy:"goto-location,omitempty"`
}
func (c TLSMirrorTrafficTransferCandidate) Build() tlsmirror.TrafficTransferCandidate {
return tlsmirror.TrafficTransferCandidate{
Weight: c.Weight,
GotoLocation: c.GotoLocation,
}
}
func (o TLSMirrorOptions) Build() *tlsmirror.Config {
if o.PrimaryKey == "" {
return nil
}
return &tlsmirror.Config{
PrimaryKey: o.PrimaryKey,
ExplicitNonceCipherSuites: o.ExplicitNonceCipherSuites,
DeferInstanceDerivedWrite: o.DeferInstanceDerivedWriteTime.Build(),
TransportLayerPadding: o.TransportLayerPadding.Build(),
ConnectionEnrolment: o.ConnectionEnrolment.Build(),
SequenceWatermarkingEnabled: o.SequenceWatermarkingEnabled,
EmbeddedTrafficGenerator: o.EmbeddedTrafficGenerator.Build(),
}
}
+58 -45
View File
@@ -13,6 +13,7 @@ import (
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
@@ -42,33 +43,34 @@ type Vmess struct {
type VmessOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
PrivateKey string `proxy:"private-key,omitempty"`
ServerName string `proxy:"servername,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
GlobalPadding bool `proxy:"global-padding,omitempty"`
AuthenticatedLength bool `proxy:"authenticated-length,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
PrivateKey string `proxy:"private-key,omitempty"`
ServerName string `proxy:"servername,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
TLSMirrorOpts TLSMirrorOptions `proxy:"tlsmirror-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
GlobalPadding bool `proxy:"global-padding,omitempty"`
AuthenticatedLength bool `proxy:"authenticated-length,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
}
type HTTPOptions struct {
@@ -124,25 +126,32 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
}
if v.option.TLS {
wsOpts.TLS = true
wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
},
Fingerprint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
})
if err != nil {
return nil, err
}
if v.option.TLSMirrorOpts.PrimaryKey != "" {
c, err = v.streamTLSConn(ctx, c, false)
if err != nil {
return nil, err
}
} else {
wsOpts.TLS = true
wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
},
Fingerprint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
})
if err != nil {
return nil, err
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
}
}
c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts)
@@ -258,6 +267,8 @@ func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
TLSMirror: v.option.TLSMirrorOpts.Build(),
TLSMirrorDialer: proxydialer.New(v, false).DialContext,
}
if isH2 {
@@ -437,6 +448,8 @@ func NewVmess(option VmessOption) (*Vmess, error) {
NextProtos: []string{"h2"},
ECH: v.echConfig,
Reality: v.realityConfig,
TLSMirror: option.TLSMirrorOpts.Build(),
TLSMirrorDialer: proxydialer.New(v, false).DialContext,
}
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
+39
View File
@@ -678,6 +678,31 @@ proxies: # socks5
# client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan.
# skip-cert-verify: true
# servername: example.com # priority over wss host
# 如果填写 tlsmirror-opts 则开启 tlsmirror(当 tls: true 时),servername 和 alpn 使用上层配置
# tlsmirror-opts:
# primary-key: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= # 必填,32 字节主密钥的 base64 编码
# explicit-nonce-ciphersuites: [156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 49195, 49196, 49197, 49198, 49199, 49200, 49201, 49202, 49290, 49291, 49293, 49316, 49317, 49318, 49319, 49320, 49321, 49322, 49323, 49324, 49325, 49326, 49327, 52392, 52393, 52394, 52395, 52396, 52397, 52398] # TLS 1.2 载体使用显式 nonce 的加密套件
# defer-instance-derived-write-time: # 首次写入前的延迟
# base-nanoseconds: 0 # 固定延迟,单位纳秒
# uniform-random-multiplier-nanoseconds: 0 # 额外随机延迟上限,单位纳秒
# transport-layer-padding: # 启用传输层填充
# enabled: false
# connection-enrolment: # 启用 v2ray 兼容的连接登记确认
# primary-egress-outbound: "" # mihomo VMess outbound 保持为空;v2ray 可填写专用的控制出站 tag
# sequence-watermarking-enabled: false # 启用序列水印
# embedded-traffic-generator: # 生成额外的 HTTP 载体流量,协议由 alpn 决定
# steps:
# - host: example.com
# path: /
# method: GET
# connection-ready: true # 此步骤完成后再交付代理连接
# connection-recall-exit: true # 代理连接关闭后退出载体流量
# h2-do-not-wait-for-download-finish: false # h2 下不等待响应体读取完成
# wait-time:
# base-nanoseconds: 1000000000
# next-step:
# - weight: 1
# goto-location: 0
# network: ws
# ech-opts:
# enable: true # 必须手动开启
@@ -1866,6 +1891,20 @@ listeners:
# after-bytes: 0 # 传输指定字节后开始限速
# bytes-per-sec: 0 # 基准速率(字节/秒)
# burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
# 如果填写tlsmirror则开启tlsmirror(注意不可与certificate和private-key同时填写)
# tlsmirror:
# dest: test.com:443
# primary-key: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= # 必填,32 字节主密钥的 base64 编码
# proxy: ""
# explicit-nonce-ciphersuites: [156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 49195, 49196, 49197, 49198, 49199, 49200, 49201, 49202, 49290, 49291, 49293, 49316, 49317, 49318, 49319, 49320, 49321, 49322, 49323, 49324, 49325, 49326, 49327, 52392, 52393, 52394, 52395, 52396, 52397, 52398] # TLS 1.2 载体使用显式 nonce 的加密套件
# defer-instance-derived-write-time: # 首次写入前的延迟
# base-nanoseconds: 0 # 固定延迟,单位纳秒
# uniform-random-multiplier-nanoseconds: 0 # 额外随机延迟上限,单位纳秒
# transport-layer-padding: # 启用传输层填充
# enabled: false
# connection-enrolment: # 启用 v2ray 兼容的连接登记确认
# primary-ingress-outbound: "" # v2ray 兼容字段,建议和对端路由中的控制出站 tag 一致
# sequence-watermarking-enabled: false # 启用序列水印
- name: tuic-in-1
type: tuic
+50
View File
@@ -5,6 +5,7 @@ import (
"github.com/metacubex/mihomo/listener/reality"
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/transport/tlsmirror"
)
type VmessUser struct {
@@ -25,9 +26,58 @@ type VmessServer struct {
ClientAuthCert string
EchKey string
RealityConfig reality.Config
TLSMirror TLSMirror
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}
type TLSMirror struct {
PrimaryKey string `yaml:"primary-key" json:"primary-key,omitempty"`
Dest string `yaml:"dest" json:"dest,omitempty"`
Proxy string `yaml:"proxy" json:"proxy,omitempty"`
ExplicitNonceCipherSuites []uint16 `yaml:"explicit-nonce-ciphersuites" json:"explicit-nonce-ciphersuites,omitempty"`
DeferInstanceDerivedWriteTime TLSMirrorTimeSpec `yaml:"defer-instance-derived-write-time" json:"defer-instance-derived-write-time,omitempty"`
TransportLayerPadding TLSMirrorTransportLayerPadding `yaml:"transport-layer-padding" json:"transport-layer-padding,omitempty"`
ConnectionEnrolment *TLSMirrorConnectionEnrolment `yaml:"connection-enrolment" json:"connection-enrolment,omitempty"`
SequenceWatermarkingEnabled bool `yaml:"sequence-watermarking-enabled" json:"sequence-watermarking-enabled,omitempty"`
}
type TLSMirrorConnectionEnrolment struct {
PrimaryIngressOutbound string `yaml:"primary-ingress-outbound" json:"primary-ingress-outbound,omitempty"`
PrimaryEgressOutbound string `yaml:"primary-egress-outbound" json:"primary-egress-outbound,omitempty"`
}
func (e *TLSMirrorConnectionEnrolment) Build() *tlsmirror.ConnectionEnrolment {
if e == nil {
return nil
}
return &tlsmirror.ConnectionEnrolment{
PrimaryIngressOutbound: e.PrimaryIngressOutbound,
PrimaryEgressOutbound: e.PrimaryEgressOutbound,
}
}
type TLSMirrorTimeSpec struct {
BaseNanoseconds uint64 `yaml:"base-nanoseconds" json:"base-nanoseconds,omitempty"`
UniformRandomMultiplierNanoseconds uint64 `yaml:"uniform-random-multiplier-nanoseconds" json:"uniform-random-multiplier-nanoseconds,omitempty"`
}
func (s TLSMirrorTimeSpec) Build() tlsmirror.TimeSpec {
return tlsmirror.TimeSpec{
BaseNanoseconds: s.BaseNanoseconds,
UniformRandomMultiplierNanoseconds: s.UniformRandomMultiplierNanoseconds,
}
}
type TLSMirrorTransportLayerPadding struct {
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
}
func (p TLSMirrorTransportLayerPadding) Build() tlsmirror.TransportLayerPadding {
return tlsmirror.TransportLayerPadding{
Enabled: p.Enabled,
}
}
func (t VmessServer) String() string {
b, _ := json.Marshal(t)
return string(b)
+66
View File
@@ -0,0 +1,66 @@
package inbound
import (
LC "github.com/metacubex/mihomo/listener/config"
)
type TLSMirror struct {
PrimaryKey string `inbound:"primary-key,omitempty"`
Dest string `inbound:"dest,omitempty"`
Proxy string `inbound:"proxy,omitempty"`
ExplicitNonceCipherSuites []uint16 `inbound:"explicit-nonce-ciphersuites,omitempty"`
DeferInstanceDerivedWriteTime TLSMirrorTimeSpec `inbound:"defer-instance-derived-write-time,omitempty"`
TransportLayerPadding TLSMirrorTransportLayerPadding `inbound:"transport-layer-padding,omitempty"`
ConnectionEnrolment *TLSMirrorConnectionEnrolment `inbound:"connection-enrolment,omitempty"`
SequenceWatermarkingEnabled bool `inbound:"sequence-watermarking-enabled,omitempty"`
}
type TLSMirrorConnectionEnrolment struct {
PrimaryIngressOutbound string `inbound:"primary-ingress-outbound,omitempty"`
PrimaryEgressOutbound string `inbound:"primary-egress-outbound,omitempty"`
}
func (e *TLSMirrorConnectionEnrolment) Build() *LC.TLSMirrorConnectionEnrolment {
if e == nil {
return nil
}
return &LC.TLSMirrorConnectionEnrolment{
PrimaryIngressOutbound: e.PrimaryIngressOutbound,
PrimaryEgressOutbound: e.PrimaryEgressOutbound,
}
}
type TLSMirrorTimeSpec struct {
BaseNanoseconds uint64 `inbound:"base-nanoseconds,omitempty"`
UniformRandomMultiplierNanoseconds uint64 `inbound:"uniform-random-multiplier-nanoseconds,omitempty"`
}
func (s TLSMirrorTimeSpec) Build() LC.TLSMirrorTimeSpec {
return LC.TLSMirrorTimeSpec{
BaseNanoseconds: s.BaseNanoseconds,
UniformRandomMultiplierNanoseconds: s.UniformRandomMultiplierNanoseconds,
}
}
type TLSMirrorTransportLayerPadding struct {
Enabled bool `inbound:"enabled,omitempty"`
}
func (p TLSMirrorTransportLayerPadding) Build() LC.TLSMirrorTransportLayerPadding {
return LC.TLSMirrorTransportLayerPadding{
Enabled: p.Enabled,
}
}
func (c TLSMirror) Build() LC.TLSMirror {
return LC.TLSMirror{
PrimaryKey: c.PrimaryKey,
Dest: c.Dest,
Proxy: c.Proxy,
ExplicitNonceCipherSuites: c.ExplicitNonceCipherSuites,
DeferInstanceDerivedWriteTime: c.DeferInstanceDerivedWriteTime.Build(),
TransportLayerPadding: c.TransportLayerPadding.Build(),
ConnectionEnrolment: c.ConnectionEnrolment.Build(),
SequenceWatermarkingEnabled: c.SequenceWatermarkingEnabled,
}
}
+2
View File
@@ -20,6 +20,7 @@ type VmessOption struct {
ClientAuthCert string `inbound:"client-auth-cert,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
TLSMirror TLSMirror `inbound:"tlsmirror,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
@@ -68,6 +69,7 @@ func NewVmess(options *VmessOption) (*Vmess, error) {
ClientAuthCert: options.ClientAuthCert,
EchKey: options.EchKey,
RealityConfig: options.RealityConfig.Build(),
TLSMirror: options.TLSMirror.Build(),
MuxOption: options.MuxOption.Build(),
},
}, nil
+174 -1
View File
@@ -7,6 +7,8 @@ import (
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/metacubex/mihomo/transport/tlsmirror"
"github.com/stretchr/testify/assert"
)
@@ -56,7 +58,7 @@ func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outbound
tunnel.DoTest(t, out)
if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc
if outboundOptions.Network == "grpc" || outboundOptions.TLSMirrorOpts.PrimaryKey != "" { // don't test sing-mux over grpc/tlsmirror
return
}
testSingMux(t, tunnel, out)
@@ -103,6 +105,17 @@ func testInboundVMessTLS(t *testing.T, inboundOptions inbound.VmessOption, outbo
})
}
func testInboundVMessTLSMirror(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) {
testInboundVMess(t, inboundOptions, outboundOptions)
t.Run("uTLS", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.ClientFingerprint = "chrome"
testInboundVMess(t, inboundOptions, outboundOptions)
})
}
var tlsMirrorPrimaryKey = tlsmirror.GeneratePrimaryKey()
func TestInboundVMess_TLS(t *testing.T) {
inboundOptions := inbound.VmessOption{
Certificate: tlsCertificate,
@@ -128,6 +141,166 @@ func TestInboundVMess_Ws(t *testing.T) {
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLSMirror(t *testing.T) {
inboundOptions := inbound.VmessOption{
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorPrimaryKey,
Dest: net.JoinHostPort(realityDest, "443"),
},
}
outboundOptions := outbound.VmessOption{
ServerName: realityDest,
TLS: true,
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorPrimaryKey,
},
}
if !realityRealDial {
outboundOptions.Fingerprint = tlsFingerprint
}
testInboundVMessTLSMirror(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLSMirror_Ws(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorPrimaryKey,
Dest: net.JoinHostPort(realityDest, "443"),
},
}
outboundOptions := outbound.VmessOption{
Network: "ws",
ServerName: realityDest,
TLS: true,
WSOpts: outbound.WSOptions{
Path: "/ws",
},
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorPrimaryKey,
},
}
if !realityRealDial {
outboundOptions.Fingerprint = tlsFingerprint
}
testInboundVMessTLSMirror(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLSMirror_Grpc(t *testing.T) {
inboundOptions := inbound.VmessOption{
GrpcServiceName: "GunService",
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorPrimaryKey,
Dest: net.JoinHostPort(realityDest, "443"),
},
}
outboundOptions := outbound.VmessOption{
Network: "grpc",
ServerName: realityDest,
TLS: true,
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorPrimaryKey,
},
}
if !realityRealDial {
outboundOptions.Fingerprint = tlsFingerprint
}
testInboundVMessTLSMirror(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLSMirror_AdvancedOptions(t *testing.T) {
inboundOptions := inbound.VmessOption{
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorPrimaryKey,
Dest: net.JoinHostPort(realityDest, "443"),
ExplicitNonceCipherSuites: tlsmirror.RecommendedExplicitNonceCipherSuites,
DeferInstanceDerivedWriteTime: inbound.TLSMirrorTimeSpec{
BaseNanoseconds: 1000000,
},
TransportLayerPadding: inbound.TLSMirrorTransportLayerPadding{Enabled: true},
SequenceWatermarkingEnabled: true,
},
}
outboundOptions := outbound.VmessOption{
ServerName: realityDest,
TLS: true,
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorPrimaryKey,
ExplicitNonceCipherSuites: tlsmirror.RecommendedExplicitNonceCipherSuites,
DeferInstanceDerivedWriteTime: outbound.TLSMirrorTimeSpec{
BaseNanoseconds: 1000000,
},
TransportLayerPadding: outbound.TLSMirrorTransportLayerPadding{Enabled: true},
SequenceWatermarkingEnabled: true,
},
}
if !realityRealDial {
outboundOptions.Fingerprint = tlsFingerprint
}
testInboundVMessTLSMirror(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLSMirror_ConnectionEnrolment(t *testing.T) {
inboundOptions := inbound.VmessOption{
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorPrimaryKey,
Dest: net.JoinHostPort(realityDest, "443"),
ConnectionEnrolment: &inbound.TLSMirrorConnectionEnrolment{
PrimaryIngressOutbound: "tlsmirror-enrollment",
},
},
}
outboundOptions := outbound.VmessOption{
ServerName: realityDest,
TLS: true,
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorPrimaryKey,
ConnectionEnrolment: &outbound.TLSMirrorConnectionEnrolment{
PrimaryIngressOutbound: "tlsmirror-enrollment",
},
},
}
if !realityRealDial {
outboundOptions.Fingerprint = tlsFingerprint
}
testInboundVMessTLSMirror(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLSMirror_EmbeddedTrafficGenerator(t *testing.T) {
inboundOptions := inbound.VmessOption{
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorPrimaryKey,
Dest: net.JoinHostPort(realityDest, "443"),
},
}
outboundOptions := outbound.VmessOption{
ServerName: realityDest,
TLS: true,
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorPrimaryKey,
EmbeddedTrafficGenerator: outbound.TLSMirrorTrafficGenerator{Steps: []outbound.TLSMirrorTrafficStep{{
Host: realityDest,
Path: httpPath + "?size=1",
Method: "GET",
ConnectionReady: true,
ConnectionRecallExit: true,
WaitTime: outbound.TLSMirrorTimeSpec{
BaseNanoseconds: 1000000,
},
NextStep: []outbound.TLSMirrorTrafficTransferCandidate{{
Weight: 1,
GotoLocation: 0,
}},
}}},
},
}
if !realityRealDial {
outboundOptions.Fingerprint = tlsFingerprint
}
testInboundVMessTLSMirror(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Ws_ed1(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
@@ -0,0 +1,838 @@
package inbound_test
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"net/netip"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/metacubex/mihomo/adapter/outbound"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/metacubex/mihomo/transport/tlsmirror"
"github.com/metacubex/http"
"github.com/metacubex/tls"
"github.com/stretchr/testify/require"
)
const v2rayTLSMirrorInteropRef = "v5.51.2"
const v2rayTLSMirrorInteropXNetRef = "bd5f1dcf71cf0d6d2424021d0a04f191396a46a7" // http2: initialize Transport on NewClientConn
var tlsMirrorInteropPrimaryKey = tlsmirror.GeneratePrimaryKey()
func TestInboundVMess_TLSMirror_V2RayInterop(t *testing.T) {
if skip, _ := strconv.ParseBool(os.Getenv("SKIP_INTEROP_TEST")); skip {
t.Skip("SKIP_INTEROP_TEST is set")
}
v2rayBin := tlsMirrorInteropV2RayBinary(t)
tlsMirrorInteropTestCase(t, v2rayBin, "default", tlsMirrorInteropAdvanced{})
tlsMirrorInteropTestCase(t, v2rayBin, "padding", tlsMirrorInteropAdvanced{
config: tlsmirror.Config{
TransportLayerPadding: tlsmirror.TransportLayerPadding{Enabled: true},
},
payloadSize: 128,
})
tlsMirrorInteropTestCase(t, v2rayBin, "watermark", tlsMirrorInteropAdvanced{
config: tlsmirror.Config{
SequenceWatermarkingEnabled: true,
},
payloadSize: 128,
})
tlsMirrorInteropTestCase(t, v2rayBin, "tls12 explicit nonce", tlsMirrorInteropAdvanced{
config: tlsmirror.Config{
ExplicitNonceCipherSuites: tlsmirror.RecommendedExplicitNonceCipherSuites,
},
configureCarrierTLS: func(config *tls.Config) {
config.MinVersion = tls.VersionTLS12
config.MaxVersion = tls.VersionTLS12
config.CipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
},
tls12: true,
payloadSize: 128,
})
tlsMirrorInteropTestCase(t, v2rayBin, "advanced tls12 padding watermark", tlsMirrorInteropAdvanced{
config: tlsmirror.Config{
ExplicitNonceCipherSuites: tlsmirror.RecommendedExplicitNonceCipherSuites,
TransportLayerPadding: tlsmirror.TransportLayerPadding{Enabled: true},
SequenceWatermarkingEnabled: true,
},
configureCarrierTLS: func(config *tls.Config) {
config.MinVersion = tls.VersionTLS12
config.MaxVersion = tls.VersionTLS12
config.CipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
},
tls12: true,
payloadSize: 128,
})
tlsMirrorInteropTestCase(t, v2rayBin, "connection enrolment", tlsMirrorInteropAdvanced{
config: tlsmirror.Config{
ConnectionEnrolment: &tlsmirror.ConnectionEnrolment{
PrimaryIngressOutbound: "tlsmirror-enrollment",
},
},
payloadSize: 128,
})
tlsMirrorInteropMihomoClientH2EmbeddedTrafficGenerator(t, v2rayBin)
}
type tlsMirrorInteropAdvanced struct {
config tlsmirror.Config
configureCarrierTLS func(*tls.Config)
tls12 bool
payloadSize int
}
type tlsMirrorInteropCarrier struct {
addr string
fingerprint string
certChainHash string
}
func tlsMirrorInteropMihomoClientH2EmbeddedTrafficGenerator(t *testing.T, v2rayBin string) {
t.Run("h2 embedded traffic/mihomo client to v2ray server", func(t *testing.T) {
echoAddr := startTLSMirrorInteropEcho(t)
forward := startTLSMirrorInteropCarrierHTTP2(t)
v2rayPort := tlsMirrorInteropReservePort(t)
config := tlsMirrorInteropServerConfig(t, v2rayPort.Port(), tlsMirrorInteropPort(forward.addr), userUUID, tlsMirrorInteropAdvanced{})
startTLSMirrorInteropV2Ray(t, v2rayBin, config, v2rayPort, net.JoinHostPort("127.0.0.1", fmt.Sprint(v2rayPort.Port())))
out, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess_tlsmirror_v2ray_server_h2",
Server: "127.0.0.1",
Port: v2rayPort.Port(),
UUID: userUUID,
Cipher: "auto",
TLS: true,
ALPN: []string{"h2"},
ServerName: "localhost",
Fingerprint: forward.fingerprint,
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorInteropPrimaryKey,
EmbeddedTrafficGenerator: outbound.TLSMirrorTrafficGenerator{Steps: []outbound.TLSMirrorTrafficStep{{
Host: "localhost",
Path: "/",
Method: "GET",
ConnectionReady: true,
ConnectionRecallExit: true,
H2DoNotWaitForDownloadFinish: true,
WaitTime: outbound.TLSMirrorTimeSpec{
BaseNanoseconds: uint64((10 * time.Millisecond).Nanoseconds()),
},
NextStep: []outbound.TLSMirrorTrafficTransferCandidate{{
Weight: 1,
GotoLocation: 0,
}},
}}},
},
})
require.NoError(t, err)
t.Cleanup(func() { _ = out.Close() })
conn, err := out.DialContext(context.Background(), tlsMirrorInteropMetadata(t, echoAddr))
require.NoError(t, err)
require.NoError(t, tlsMirrorInteropRoundTripConn(conn, 128))
})
}
func tlsMirrorInteropTestCase(t *testing.T, v2rayBin, name string, advanced tlsMirrorInteropAdvanced) {
t.Run(name+"/mihomo client to v2ray server", func(t *testing.T) {
echoAddr := startTLSMirrorInteropEcho(t)
forward := startTLSMirrorInteropCarrierTLS(t, advanced.configureCarrierTLS)
v2rayPort := tlsMirrorInteropReservePort(t)
config := tlsMirrorInteropServerConfig(t, v2rayPort.Port(), tlsMirrorInteropPort(forward.addr), userUUID, advanced)
startTLSMirrorInteropV2Ray(t, v2rayBin, config, v2rayPort, net.JoinHostPort("127.0.0.1", fmt.Sprint(v2rayPort.Port())))
out, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess_tlsmirror_v2ray_server",
Server: "127.0.0.1",
Port: v2rayPort.Port(),
UUID: userUUID,
Cipher: "auto",
TLS: true,
ServerName: "localhost",
Fingerprint: forward.fingerprint,
TLSMirrorOpts: outbound.TLSMirrorOptions{
PrimaryKey: tlsMirrorInteropPrimaryKey,
ExplicitNonceCipherSuites: advanced.config.ExplicitNonceCipherSuites,
TransportLayerPadding: outbound.TLSMirrorTransportLayerPadding{Enabled: advanced.config.TransportLayerPadding.Enabled},
ConnectionEnrolment: tlsMirrorInteropOutboundConnectionEnrolment(advanced),
SequenceWatermarkingEnabled: advanced.config.SequenceWatermarkingEnabled,
},
})
require.NoError(t, err)
t.Cleanup(func() { _ = out.Close() })
conn, err := out.DialContext(context.Background(), tlsMirrorInteropMetadata(t, echoAddr))
require.NoError(t, err)
require.NoError(t, tlsMirrorInteropRoundTripConn(conn, advanced.payloadSize))
})
t.Run(name+"/v2ray client to mihomo server", func(t *testing.T) {
echoAddr := startTLSMirrorInteropEcho(t)
forward := startTLSMirrorInteropCarrierTLS(t, advanced.configureCarrierTLS)
v2rayPort := tlsMirrorInteropReservePort(t)
in, err := inbound.NewVmess(&inbound.VmessOption{
BaseOption: inbound.BaseOption{
NameStr: "vmess_tlsmirror_v2ray_client",
Listen: "127.0.0.1",
Port: "0",
},
Users: []inbound.VmessUser{
{Username: "test", UUID: userUUID},
},
TLSMirror: inbound.TLSMirror{
PrimaryKey: tlsMirrorInteropPrimaryKey,
Dest: forward.addr,
ExplicitNonceCipherSuites: advanced.config.ExplicitNonceCipherSuites,
TransportLayerPadding: inbound.TLSMirrorTransportLayerPadding{Enabled: advanced.config.TransportLayerPadding.Enabled},
ConnectionEnrolment: tlsMirrorInteropInboundConnectionEnrolment(advanced),
SequenceWatermarkingEnabled: advanced.config.SequenceWatermarkingEnabled,
},
})
require.NoError(t, err)
tunnel := tlsMirrorInteropDirectTunnel(t)
require.NoError(t, in.Listen(tunnel))
t.Cleanup(func() { _ = in.Close() })
inboundPort := tlsMirrorInteropParsePort(t, tlsMirrorInteropPort(in.Address()))
config := tlsMirrorInteropClientConfig(t, v2rayPort.Port(), inboundPort, tlsMirrorInteropPort(echoAddr), userUUID, forward.certChainHash, advanced)
startTLSMirrorInteropV2Ray(t, v2rayBin, config, v2rayPort, "")
tlsMirrorInteropRoundTripWithRetry(t, func() (net.Conn, error) {
return net.Dial("tcp", net.JoinHostPort("127.0.0.1", fmt.Sprint(v2rayPort.Port())))
}, advanced.payloadSize)
})
}
func tlsMirrorInteropV2RayBinary(t *testing.T) string {
t.Helper()
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("go toolchain not found, skip real v2ray tlsmirror interop test")
}
root := filepath.Join(os.TempDir(), "mihomo-v2ray-tlsmirror-interop", v2rayTLSMirrorInteropRef)
binDir := filepath.Join(root, "bin")
exe := ""
if runtime.GOOS == "windows" {
exe = ".exe"
}
v2rayBin := filepath.Join(binDir, "v2ray"+exe)
if _, err := os.Stat(v2rayBin); err == nil {
return v2rayBin
}
goVersion := tlsMirrorInteropGoVersion(t, goBin)
goMajor, goMinor, ok := tlsMirrorInteropGoVersionMajorMinor(goVersion)
if ok && goMajor == 1 && goMinor < 21 {
t.Skipf("%s does not support GOTOOLCHAIN toolchain download, skip real v2ray tlsmirror interop test", goVersion)
}
require.NoError(t, os.RemoveAll(root))
require.NoError(t, os.MkdirAll(binDir, 0o755))
tlsMirrorInteropGo(t, goBin, root, "mod", "init", "mihomo-v2ray-tlsmirror-interop")
tlsMirrorInteropGo(t, goBin, root, "get", "github.com/v2fly/v2ray-core/v5@"+v2rayTLSMirrorInteropRef)
if ok && (goMajor > 1 || goMajor == 1 && goMinor > 26) {
tlsMirrorInteropGo(t, goBin, root, "get", "golang.org/x/net@"+v2rayTLSMirrorInteropXNetRef)
}
tlsMirrorInteropGo(t, goBin, root, "build", "-mod=mod", "-trimpath", "-o", v2rayBin, "github.com/v2fly/v2ray-core/v5/main")
return v2rayBin
}
func tlsMirrorInteropGoVersion(t *testing.T, goBin string) string {
t.Helper()
cmd := exec.Command(goBin, "version")
output, err := cmd.Output()
require.NoError(t, err, "go version")
return tlsMirrorInteropParseGoVersion(string(output))
}
func tlsMirrorInteropParseGoVersion(output string) string {
for _, field := range strings.Fields(output) {
if strings.HasPrefix(field, "go1.") {
return field
}
}
return ""
}
func tlsMirrorInteropGoVersionMajorMinor(version string) (int, int, bool) {
version = strings.TrimPrefix(version, "go")
parts := strings.SplitN(version, ".", 3)
if len(parts) < 2 {
return 0, 0, false
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, false
}
minorText := parts[1]
for i, r := range minorText {
if r < '0' || r > '9' {
minorText = minorText[:i]
break
}
}
if minorText == "" {
return 0, 0, false
}
minor, err := strconv.Atoi(minorText)
if err != nil {
return 0, 0, false
}
return major, minor, true
}
func tlsMirrorInteropGo(t *testing.T, goBin, dir string, args ...string) {
t.Helper()
cmd := exec.Command(goBin, args...)
cmd.Dir = dir
cmd.Env = tlsMirrorInteropGoEnv()
output, err := cmd.CombinedOutput()
require.NoError(t, err, "go %s\n%s", strings.Join(args, " "), string(output))
}
func tlsMirrorInteropGoEnv() []string {
env := os.Environ()
hasGoToolchain := false
for i, value := range env {
if strings.HasPrefix(value, "GOTOOLCHAIN=") {
env[i] = "GOTOOLCHAIN=auto"
hasGoToolchain = true
}
}
if !hasGoToolchain {
env = append(env, "GOTOOLCHAIN=auto")
}
return env
}
func tlsMirrorInteropServerConfig(t *testing.T, listenPort int, forwardPort string, userID string, advanced tlsMirrorInteropAdvanced) []byte {
t.Helper()
forwardPortValue := tlsMirrorInteropParsePort(t, forwardPort)
config := tlsMirrorInteropBaseConfig()
config["inbounds"] = []any{map[string]any{
"protocol": "vmess",
"listen": "127.0.0.1",
"port": listenPort,
"settings": map[string]any{
"users": []string{userID},
},
"streamSettings": tlsMirrorInteropStreamConfig(tlsMirrorInteropServerSettings(advanced, forwardPortValue), nil),
}}
config["outbounds"] = []any{tlsMirrorInteropDirectOutbound()}
if advanced.config.ConnectionEnrolment != nil {
config["router"] = map[string]any{
"rule": []any{map[string]any{
"tag": advanced.config.ConnectionEnrolment.PrimaryIngressOutbound,
"domain": []any{map[string]any{
"type": "Full",
"value": tlsMirrorInteropEnrollmentControlHost(t),
}},
}},
}
}
return tlsMirrorInteropMarshalJSONConfig(t, config)
}
func tlsMirrorInteropClientConfig(t *testing.T, listenPort, serverPort int, targetPort string, userID, carrierCertHash string, advanced tlsMirrorInteropAdvanced) []byte {
t.Helper()
targetPortValue := tlsMirrorInteropParsePort(t, targetPort)
config := tlsMirrorInteropBaseConfig()
config["inbounds"] = []any{map[string]any{
"protocol": "dokodemo-door",
"listen": "127.0.0.1",
"port": listenPort,
"settings": map[string]any{
"address": "127.0.0.1",
"port": targetPortValue,
"networks": "tcp",
},
}}
config["outbounds"] = []any{
map[string]any{
"protocol": "vmess",
"tag": "vmess-tlsmirror",
"streamSettings": tlsMirrorInteropStreamConfig(tlsMirrorInteropClientSettings(advanced), tlsMirrorInteropSecuritySettings(advanced, carrierCertHash)),
"settings": map[string]any{
"address": "127.0.0.1",
"port": serverPort,
"uuid": userID,
},
},
tlsMirrorInteropDirectOutbound(),
}
if advanced.config.ConnectionEnrolment != nil {
controlAdvanced := advanced
controlAdvanced.config.ConnectionEnrolment = nil
config["outbounds"] = append(config["outbounds"].([]any), map[string]any{
"protocol": "vmess",
"tag": "vmess-tlsmirror-control",
"streamSettings": tlsMirrorInteropStreamConfig(tlsMirrorInteropClientControlSettings(controlAdvanced), tlsMirrorInteropSecuritySettings(advanced, carrierCertHash)),
"settings": map[string]any{
"address": "127.0.0.1",
"port": serverPort,
"uuid": userID,
},
})
}
return tlsMirrorInteropMarshalJSONConfig(t, config)
}
func tlsMirrorInteropBaseConfig() map[string]any {
return map[string]any{
"log": map[string]any{
"error": map[string]any{
"type": "Console",
"level": "Debug",
},
},
}
}
func tlsMirrorInteropStreamConfig(tlsMirrorSettings map[string]any, securitySettings map[string]any) map[string]any {
config := map[string]any{
"transport": "tlsmirror",
"transportSettings": tlsMirrorSettings,
}
if securitySettings != nil {
config["security"] = "tls"
config["securitySettings"] = securitySettings
}
return config
}
func tlsMirrorInteropServerSettings(advanced tlsMirrorInteropAdvanced, forwardPort int) map[string]any {
settings := tlsMirrorInteropTLSMirrorSettings(advanced)
settings["forwardAddress"] = "127.0.0.1"
settings["forwardPort"] = forwardPort
return settings
}
func tlsMirrorInteropClientSettings(advanced tlsMirrorInteropAdvanced) map[string]any {
settings := tlsMirrorInteropTLSMirrorSettings(advanced)
settings["carrierConnectionTag"] = "tlsmirror-carrier"
settings["forwardTag"] = "direct"
if advanced.config.ConnectionEnrolment != nil {
settings["connectionEnrolment"].(map[string]any)["primaryEgressOutbound"] = "vmess-tlsmirror-control"
}
settings["embeddedTrafficGenerator"] = tlsMirrorInteropEmbeddedTrafficGeneratorSettings()
return settings
}
func tlsMirrorInteropClientControlSettings(advanced tlsMirrorInteropAdvanced) map[string]any {
settings := tlsMirrorInteropTLSMirrorSettings(advanced)
settings["carrierConnectionTag"] = "tlsmirror-carrier-control"
settings["forwardTag"] = "direct"
settings["embeddedTrafficGenerator"] = tlsMirrorInteropEmbeddedTrafficGeneratorSettings()
return settings
}
func tlsMirrorInteropEmbeddedTrafficGeneratorSettings() map[string]any {
return map[string]any{
"steps": []any{map[string]any{
"host": "localhost",
"path": "/",
"method": "GET",
"connectionReady": true,
"connectionRecallExit": true,
"waitTime": map[string]any{
"baseNanoseconds": uint64(time.Second),
},
"nextStep": []any{map[string]any{
"weight": 1,
"gotoLocation": 0,
}},
}},
}
}
func tlsMirrorInteropSecuritySettings(advanced tlsMirrorInteropAdvanced, carrierCertHash string) map[string]any {
return map[string]any{
"allowInsecureIfPinnedPeerCertificate": true,
"pinnedPeerCertificateChainSha256": []string{carrierCertHash},
"serverName": "localhost",
"minVersion": tlsMirrorInteropTLSVersion(advanced),
"maxVersion": tlsMirrorInteropTLSVersion(advanced),
}
}
func tlsMirrorInteropTLSMirrorSettings(advanced tlsMirrorInteropAdvanced) map[string]any {
settings := map[string]any{
"primaryKey": tlsMirrorInteropPrimaryKey,
"sequenceWatermarkingEnabled": advanced.config.SequenceWatermarkingEnabled,
}
if advanced.config.ConnectionEnrolment != nil {
settings["connectionEnrolment"] = map[string]any{
"primaryIngressOutbound": advanced.config.ConnectionEnrolment.PrimaryIngressOutbound,
"primaryEgressOutbound": advanced.config.ConnectionEnrolment.PrimaryEgressOutbound,
}
}
if advanced.config.TransportLayerPadding.Enabled {
settings["transportLayerPadding"] = map[string]any{"enabled": true}
}
if advanced.tls12 {
settings["explicitNonceCiphersuites"] = []uint32{0xc02b}
}
return settings
}
func tlsMirrorInteropOutboundConnectionEnrolment(advanced tlsMirrorInteropAdvanced) *outbound.TLSMirrorConnectionEnrolment {
if advanced.config.ConnectionEnrolment == nil {
return nil
}
return &outbound.TLSMirrorConnectionEnrolment{
PrimaryIngressOutbound: advanced.config.ConnectionEnrolment.PrimaryIngressOutbound,
PrimaryEgressOutbound: advanced.config.ConnectionEnrolment.PrimaryEgressOutbound,
}
}
func tlsMirrorInteropInboundConnectionEnrolment(advanced tlsMirrorInteropAdvanced) *inbound.TLSMirrorConnectionEnrolment {
if advanced.config.ConnectionEnrolment == nil {
return nil
}
return &inbound.TLSMirrorConnectionEnrolment{
PrimaryIngressOutbound: advanced.config.ConnectionEnrolment.PrimaryIngressOutbound,
PrimaryEgressOutbound: advanced.config.ConnectionEnrolment.PrimaryEgressOutbound,
}
}
func tlsMirrorInteropEnrollmentControlHost(t *testing.T) string {
t.Helper()
key, err := tlsmirror.DecodePrimaryKey(tlsMirrorInteropPrimaryKey)
require.NoError(t, err)
host, err := tlsmirror.ServerIdentifierHost(key)
require.NoError(t, err)
return host
}
func tlsMirrorInteropTLSVersion(advanced tlsMirrorInteropAdvanced) string {
if advanced.tls12 {
return "TLS1_2"
}
return "TLS1_3"
}
func tlsMirrorInteropDirectOutbound() map[string]any {
return map[string]any{
"protocol": "freedom",
"tag": "direct",
}
}
func tlsMirrorInteropMarshalJSONConfig(t *testing.T, config map[string]any) []byte {
t.Helper()
data, err := json.MarshalIndent(config, "", " ")
require.NoError(t, err)
data = append(data, '\n')
return data
}
func tlsMirrorInteropParsePort(t *testing.T, port string) int {
t.Helper()
value, err := strconv.Atoi(port)
require.NoError(t, err)
return value
}
func startTLSMirrorInteropV2Ray(t *testing.T, v2rayBin string, config []byte, port *tlsMirrorInteropReservedPort, waitAddr string) {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, v2rayBin, "run", "-format=jsonv5")
var output bytes.Buffer
cmd.Stdin = bytes.NewReader(config)
cmd.Stdout = &output
cmd.Stderr = &output
port.Release()
require.NoError(t, cmd.Start())
t.Cleanup(func() {
cancel()
_ = cmd.Wait()
if t.Failed() {
t.Log(output.String())
}
})
if waitAddr == "" {
time.Sleep(300 * time.Millisecond)
return
}
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
conn, err := net.DialTimeout("tcp", waitAddr, 100*time.Millisecond)
if err == nil {
_ = conn.Close()
return
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("v2ray did not listen on %s\n%s", waitAddr, output.String())
}
func startTLSMirrorInteropCarrierTLS(t *testing.T, configure ...func(*tls.Config)) tlsMirrorInteropCarrier {
t.Helper()
certPEM, keyPEM, fingerprint, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
require.NoError(t, err)
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
require.NoError(t, err)
config := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
for _, configure := range configure {
if configure != nil {
configure(config)
}
}
ln, err := tls.Listen("tcp", "127.0.0.1:0", config)
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}
if line == "\r\n" {
break
}
}
_, _ = conn.Write([]byte("HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"))
_, _ = io.Copy(io.Discard, reader)
}()
}
}()
return tlsMirrorInteropCarrier{
addr: ln.Addr().String(),
fingerprint: fingerprint,
certChainHash: tlsMirrorInteropCertChainHash([]byte(certPEM)),
}
}
func tlsMirrorInteropCertChainHash(certContent []byte) string {
var hashValue []byte
for {
block, remain := pem.Decode(certContent)
if block == nil {
break
}
certHash := sha256.Sum256(block.Bytes)
if hashValue == nil {
hashValue = certHash[:]
} else {
chainHash := sha256.Sum256(append(hashValue, certHash[:]...))
hashValue = chainHash[:]
}
certContent = remain
}
return base64.StdEncoding.EncodeToString(hashValue)
}
func startTLSMirrorInteropCarrierHTTP2(t *testing.T) tlsMirrorInteropCarrier {
t.Helper()
certPEM, keyPEM, fingerprint, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
require.NoError(t, err)
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
require.NoError(t, err)
ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
NextProtos: []string{"h2"},
})
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
Protocols: new(http.Protocols),
}
server.Protocols.SetHTTP2(true)
server.Protocols.SetUnencryptedHTTP2(true)
go func() { _ = server.Serve(ln) }()
t.Cleanup(func() { _ = server.Close() })
return tlsMirrorInteropCarrier{
addr: ln.Addr().String(),
fingerprint: fingerprint,
certChainHash: tlsMirrorInteropCertChainHash([]byte(certPEM)),
}
}
func startTLSMirrorInteropEcho(t *testing.T) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
defer conn.Close()
_, _ = io.Copy(conn, conn)
}()
}
}()
return ln.Addr().String()
}
func tlsMirrorInteropDirectTunnel(t *testing.T) *TestTunnel {
t.Helper()
return &TestTunnel{
HandleTCPConnFn: func(conn net.Conn, metadata *C.Metadata) {
target, err := net.Dial("tcp", metadata.RemoteAddress())
if err != nil {
_ = conn.Close()
return
}
N.Relay(target, conn)
},
HandleUDPPacketFn: func(packet C.UDPPacket, metadata *C.Metadata) {
packet.Drop()
},
NatTableFn: func() C.NatTable {
return nil
},
CloseFn: func() error {
return nil
},
NewDialerFn: func() C.Dialer {
return nil
},
}
}
func tlsMirrorInteropMetadata(t *testing.T, addr string) *C.Metadata {
t.Helper()
host, port, err := net.SplitHostPort(addr)
require.NoError(t, err)
ip, err := netip.ParseAddr(host)
require.NoError(t, err)
portNum, err := net.LookupPort("tcp", port)
require.NoError(t, err)
return &C.Metadata{
NetWork: C.TCP,
DstIP: ip,
DstPort: uint16(portNum),
}
}
func tlsMirrorInteropRoundTripWithRetry(t *testing.T, dial func() (net.Conn, error), payloadSize int) {
t.Helper()
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
conn, err := dial()
if err == nil {
err = tlsMirrorInteropRoundTripConn(conn, payloadSize)
} else {
err = fmt.Errorf("dial: %w", err)
}
if err == nil {
return
}
lastErr = err
var netErr net.Error
if !errors.As(err, &netErr) || !netErr.Timeout() {
break
}
// v2ray-core registers the tlsmirror carrier outbound asynchronously on
// first use, so slower builders may hit a startup-only timeout.
time.Sleep(200 * time.Millisecond)
}
require.NoError(t, lastErr)
}
func tlsMirrorInteropRoundTripConn(conn net.Conn, payloadSize int) error {
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
return fmt.Errorf("set deadline: %w", err)
}
if payloadSize == 0 {
payloadSize = len("tlsmirror-interop-") * 256
}
payload := bytes.Repeat([]byte("x"), payloadSize)
_, err := conn.Write(payload)
if err != nil {
return fmt.Errorf("write: %w", err)
}
got := make([]byte, len(payload))
_, err = io.ReadFull(conn, got)
if err != nil {
return fmt.Errorf("read full: %w", err)
}
if !bytes.Equal(payload, got) {
return fmt.Errorf("unexpected payload: got %d bytes", len(got))
}
return nil
}
type tlsMirrorInteropReservedPort struct {
ln net.Listener
port int
once sync.Once
}
func tlsMirrorInteropReservePort(t *testing.T) *tlsMirrorInteropReservedPort {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := &tlsMirrorInteropReservedPort{
ln: ln,
port: ln.Addr().(*net.TCPAddr).Port,
}
t.Cleanup(port.Release)
return port
}
func (p *tlsMirrorInteropReservedPort) Port() int {
return p.port
}
func (p *tlsMirrorInteropReservedPort) Release() {
p.once.Do(func() {
_ = p.ln.Close()
})
}
func tlsMirrorInteropPort(addr string) string {
_, port, _ := net.SplitHostPort(addr)
return port
}
+18 -1
View File
@@ -14,6 +14,7 @@ import (
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/reality"
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/listener/tlsmirror"
"github.com/metacubex/mihomo/ntp"
"github.com/metacubex/mihomo/transport/gun"
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
@@ -83,6 +84,7 @@ func New(config LC.VmessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit
}
tlsConfig := &tls.Config{Time: ntp.Now}
var realityBuilder *reality.Builder
var tlsMirrorBuilder *tlsmirror.Builder
if config.Certificate != "" && config.PrivateKey != "" {
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
@@ -125,6 +127,19 @@ func New(config LC.VmessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit
return nil, err
}
}
if config.TLSMirror.PrimaryKey != "" {
tlsMirrorBuilder = tlsmirror.Config{
PrimaryKey: config.TLSMirror.PrimaryKey,
Dest: config.TLSMirror.Dest,
Proxy: config.TLSMirror.Proxy,
ExplicitNonceCipherSuites: config.TLSMirror.ExplicitNonceCipherSuites,
DeferInstanceDerivedWriteTime: config.TLSMirror.DeferInstanceDerivedWriteTime.Build(),
TransportLayerPadding: config.TLSMirror.TransportLayerPadding.Build(),
ConnectionEnrolment: config.TLSMirror.ConnectionEnrolment.Build(),
SequenceWatermarkingEnabled: config.TLSMirror.SequenceWatermarkingEnabled,
}.Build(tunnel)
h.Tunnel = tlsMirrorBuilder.WrapTunnel(tunnel)
}
if config.WsPath != "" {
httpMux := http.NewServeMux()
httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) {
@@ -167,7 +182,9 @@ func New(config LC.VmessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit
if err != nil {
return nil, err
}
if realityBuilder != nil {
if tlsMirrorBuilder != nil {
l = tlsMirrorBuilder.NewListener(l)
} else if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if tlsConfig.GetCertificate != nil {
l = tls.NewListener(l, tlsConfig)
+98
View File
@@ -0,0 +1,98 @@
package tlsmirror
import (
"context"
"net"
"runtime/debug"
"strings"
N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inner"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/tlsmirror"
)
type Config struct {
PrimaryKey string
Dest string
Proxy string
ExplicitNonceCipherSuites []uint16
DeferInstanceDerivedWriteTime tlsmirror.TimeSpec
TransportLayerPadding tlsmirror.TransportLayerPadding
ConnectionEnrolment *tlsmirror.ConnectionEnrolment
SequenceWatermarkingEnabled bool
}
func (c Config) Build(tunnel C.Tunnel) *Builder {
return &Builder{
config: c,
tunnel: tunnel,
}
}
func (b Builder) WrapTunnel(tunnel C.Tunnel) C.Tunnel {
if b.config.ConnectionEnrolment == nil {
return tunnel
}
key, err := tlsmirror.DecodePrimaryKey(b.config.PrimaryKey)
if err != nil {
return tunnel
}
controlHost, err := tlsmirror.ServerIdentifierHost(key)
if err != nil {
return tunnel
}
return &enrollmentTunnel{
Tunnel: tunnel,
primaryKey: b.config.PrimaryKey,
controlHost: controlHost,
}
}
type Builder struct {
config Config
tunnel C.Tunnel
}
func (b Builder) NewListener(l net.Listener) net.Listener {
return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) {
forwardConn, err := inner.HandleTcp(b.tunnel, b.config.Dest, b.config.Proxy)
if err != nil {
return nil, err
}
hiddenConn, err := tlsmirror.ServeConnReady(ctx, conn, forwardConn, tlsmirror.ServerConfig{
PrimaryKey: b.config.PrimaryKey,
ExplicitNonceCipherSuites: b.config.ExplicitNonceCipherSuites,
DeferInstanceDerivedWrite: b.config.DeferInstanceDerivedWriteTime,
TransportLayerPadding: b.config.TransportLayerPadding,
ConnectionEnrolment: b.config.ConnectionEnrolment,
SequenceWatermarkingEnabled: b.config.SequenceWatermarkingEnabled,
})
if err != nil {
_ = forwardConn.Close()
return nil, err
}
return hiddenConn, nil
}, func(a any) {
stack := debug.Stack()
log.Errorln("tlsmirror server panic: %s\n%s", a, stack)
})
}
type enrollmentTunnel struct {
C.Tunnel
primaryKey string
controlHost string
}
func (t *enrollmentTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) {
if metadata.NetWork == C.TCP && metadata.DstPort == 80 && strings.EqualFold(metadata.Host, t.controlHost) {
if err := tlsmirror.ServeEnrollmentControlConnection(context.Background(), conn, t.primaryKey); err != nil {
log.Warnln("tlsmirror enrollment control connection failed: %s", err)
_ = conn.Close()
}
return
}
t.Tunnel.HandleTCPConn(conn, metadata)
}
+1 -1
View File
@@ -279,7 +279,7 @@ func NewTransport(dialFn DialFn, tlsConfig *vmess.TLSConfig, gunCfg *Config) *Tr
return nil, err
}
if tlsConfig.Reality == nil { // reality doesn't return the negotiated ALPN
if tlsConfig.Reality == nil && tlsConfig.TLSMirror == nil { // reality and tlsmirror don't return the negotiated ALPN
state := tlsC.GetTLSConnectionState(conn)
if p := state.NegotiatedProtocol; p != Http2NextProtoTLS {
_ = conn.Close()
+142
View File
@@ -0,0 +1,142 @@
package tlsmirror
import (
"context"
"fmt"
"net"
"sync"
"github.com/metacubex/mihomo/component/ca"
tlsC "github.com/metacubex/mihomo/component/tls"
"github.com/metacubex/tls"
)
func Dial(ctx context.Context, rawConn net.Conn, cfg ClientConfig) (*Conn, error) {
key, err := DecodePrimaryKey(cfg.PrimaryKey)
if err != nil {
return nil, err
}
if cfg.ConnectionEnrolment != nil {
serverID, err := deriveEnrollmentServerIdentifier(key)
if err != nil {
_ = rawConn.Close()
return nil, err
}
if IsLoopbackProtectionEnabled(ctx, serverID) {
_ = rawConn.Close()
return nil, fmt.Errorf("tlsmirror: loopback protection refused dialing to self")
}
}
serverName := cfg.ServerName
if serverName == "" {
serverName = cfg.ForwardAddressHint
}
if serverName == "" && !cfg.SkipCertVerify {
return nil, fmt.Errorf("tlsmirror: server-name is required when certificate verification is enabled")
}
tlsSide, mirrorSide := net.Pipe()
lifetimeCtx := context.Background()
var hidden *Conn
mirror := newMirrorConn(lifetimeCtx, mirrorSide, rawConn,
cfg.Config,
nil,
func(rec *record) (bool, error) {
return hidden.handleInboundRecord(rec)
},
nil,
nil,
)
hidden, err = newHiddenConn(lifetimeCtx, mirror, key, false, cfg.Config)
if err != nil {
_ = mirror.Close()
return nil, err
}
mirror.onC2SMessageTx = hidden.handleOutboundRecordTx
mirror.start()
tlsConfig, err := ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
ServerName: serverName,
InsecureSkipVerify: cfg.SkipCertVerify,
NextProtos: cfg.ALPN,
},
Fingerprint: cfg.Fingerprint,
Certificate: cfg.Certificate,
PrivateKey: cfg.PrivateKey,
})
if err != nil {
_ = hidden.Close()
return nil, err
}
var carrierTLS net.Conn
if clientFingerprint, ok := tlsC.GetFingerprint(cfg.ClientFingerprint); ok {
uConfig := tlsC.UConfig(tlsConfig)
if cfg.ECH != nil {
err = cfg.ECH.ClientHandleUTLS(ctx, uConfig)
if err != nil {
_ = hidden.Close()
return nil, err
}
}
uConn := tlsC.UClient(tlsSide, uConfig, clientFingerprint)
if err := uConn.HandshakeContext(ctx); err != nil {
_ = hidden.Close()
return nil, fmt.Errorf("%w: %w", errCarrierHandshake, err)
}
carrierTLS = uConn
} else {
if cfg.ECH != nil {
err = cfg.ECH.ClientHandle(ctx, tlsConfig)
if err != nil {
_ = hidden.Close()
return nil, err
}
}
tlsConn := tls.Client(tlsSide, tlsConfig)
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = hidden.Close()
return nil, fmt.Errorf("%w: %w", errCarrierHandshake, err)
}
carrierTLS = tlsConn
}
carrierALPN := tlsC.GetTLSConnectionState(carrierTLS).NegotiatedProtocol
ready := make(chan struct{})
recall := make(chan struct{})
var recallOnce sync.Once
hidden.recallTrafficGenerator = func() {
recallOnce.Do(func() {
close(recall)
})
}
done := make(chan struct{})
go func() {
defer close(done)
runTrafficGenerator(hidden.ctx, carrierTLS, cfg.EmbeddedTrafficGenerator, carrierALPN, func() {
close(ready)
}, recall)
}()
if trafficGeneratorWaitsForReady(cfg.EmbeddedTrafficGenerator) {
select {
case <-ready:
case <-done:
_ = hidden.Close()
return nil, fmt.Errorf("tlsmirror: carrier traffic generator exited before ready")
case <-hidden.ctx.Done():
return nil, hidden.ctx.Err()
case <-ctx.Done():
_ = hidden.Close()
return nil, ctx.Err()
}
}
if cfg.ConnectionEnrolment != nil && !isConnectionEnrollmentBypassed(ctx) {
if err := hidden.verifyConnectionEnrollment(ctx, cfg); err != nil {
_ = hidden.Close()
return nil, err
}
}
return hidden, nil
}
+129
View File
@@ -0,0 +1,129 @@
package tlsmirror
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"math/big"
"net"
"time"
tlsC "github.com/metacubex/mihomo/component/tls"
"github.com/metacubex/tls"
)
type ClientConfig struct {
Config
ServerName string
SkipCertVerify bool
ALPN []string
Fingerprint string
Certificate string
PrivateKey string
ClientFingerprint string
ForwardAddressHint string
ECH ECHConfig
EnrollmentDialer EnrollmentDialer
}
type ServerConfig = Config
// RecommendedExplicitNonceCipherSuites is the recommended TLS 1.2 cipher suite list for explicit nonce carriers.
var RecommendedExplicitNonceCipherSuites = []uint16{
156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171,
172, 173, 49195, 49196, 49197, 49198, 49199, 49200, 49201, 49202, 49290,
49291, 49293, 49316, 49317, 49318, 49319, 49320, 49321, 49322, 49323,
49324, 49325, 49326, 49327, 52392, 52393, 52394, 52395, 52396, 52397,
52398,
}
type Config struct {
PrimaryKey string
ExplicitNonceCipherSuites []uint16
DeferInstanceDerivedWrite TimeSpec
TransportLayerPadding TransportLayerPadding
ConnectionEnrolment *ConnectionEnrolment
SequenceWatermarkingEnabled bool
EmbeddedTrafficGenerator *TrafficGenerator
}
type ConnectionEnrolment struct {
PrimaryIngressOutbound string
PrimaryEgressOutbound string
}
type EnrollmentDialer func(ctx context.Context, network, address string) (net.Conn, error)
type ECHConfig interface {
ClientHandle(context.Context, *tls.Config) error
ClientHandleUTLS(context.Context, *tlsC.Config) error
}
type TrafficGenerator struct {
Steps []TrafficStep
}
type TrafficStep struct {
Name string
Host string
Path string
Method string
Headers []TrafficHeader
NextStep []TrafficTransferCandidate
ConnectionReady bool
ConnectionRecallExit bool
WaitTime TimeSpec
H2DoNotWaitForDownloadFinish bool
}
type TrafficHeader struct {
Name string
Value string
Values []string
}
type TrafficTransferCandidate struct {
Weight int32
GotoLocation int
}
type TimeSpec struct {
BaseNanoseconds uint64
UniformRandomMultiplierNanoseconds uint64
}
func (s TimeSpec) Duration() (time.Duration, error) {
delay := s.BaseNanoseconds
if s.UniformRandomMultiplierNanoseconds > 0 {
n, err := rand.Int(rand.Reader, new(big.Int).SetUint64(s.UniformRandomMultiplierNanoseconds))
if err != nil {
return 0, err
}
delay += n.Uint64()
}
return time.Duration(delay), nil
}
type TransportLayerPadding struct {
Enabled bool
}
func GeneratePrimaryKey() string {
key := make([]byte, 32)
_, _ = rand.Read(key)
return base64.StdEncoding.EncodeToString(key)
}
func DecodePrimaryKey(value string) ([]byte, error) {
if value == "" {
return nil, errors.New("missing tlsmirror primary key")
}
key, err := base64.StdEncoding.DecodeString(value)
if err == nil && len(key) == 32 {
return key, nil
}
return nil, errors.New("tlsmirror primary key must be standard base64 and decode to 32 bytes")
}
+421
View File
@@ -0,0 +1,421 @@
package tlsmirror
import (
"bytes"
"context"
"crypto/cipher"
"errors"
"net"
"os"
"sync"
"time"
"github.com/metacubex/mihomo/common/net/deadline"
)
type Conn struct {
ctx context.Context
cancel context.CancelFunc
mirror *mirrorConn
primaryKey []byte
isServer bool
config Config
mu sync.Mutex
readMu sync.Mutex
writeMu sync.Mutex
encryptor *encryptor
decryptor *decryptor
protocolVersion [2]byte
firstWrite bool
firstWriteDelay time.Duration
watermarkTx cipher.Stream
watermarkRx cipher.Stream
readCh chan []byte
readBuffer *bytes.Buffer
recallTrafficGenerator func()
enrollmentMu sync.Mutex
enrollmentRemove func()
enrollmentRemoved bool
readDeadline deadline.PipeDeadline
writeDeadline deadline.PipeDeadline
}
func newHiddenConn(ctx context.Context, mirror *mirrorConn, primaryKey []byte, isServer bool, cfg Config) (*Conn, error) {
cctx, cancel := context.WithCancel(ctx)
firstWriteDelay, err := cfg.DeferInstanceDerivedWrite.Duration()
if err != nil {
cancel()
return nil, err
}
return &Conn{
ctx: cctx,
cancel: cancel,
mirror: mirror,
primaryKey: primaryKey,
isServer: isServer,
config: cfg,
firstWrite: true,
firstWriteDelay: firstWriteDelay,
readCh: make(chan []byte, 32),
readDeadline: deadline.MakePipeDeadline(),
writeDeadline: deadline.MakePipeDeadline(),
}, nil
}
func (c *Conn) ensureCryptoLocked(version [2]byte) error {
if c.encryptor != nil && c.decryptor != nil {
return nil
}
clientRandom, serverRandom, err := c.mirror.handshakeRandom()
if err != nil {
return err
}
encryptTag := ":c2s"
decryptTag := ":s2c"
if c.isServer {
encryptTag = ":s2c"
decryptTag = ":c2s"
}
encKey, encMask, err := deriveEncryptionKey(c.primaryKey, clientRandom, serverRandom, encryptTag)
if err != nil {
return err
}
decKey, decMask, err := deriveEncryptionKey(c.primaryKey, clientRandom, serverRandom, decryptTag)
if err != nil {
return err
}
c.encryptor, err = newEncryptor(encKey, encMask)
if err != nil {
return err
}
c.decryptor, err = newDecryptor(decKey, decMask)
if err != nil {
return err
}
c.protocolVersion = version
if c.protocolVersion == [2]byte{} {
c.protocolVersion = [2]byte{0x03, 0x03}
}
return nil
}
func (c *Conn) handleInboundRecord(rec *record) (bool, error) {
if err := c.applySequenceWatermarkRx(rec); err != nil {
return false, err
}
if rec.recordType != recordTypeApplicationData {
return false, nil
}
c.mu.Lock()
err := c.ensureCryptoLocked(rec.version)
decryptor := c.decryptor
c.mu.Unlock()
if err != nil {
return false, nil
}
overhead := c.mirror.explicitNonceOverhead()
if len(rec.fragment) < overhead+decryptor.NonceSize() {
return false, nil
}
payload, err := decryptor.Open(nil, rec.fragment[overhead:])
if err != nil {
return false, nil
}
c.initSequenceWatermarkRx()
if c.config.TransportLayerPadding.Enabled {
payload, _ = unpackPadding(payload)
if payload == nil {
return true, nil
}
}
select {
case <-c.ctx.Done():
return true, c.ctx.Err()
case c.readCh <- payload:
return true, nil
}
}
func (c *Conn) Read(b []byte) (int, error) {
c.readMu.Lock()
defer c.readMu.Unlock()
for {
if c.readBuffer != nil {
n, _ := c.readBuffer.Read(b)
if n > 0 {
return n, nil
}
c.readBuffer = nil
}
select {
case <-c.ctx.Done():
return 0, c.ctx.Err()
case <-c.mirror.ctx.Done():
return 0, c.mirror.ctx.Err()
case <-c.readDeadline.Wait():
return 0, os.ErrDeadlineExceeded
case data := <-c.readCh:
c.readBuffer = bytes.NewBuffer(data)
}
}
}
func (c *Conn) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
writeDeadline := c.writeDeadline.Wait()
if err := c.waitWriteReady(writeDeadline); err != nil {
return 0, err
}
if c.firstWrite {
c.firstWrite = false
if c.firstWriteDelay > 0 {
timer := time.NewTimer(c.firstWriteDelay)
select {
case <-c.ctx.Done():
timer.Stop()
return 0, c.ctx.Err()
case <-c.mirror.ctx.Done():
timer.Stop()
return 0, c.mirror.ctx.Err()
case <-writeDeadline:
timer.Stop()
return 0, os.ErrDeadlineExceeded
case <-timer.C:
}
}
}
payloadSize := len(b)
c.mu.Lock()
if err := c.ensureCryptoLocked(c.protocolVersion); err != nil {
c.mu.Unlock()
return 0, err
}
encryptor := c.encryptor
version := c.protocolVersion
c.mu.Unlock()
overhead := c.mirror.explicitNonceOverhead()
maxPlaintext := maxTLSRecordPayload - overhead - encryptor.Overhead()
if c.config.TransportLayerPadding.Enabled {
maxPlaintext -= 4
}
if maxPlaintext <= 0 {
return 0, errors.New("tlsmirror: invalid tls record overhead")
}
for written := 0; written < len(b); {
end := written + maxPlaintext
if end > len(b) {
end = len(b)
}
plain := b[written:end]
if c.config.TransportLayerPadding.Enabled {
plain = packPadding(append([]byte(nil), plain...), 0)
}
fragment := make([]byte, overhead, overhead+len(plain)+encryptor.Overhead())
fragment = encryptor.Seal(fragment, plain)
rec := &record{
recordType: recordTypeApplicationData,
version: version,
fragment: fragment,
inserted: true,
}
var err error
if c.isServer {
err = c.insertS2C(rec, writeDeadline)
} else {
err = c.insertC2S(rec, writeDeadline)
}
if err != nil {
return written, err
}
written = end
}
return payloadSize, nil
}
func (c *Conn) waitWriteReady(deadline <-chan struct{}) error {
ready := c.mirror.c2sReady
if c.isServer {
ready = c.mirror.s2cReady
}
select {
case <-ready:
return nil
case <-c.ctx.Done():
return c.ctx.Err()
case <-c.mirror.ctx.Done():
return c.mirror.ctx.Err()
case <-deadline:
return os.ErrDeadlineExceeded
}
}
func (c *Conn) insertC2S(rec *record, deadline <-chan struct{}) error {
select {
case <-c.ctx.Done():
return c.ctx.Err()
case <-c.mirror.ctx.Done():
return c.mirror.ctx.Err()
case <-deadline:
return os.ErrDeadlineExceeded
case c.mirror.c2sInsert <- writeTask{rec: duplicateRecord(rec)}:
return nil
}
}
func (c *Conn) insertS2C(rec *record, deadline <-chan struct{}) error {
select {
case <-c.ctx.Done():
return c.ctx.Err()
case <-c.mirror.ctx.Done():
return c.mirror.ctx.Err()
case <-deadline:
return os.ErrDeadlineExceeded
case c.mirror.s2cInsert <- writeTask{rec: duplicateRecord(rec)}:
return nil
}
}
func (c *Conn) Close() error {
if c.recallTrafficGenerator != nil {
c.recallTrafficGenerator()
}
c.removeEnrollment()
c.cancel()
return c.mirror.Close()
}
func (c *Conn) removeEnrollment() {
c.enrollmentMu.Lock()
if c.enrollmentRemoved {
c.enrollmentMu.Unlock()
return
}
c.enrollmentRemoved = true
remove := c.enrollmentRemove
c.enrollmentRemove = nil
c.enrollmentMu.Unlock()
if remove != nil {
remove()
}
}
func (c *Conn) setEnrollmentRemove(remove func()) {
c.enrollmentMu.Lock()
if c.enrollmentRemoved {
c.enrollmentMu.Unlock()
if remove != nil {
remove()
}
return
}
c.enrollmentRemove = remove
c.enrollmentMu.Unlock()
}
func (c *Conn) addrConn() net.Conn {
if c.isServer {
return c.mirror.clientConn
}
return c.mirror.serverConn
}
func (c *Conn) LocalAddr() net.Addr {
return c.addrConn().LocalAddr()
}
func (c *Conn) RemoteAddr() net.Addr {
return c.addrConn().RemoteAddr()
}
func (c *Conn) SetDeadline(t time.Time) error {
if err := c.SetReadDeadline(t); err != nil {
return err
}
return c.SetWriteDeadline(t)
}
func (c *Conn) SetReadDeadline(t time.Time) error {
c.readDeadline.Set(t)
return nil
}
func (c *Conn) SetWriteDeadline(t time.Time) error {
c.writeDeadline.Set(t)
return nil
}
var _ net.Conn = (*Conn)(nil)
var errCarrierHandshake = errors.New("tlsmirror: carrier handshake failed")
func (c *Conn) applySequenceWatermarkRx(rec *record) error {
if !c.config.SequenceWatermarkingEnabled || c.watermarkRx == nil {
return nil
}
if rec.recordType != recordTypeApplicationData && rec.recordType != recordTypeAlert {
return nil
}
if len(rec.fragment) < 16 {
return nil
}
watermarkRegion := rec.fragment[len(rec.fragment)-16:]
c.watermarkRx.XORKeyStream(watermarkRegion, watermarkRegion)
return nil
}
func (c *Conn) handleOutboundRecordTx(rec *record) (bool, error) {
if !c.config.SequenceWatermarkingEnabled {
return false, nil
}
if c.watermarkTx != nil {
if (rec.recordType == recordTypeApplicationData || rec.recordType == recordTypeAlert) && len(rec.fragment) >= 16 {
watermarkRegion := rec.fragment[len(rec.fragment)-16:]
c.watermarkTx.XORKeyStream(watermarkRegion, watermarkRegion)
}
}
if rec.inserted && c.watermarkTx == nil {
if err := c.initSequenceWatermarkTx(); err != nil {
return true, nil
}
}
return false, nil
}
func (c *Conn) initSequenceWatermarkTx() error {
clientRandom, serverRandom, err := c.mirror.handshakeRandom()
if err != nil {
return err
}
tag := ":c2s"
if c.isServer {
tag = ":s2c"
}
c.watermarkTx, err = newSequenceWatermark(c.primaryKey, clientRandom, serverRandom, tag)
return err
}
func (c *Conn) initSequenceWatermarkRx() {
if !c.config.SequenceWatermarkingEnabled || c.watermarkRx != nil {
return
}
clientRandom, serverRandom, err := c.mirror.handshakeRandom()
if err != nil {
return
}
tag := ":s2c"
if c.isServer {
tag = ":c2s"
}
c.watermarkRx, _ = newSequenceWatermark(c.primaryKey, clientRandom, serverRandom, tag)
}
+193
View File
@@ -0,0 +1,193 @@
package tlsmirror
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"errors"
"io"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/hkdf"
)
type xorNonceAEAD struct {
nonceMask [12]byte
aead cipher.AEAD
}
func newXORNonceAEAD(key, nonceMask []byte) (cipher.AEAD, error) {
if len(nonceMask) != 12 {
return nil, errors.New("tlsmirror: invalid nonce mask size")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ret := &xorNonceAEAD{aead: aead}
copy(ret.nonceMask[:], nonceMask)
return ret, nil
}
func (a *xorNonceAEAD) NonceSize() int {
return 8
}
func (a *xorNonceAEAD) Overhead() int {
return a.aead.Overhead()
}
func (a *xorNonceAEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
mask := a.nonceMask
for i, b := range nonce {
mask[4+i] ^= b
}
return a.aead.Seal(dst, mask[:], plaintext, additionalData)
}
func (a *xorNonceAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
mask := a.nonceMask
for i, b := range nonce {
mask[4+i] ^= b
}
return a.aead.Open(dst, mask[:], ciphertext, additionalData)
}
func deriveEncryptionKey(primaryKey []byte, clientRandom, serverRandom [32]byte, tag string) ([]byte, []byte, error) {
if len(primaryKey) != 32 {
return nil, nil, errors.New("tlsmirror: invalid primary key size")
}
combined := make([]byte, 0, 96)
combined = append(combined, primaryKey...)
combined = append(combined, clientRandom[:]...)
combined = append(combined, serverRandom[:]...)
encryptionKey := make([]byte, 16)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, combined, []byte("v2ray-sp76YMKM-EkGrFUNL-rTJRJMkU:tlsmirror-encryption"+tag)), encryptionKey); err != nil {
return nil, nil, err
}
nonceMask := make([]byte, 12)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, combined, []byte("v2ray-sp76YMKM-EkGrFUNL-rTJRJMkU:tlsmirror-noncemask"+tag)), nonceMask); err != nil {
return nil, nil, err
}
return encryptionKey, nonceMask, nil
}
func deriveSequenceWatermarkingKey(primaryKey []byte, clientRandom, serverRandom [32]byte, tag string) ([]byte, []byte, error) {
if len(primaryKey) != 32 {
return nil, nil, errors.New("tlsmirror: invalid primary key size")
}
combined := make([]byte, 0, 96)
combined = append(combined, primaryKey...)
combined = append(combined, clientRandom[:]...)
combined = append(combined, serverRandom[:]...)
encryptionKey := make([]byte, chacha20.KeySize)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, combined, []byte("v2ray-xv64FXUU-GxMn8UYz-bTy6UDeE:tlsmirror-sequence-watermark-encryption"+tag)), encryptionKey); err != nil {
return nil, nil, err
}
nonce := make([]byte, chacha20.NonceSizeX)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, combined, []byte("v2ray-xv64FXUU-GxMn8UYz-bTy6UDeE:tlsmirror-sequence-watermark-noncemask"+tag)), nonce); err != nil {
return nil, nil, err
}
return encryptionKey, nonce, nil
}
func newSequenceWatermark(primaryKey []byte, clientRandom, serverRandom [32]byte, tag string) (cipher.Stream, error) {
key, nonce, err := deriveSequenceWatermarkingKey(primaryKey, clientRandom, serverRandom, tag)
if err != nil {
return nil, err
}
return chacha20.NewUnauthenticatedCipher(key, nonce)
}
type nonceGenerator struct {
next [8]byte
}
func newNonceGenerator() nonceGenerator {
return nonceGenerator{next: [8]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}
}
func (g *nonceGenerator) Next() []byte {
for i := range g.next {
g.next[i]++
if g.next[i] != 0 {
break
}
}
return g.next[:]
}
type explicitNonceGenerator struct {
next [8]byte
}
func (g *explicitNonceGenerator) Next() []byte {
for i := len(g.next) - 1; i >= 0; i-- {
g.next[i]++
if g.next[i] != 0 {
break
}
}
return g.next[:]
}
type encryptor struct {
nonce nonceGenerator
aead cipher.AEAD
}
func newEncryptor(key, mask []byte) (*encryptor, error) {
aead, err := newXORNonceAEAD(key, mask)
if err != nil {
return nil, err
}
return &encryptor{nonce: newNonceGenerator(), aead: aead}, nil
}
func (e *encryptor) Seal(dst, src []byte) []byte {
return e.aead.Seal(dst, e.nonce.Next(), src, nil)
}
func (e *encryptor) NonceSize() int {
return e.aead.NonceSize()
}
func (e *encryptor) Overhead() int {
return e.aead.Overhead()
}
type decryptor struct {
nonce nonceGenerator
aead cipher.AEAD
nextNonce []byte
}
func newDecryptor(key, mask []byte) (*decryptor, error) {
aead, err := newXORNonceAEAD(key, mask)
if err != nil {
return nil, err
}
return &decryptor{nonce: newNonceGenerator(), aead: aead}, nil
}
func (d *decryptor) Open(dst, src []byte) ([]byte, error) {
if d.nextNonce == nil {
d.nextNonce = append([]byte(nil), d.nonce.Next()...)
}
out, err := d.aead.Open(dst, d.nextNonce, src, nil)
if err != nil {
return nil, err
}
d.nextNonce = nil
return out, nil
}
func (d *decryptor) NonceSize() int {
return d.aead.NonceSize()
}
+486
View File
@@ -0,0 +1,486 @@
package tlsmirror
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base32"
"errors"
"fmt"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/metacubex/http"
"golang.org/x/crypto/hkdf"
)
const enrollmentControlConnectionPostfix = ".tlsmirror-controlconnection.v2fly.arpa"
var enrollmentBase32 = base32.NewEncoding("0123456789abcdefghijklmnopqrstuv").WithPadding(base32.NoPadding)
type connectionLoopbackPrevention struct {
Key string
}
type enrollmentBypassContextKey struct{}
type enrollmentConfirmationReq struct {
serverIdentifier []byte
clientRandom []byte
serverRandom []byte
clientIdentifier []byte
replyAddressTag []byte
}
func WithConnectionEnrollmentBypass(ctx context.Context) context.Context {
return context.WithValue(ctx, enrollmentBypassContextKey{}, true)
}
func isConnectionEnrollmentBypassed(ctx context.Context) bool {
bypassed, _ := ctx.Value(enrollmentBypassContextKey{}).(bool)
return bypassed
}
func WithLoopbackProtection(ctx context.Context, enrollmentID []byte) context.Context {
return context.WithValue(ctx, connectionLoopbackPrevention{Key: string(enrollmentID)}, true)
}
func WithSecondaryLoopbackProtection(ctx context.Context, enrollmentID []byte) context.Context {
return context.WithValue(ctx, connectionLoopbackPrevention{Key: string(enrollmentID)}, false)
}
func IsLoopbackProtectionEnabled(ctx context.Context, enrollmentID []byte) bool {
enabled, ok := ctx.Value(connectionLoopbackPrevention{Key: string(enrollmentID)}).(bool)
return ok && enabled
}
func ServerIdentifierHost(primaryKey []byte) (string, error) {
serverID, err := deriveEnrollmentServerIdentifier(primaryKey)
if err != nil {
return "", err
}
return enrollmentBase32.EncodeToString(serverID) + enrollmentControlConnectionPostfix, nil
}
func ServeEnrollmentControlConnection(ctx context.Context, conn net.Conn, primaryKey string) error {
key, err := DecodePrimaryKey(primaryKey)
if err != nil {
_ = conn.Close()
return err
}
processor := enrollmentProcessorFor(key)
listener := newSingleConnListener(conn)
server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read request body: "+err.Error(), http.StatusInternalServerError)
return
}
req, err := unmarshalEnrollmentConfirmationReq(body)
if err != nil {
http.Error(w, "failed to unmarshal request: "+err.Error(), http.StatusBadRequest)
return
}
enrolled, err := processor.verify(req)
if err != nil {
http.Error(w, "failed to verify connection enrollment: "+err.Error(), http.StatusInternalServerError)
return
}
responseBody := marshalEnrollmentConfirmationResp(enrolled)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.Itoa(len(responseBody)))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(responseBody)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}),
Protocols: new(http.Protocols),
}
server.Protocols.SetHTTP2(true)
server.Protocols.SetUnencryptedHTTP2(true)
go func() {
<-ctx.Done()
_ = server.Close()
_ = listener.Close()
}()
err = server.Serve(listener)
if errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
func (c *Conn) verifyConnectionEnrollment(ctx context.Context, cfg ClientConfig) error {
if cfg.EnrollmentDialer == nil {
return errors.New("tlsmirror: connection enrolment requires an enrollment dialer")
}
clientRandom, serverRandom, err := c.mirror.handshakeRandom()
if err != nil {
return err
}
serverID, err := deriveEnrollmentServerIdentifier(c.primaryKey)
if err != nil {
return err
}
host := enrollmentBase32.EncodeToString(serverID) + enrollmentControlConnectionPostfix
requestCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
controlCtx := WithLoopbackProtection(requestCtx, serverID)
if cfg.ConnectionEnrolment.PrimaryEgressOutbound == "" {
controlCtx = WithSecondaryLoopbackProtection(requestCtx, serverID)
}
controlCtx = WithConnectionEnrollmentBypass(controlCtx)
controlConn, err := cfg.EnrollmentDialer(controlCtx, "tcp", net.JoinHostPort(host, "80"))
if err != nil {
return err
}
defer controlConn.Close()
reqBody := marshalEnrollmentConfirmationReq(enrollmentConfirmationReq{
serverIdentifier: serverID,
clientRandom: clientRandom[:],
serverRandom: serverRandom[:],
})
transport, err := newTrafficHTTPTransport(requestCtx, controlConn, "h2")
if err != nil {
return err
}
httpReq, err := http.NewRequestWithContext(requestCtx, http.MethodPost, "http://"+host, bytes.NewReader(reqBody))
if err != nil {
return err
}
httpResp, err := transport.RoundTrip(httpReq)
if httpResp != nil && httpResp.Body != nil {
defer httpResp.Body.Close()
}
if err != nil {
return err
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("tlsmirror: unexpected enrollment response status %d", httpResp.StatusCode)
}
respBody, err := io.ReadAll(httpResp.Body)
if err != nil {
return err
}
enrolled, err := unmarshalEnrollmentConfirmationResp(respBody)
if err != nil {
return err
}
if !enrolled {
return errors.New("tlsmirror: connection enrollment failed")
}
return nil
}
func deriveEnrollmentServerIdentifier(primaryKey []byte) ([]byte, error) {
return deriveSecondaryKey(primaryKey, ":connection-enrollment-server-identifier-av38NNGF-TJvRw7C3-p8KM8yKd")
}
func deriveSecondaryKey(primaryKey []byte, tag string) ([]byte, error) {
if len(primaryKey) != 32 {
return nil, errors.New("tlsmirror: invalid primary key size")
}
key := make([]byte, 16)
if _, err := io.ReadFull(hkdf.Expand(sha256.New, primaryKey, []byte("v2ray-sv77RCEY-e8AhYsbD-BmFC7XRK:tlsmirror-secondary"+tag)), key); err != nil {
return nil, err
}
return key, nil
}
func deriveEnrollmentRequestKey(primaryKey []byte, clientRandom, serverRandom [32]byte) ([]byte, error) {
requestKey, _, err := deriveEncryptionKey(primaryKey, clientRandom, serverRandom, ":connection-enrollment-re78HQNM-CmpRnPbr-PNJVRMhu")
return requestKey, err
}
var enrollmentProcessors sync.Map
type enrollmentProcessor struct {
primaryKey []byte
active sync.Map
}
func enrollmentProcessorFor(primaryKey []byte) *enrollmentProcessor {
key := string(primaryKey)
if value, ok := enrollmentProcessors.Load(key); ok {
return value.(*enrollmentProcessor)
}
processor := &enrollmentProcessor{primaryKey: append([]byte(nil), primaryKey...)}
value, _ := enrollmentProcessors.LoadOrStore(key, processor)
return value.(*enrollmentProcessor)
}
func (p *enrollmentProcessor) add(clientRandom, serverRandom [32]byte, conn *Conn) (func(), error) {
requestKey, err := deriveEnrollmentRequestKey(p.primaryKey, clientRandom, serverRandom)
if err != nil {
return nil, err
}
if _, loaded := p.active.LoadOrStore(string(requestKey), conn); loaded {
return nil, errors.New("tlsmirror: enrollment connection already exists")
}
return func() {
p.active.Delete(string(requestKey))
}, nil
}
func (p *enrollmentProcessor) verify(req enrollmentConfirmationReq) (bool, error) {
if len(req.clientRandom) != 32 || len(req.serverRandom) != 32 {
return false, errors.New("tlsmirror: enrollment request is missing handshake random")
}
var clientRandom, serverRandom [32]byte
copy(clientRandom[:], req.clientRandom)
copy(serverRandom[:], req.serverRandom)
requestKey, err := deriveEnrollmentRequestKey(p.primaryKey, clientRandom, serverRandom)
if err != nil {
return false, err
}
_, ok := p.active.Load(string(requestKey))
return ok, nil
}
func marshalEnrollmentConfirmationReq(req enrollmentConfirmationReq) []byte {
var out []byte
out = appendProtoBytes(out, 1, req.serverIdentifier)
out = appendProtoBytes(out, 2, req.clientRandom)
out = appendProtoBytes(out, 3, req.serverRandom)
out = appendProtoBytes(out, 4, req.clientIdentifier)
out = appendProtoBytes(out, 5, req.replyAddressTag)
return out
}
func unmarshalEnrollmentConfirmationReq(data []byte) (enrollmentConfirmationReq, error) {
var req enrollmentConfirmationReq
for len(data) > 0 {
key, n, err := consumeProtoVarint(data)
if err != nil {
return req, err
}
data = data[n:]
field := int(key >> 3)
wireType := key & 0x7
if field == 0 {
return req, errors.New("tlsmirror: invalid protobuf field number")
}
if wireType == 2 {
size, n, err := consumeProtoVarint(data)
if err != nil {
return req, err
}
data = data[n:]
if uint64(len(data)) < size {
return req, io.ErrUnexpectedEOF
}
value := append([]byte(nil), data[:size]...)
data = data[size:]
switch field {
case 1:
req.serverIdentifier = value
case 2:
req.clientRandom = value
case 3:
req.serverRandom = value
case 4:
req.clientIdentifier = value
case 5:
req.replyAddressTag = value
}
continue
}
n, err = skipProtoValue(data, field, wireType)
if err != nil {
return req, err
}
data = data[n:]
}
return req, nil
}
func marshalEnrollmentConfirmationResp(enrolled bool) []byte {
if !enrolled {
return nil
}
out := appendProtoVarint(nil, 1<<3)
return appendProtoVarint(out, 1)
}
func unmarshalEnrollmentConfirmationResp(data []byte) (bool, error) {
var enrolled bool
for len(data) > 0 {
key, n, err := consumeProtoVarint(data)
if err != nil {
return false, err
}
data = data[n:]
field := int(key >> 3)
wireType := key & 0x7
if field == 0 {
return false, errors.New("tlsmirror: invalid protobuf field number")
}
if field == 1 && wireType == 0 {
value, n, err := consumeProtoVarint(data)
if err != nil {
return false, err
}
data = data[n:]
enrolled = value != 0
continue
}
n, err = skipProtoValue(data, field, wireType)
if err != nil {
return false, err
}
data = data[n:]
}
return enrolled, nil
}
func appendProtoBytes(out []byte, field int, value []byte) []byte {
if len(value) == 0 {
return out
}
out = appendProtoVarint(out, uint64(field<<3|2))
out = appendProtoVarint(out, uint64(len(value)))
return append(out, value...)
}
func appendProtoVarint(out []byte, value uint64) []byte {
for value >= 0x80 {
out = append(out, byte(value)|0x80)
value >>= 7
}
return append(out, byte(value))
}
func consumeProtoVarint(data []byte) (uint64, int, error) {
var value uint64
for i, b := range data {
if i == 10 {
return 0, 0, errors.New("tlsmirror: invalid protobuf varint")
}
value |= uint64(b&0x7f) << (7 * i)
if b < 0x80 {
return value, i + 1, nil
}
}
return 0, 0, io.ErrUnexpectedEOF
}
func skipProtoValue(data []byte, startField int, wireType uint64) (int, error) {
switch wireType {
case 0:
_, n, err := consumeProtoVarint(data)
return n, err
case 1:
if len(data) < 8 {
return 0, io.ErrUnexpectedEOF
}
return 8, nil
case 2:
size, n, err := consumeProtoVarint(data)
if err != nil {
return 0, err
}
data = data[n:]
if uint64(len(data)) < size {
return 0, io.ErrUnexpectedEOF
}
return n + int(size), nil
case 3:
consumed := 0
for {
key, n, err := consumeProtoVarint(data)
if err != nil {
return 0, err
}
data = data[n:]
consumed += n
nestedField := int(key >> 3)
nestedWireType := key & 0x7
if nestedField == 0 {
return 0, errors.New("tlsmirror: invalid protobuf field number")
}
if nestedWireType == 4 {
if nestedField != startField {
return 0, errors.New("tlsmirror: mismatched protobuf end group")
}
return consumed, nil
}
n, err = skipProtoValue(data, nestedField, nestedWireType)
if err != nil {
return 0, err
}
data = data[n:]
consumed += n
}
case 4:
return 0, errors.New("tlsmirror: unexpected protobuf end group")
case 5:
if len(data) < 4 {
return 0, io.ErrUnexpectedEOF
}
return 4, nil
default:
return 0, fmt.Errorf("tlsmirror: unsupported enrollment protobuf wire type %d", wireType)
}
}
type singleConnListener struct {
mu sync.Mutex
conn net.Conn
done chan struct{}
once sync.Once
}
func newSingleConnListener(conn net.Conn) *singleConnListener {
return &singleConnListener{
conn: conn,
done: make(chan struct{}),
}
}
func (l *singleConnListener) Accept() (net.Conn, error) {
l.mu.Lock()
if l.conn != nil {
conn := l.conn
l.conn = nil
l.mu.Unlock()
return &notifyCloseConn{Conn: conn, closeNotify: l.Close}, nil
}
l.mu.Unlock()
<-l.done
return nil, net.ErrClosed
}
func (l *singleConnListener) Close() error {
l.once.Do(func() {
close(l.done)
})
return nil
}
func (l *singleConnListener) Addr() net.Addr {
l.mu.Lock()
conn := l.conn
l.mu.Unlock()
if conn != nil {
return conn.LocalAddr()
}
return &net.TCPAddr{}
}
type notifyCloseConn struct {
net.Conn
closeNotify func() error
}
func (c *notifyCloseConn) Close() error {
_ = c.closeNotify()
return c.Conn.Close()
}
+483
View File
@@ -0,0 +1,483 @@
package tlsmirror
import (
"bufio"
"bytes"
"context"
"errors"
"io"
"net"
"sync"
)
type messageHook func(*record) (drop bool, err error)
type writeTask struct {
rec *record
raw []byte
fallback *bufio.Reader
}
type mirrorConn struct {
ctx context.Context
cancel context.CancelFunc
clientConn net.Conn
serverConn net.Conn
onC2SMessage messageHook
onS2CMessage messageHook
onC2SMessageTx messageHook
onS2CMessageTx messageHook
c2sInsert chan writeTask
s2cInsert chan writeTask
c2sReady chan struct{}
s2cReady chan struct{}
onClose func()
randomMu sync.RWMutex
clientRandom [32]byte
serverRandom [32]byte
clientRandomReady bool
serverRandomReady bool
c2sReadyOnce sync.Once
s2cReadyOnce sync.Once
tls12Explicit bool
explicitReady chan struct{}
explicitSuites map[uint16]struct{}
c2sExplicitNonce explicitNonceGenerator
s2cExplicitNonce explicitNonceGenerator
}
func newMirrorConn(ctx context.Context, clientConn, serverConn net.Conn, cfg Config, onC2S, onS2C, onC2STx, onS2CTx messageHook) *mirrorConn {
mctx, cancel := context.WithCancel(ctx)
explicitSuites := make(map[uint16]struct{}, len(cfg.ExplicitNonceCipherSuites))
for _, suite := range cfg.ExplicitNonceCipherSuites {
explicitSuites[suite] = struct{}{}
}
return &mirrorConn{
ctx: mctx,
cancel: cancel,
clientConn: clientConn,
serverConn: serverConn,
onC2SMessage: onC2S,
onS2CMessage: onS2C,
onC2SMessageTx: onC2STx,
onS2CMessageTx: onS2CTx,
c2sInsert: make(chan writeTask, 100),
s2cInsert: make(chan writeTask, 100),
c2sReady: make(chan struct{}),
s2cReady: make(chan struct{}),
explicitReady: make(chan struct{}),
explicitSuites: explicitSuites,
}
}
func (m *mirrorConn) start() {
go m.c2sWorker()
go m.s2cWorker()
go func() {
<-m.ctx.Done()
if m.onClose != nil {
m.onClose()
}
_ = m.clientConn.Close()
_ = m.serverConn.Close()
}()
}
func (m *mirrorConn) Close() error {
m.cancel()
return nil
}
func (m *mirrorConn) handshakeRandom() ([32]byte, [32]byte, error) {
m.randomMu.RLock()
defer m.randomMu.RUnlock()
if !m.clientRandomReady || !m.serverRandomReady {
return [32]byte{}, [32]byte{}, errors.New("tlsmirror: handshake random is not ready")
}
return m.clientRandom, m.serverRandom, nil
}
func (m *mirrorConn) explicitNonceOverhead() int {
select {
case <-m.explicitReady:
case <-m.ctx.Done():
return 0
}
if m.tls12Explicit {
return 8
}
return 0
}
func (m *mirrorConn) waitC2SReady(ctx context.Context) error {
select {
case <-m.c2sReady:
return nil
case <-m.ctx.Done():
return m.ctx.Err()
case <-ctx.Done():
return ctx.Err()
}
}
func (m *mirrorConn) waitS2CReady(ctx context.Context) error {
select {
case <-m.s2cReady:
return nil
case <-m.ctx.Done():
return m.ctx.Err()
case <-ctx.Done():
return ctx.Err()
}
}
func (m *mirrorConn) InsertC2S(rec *record) error {
select {
case <-m.ctx.Done():
return m.ctx.Err()
case m.c2sInsert <- writeTask{rec: duplicateRecord(rec)}:
return nil
}
}
func (m *mirrorConn) InsertS2C(rec *record) error {
select {
case <-m.ctx.Done():
return m.ctx.Err()
case m.s2cInsert <- writeTask{rec: duplicateRecord(rec)}:
return nil
}
}
func (m *mirrorConn) c2sWorker() {
serverWriter := bufio.NewWriterSize(m.serverConn, 65536)
first, clientReader, firstRaw, err := m.captureFirstHandshakeRecord(m.clientConn, serverWriter)
if err != nil {
return
}
clientRandom, err := parseClientRandom(first.fragment)
if err != nil {
m.fallbackDirectCopy(serverWriter, clientReader, firstRaw)
return
}
m.randomMu.Lock()
m.clientRandom = clientRandom
m.clientRandomReady = true
m.randomMu.Unlock()
if err := writeRawFlush(serverWriter, firstRaw); err != nil {
m.cancel()
return
}
go m.recordWriter(serverWriter, m.c2sInsert, m.onC2SMessageTx, true)
explicitNonceSessionAndChangeCipherSpecWasLastMessage := false
for m.ctx.Err() == nil {
rec, raw, err := readRecord(clientReader)
if err != nil {
m.fallbackQueuedCopy(m.c2sInsert, clientReader, nil, raw)
return
}
if rec.recordType == recordTypeHandshake && explicitNonceSessionAndChangeCipherSpecWasLastMessage && !hasZeroExplicitNonce(rec.fragment) {
m.fallbackQueuedCopy(m.c2sInsert, clientReader, rec, nil)
return
}
if rec.recordType == recordTypeChangeCipherSpec {
select {
case <-m.explicitReady:
default:
m.fallbackQueuedCopy(m.c2sInsert, clientReader, rec, nil)
return
}
}
if m.onC2SMessage != nil {
drop, err := m.onC2SMessage(rec)
if err != nil {
m.fallbackQueuedCopy(m.c2sInsert, clientReader, rec, nil)
return
}
if drop {
continue
}
}
if err := m.InsertC2S(rec); err != nil {
m.cancel()
return
}
if rec.recordType == recordTypeChangeCipherSpec && m.tls12Explicit {
explicitNonceSessionAndChangeCipherSpecWasLastMessage = true
continue
}
if rec.recordType == recordTypeApplicationData || rec.recordType == recordTypeHandshake && explicitNonceSessionAndChangeCipherSpecWasLastMessage {
m.c2sReadyOnce.Do(func() {
close(m.c2sReady)
})
}
explicitNonceSessionAndChangeCipherSpecWasLastMessage = false
}
}
func (m *mirrorConn) s2cWorker() {
clientWriter := bufio.NewWriterSize(m.clientConn, 65536)
first, serverReader, firstRaw, err := m.captureFirstHandshakeRecord(m.serverConn, clientWriter)
if err != nil {
return
}
serverRandom, cipherSuite, err := parseServerHello(first.fragment)
if err != nil {
m.fallbackDirectCopy(clientWriter, serverReader, firstRaw)
return
}
m.randomMu.Lock()
m.serverRandom = serverRandom
m.serverRandomReady = true
_, m.tls12Explicit = m.explicitSuites[cipherSuite]
m.randomMu.Unlock()
close(m.explicitReady)
if m.onS2CMessage != nil {
drop, err := m.onS2CMessage(first)
if err != nil {
m.fallbackDirectCopy(clientWriter, serverReader, firstRaw)
return
}
_ = drop
}
if err := writeRawFlush(clientWriter, firstRaw); err != nil {
m.cancel()
return
}
go m.recordWriter(clientWriter, m.s2cInsert, m.onS2CMessageTx, false)
explicitNonceSessionAndChangeCipherSpecWasLastMessage := false
for m.ctx.Err() == nil {
rec, raw, err := readRecord(serverReader)
if err != nil {
m.fallbackQueuedCopy(m.s2cInsert, serverReader, nil, raw)
return
}
if rec.recordType == recordTypeHandshake && explicitNonceSessionAndChangeCipherSpecWasLastMessage && !hasZeroExplicitNonce(rec.fragment) {
m.fallbackQueuedCopy(m.s2cInsert, serverReader, rec, nil)
return
}
if m.onS2CMessage != nil {
drop, err := m.onS2CMessage(rec)
if err != nil {
m.fallbackQueuedCopy(m.s2cInsert, serverReader, rec, nil)
return
}
if drop {
continue
}
}
if err := m.InsertS2C(rec); err != nil {
m.cancel()
return
}
if rec.recordType == recordTypeChangeCipherSpec && m.tls12Explicit {
explicitNonceSessionAndChangeCipherSpecWasLastMessage = true
continue
}
if rec.recordType == recordTypeApplicationData || rec.recordType == recordTypeHandshake && explicitNonceSessionAndChangeCipherSpecWasLastMessage {
m.s2cReadyOnce.Do(func() {
close(m.s2cReady)
})
}
explicitNonceSessionAndChangeCipherSpecWasLastMessage = false
}
}
func (m *mirrorConn) fallbackDirectCopy(writer *bufio.Writer, src *bufio.Reader, raw []byte) {
if err := writeRawFlush(writer, raw); err != nil {
m.cancel()
return
}
_ = copyFlush(writer, src)
m.cancel()
}
func (m *mirrorConn) captureFirstHandshakeRecord(src net.Conn, dst *bufio.Writer) (*record, *bufio.Reader, []byte, error) {
var readBuffer [65536]byte
var copied int
for m.ctx.Err() == nil {
n, err := src.Read(readBuffer[copied:])
if err != nil {
m.cancel()
return nil, nil, nil, err
}
buffer := readBuffer[:copied+n]
rec, needMore, processed, err := peekFirstHandshakeRecord(buffer)
if processed == 0 {
if needMore == 0 {
_, _ = dst.Write(buffer)
_ = dst.Flush()
_ = copyFlush(dst, src)
m.cancel()
return nil, nil, nil, err
}
if _, err := dst.Write(readBuffer[copied : copied+n]); err != nil {
m.cancel()
return nil, nil, nil, err
}
if err := dst.Flush(); err != nil {
m.cancel()
return nil, nil, nil, err
}
copied += n
continue
}
raw := append([]byte(nil), readBuffer[copied:processed]...)
rest := append([]byte(nil), buffer[processed:]...)
return rec, bufio.NewReaderSize(io.MultiReader(bytes.NewReader(rest), src), 65536), raw, nil
}
return nil, nil, nil, m.ctx.Err()
}
func peekFirstHandshakeRecord(buffer []byte) (*record, int, int, error) {
if len(buffer) < 5 {
return nil, 5, 0, nil
}
if buffer[0] != recordTypeHandshake {
return nil, 0, 0, errors.New("tlsmirror: unexpected first tls record type")
}
switch buffer[1] {
case 0x01, 0x02:
case 0x03:
if buffer[2] > 0x03 {
return nil, 0, 0, errors.New("tlsmirror: unexpected first tls record version")
}
default:
return nil, 0, 0, errors.New("tlsmirror: unexpected first tls record version")
}
length := int(buffer[3])<<8 | int(buffer[4])
if length > maxTLSRecordPayload {
return nil, 0, 0, errors.New("tlsmirror: tls record is too large")
}
processed := 5 + length
if len(buffer) < processed {
return nil, processed, 0, nil
}
return &record{
recordType: buffer[0],
version: [2]byte{buffer[1], buffer[2]},
fragment: append([]byte(nil), buffer[5:processed]...),
}, 0, processed, nil
}
func (m *mirrorConn) fallbackQueuedCopy(ch chan<- writeTask, src *bufio.Reader, first *record, raw []byte) {
var rec *record
if first != nil {
rec = duplicateRecord(first)
}
raw = append([]byte(nil), raw...)
select {
case <-m.ctx.Done():
case ch <- writeTask{rec: rec, raw: raw, fallback: src}:
}
}
func copyFlush(writer *bufio.Writer, src io.Reader) error {
buf := make([]byte, 32*1024)
for {
n, readErr := src.Read(buf)
if n > 0 {
if _, err := writer.Write(buf[:n]); err != nil {
return err
}
if err := writer.Flush(); err != nil {
return err
}
}
if readErr != nil {
if readErr == io.EOF {
return nil
}
return readErr
}
}
}
func writeRawFlush(writer *bufio.Writer, raw []byte) error {
if len(raw) == 0 {
return nil
}
if _, err := writer.Write(raw); err != nil {
return err
}
return writer.Flush()
}
func (m *mirrorConn) recordWriter(writer *bufio.Writer, ch <-chan writeTask, hook messageHook, c2s bool) {
for m.ctx.Err() == nil {
select {
case <-m.ctx.Done():
return
case task := <-ch:
rec := task.rec
if rec != nil {
m.fillExplicitNonce(rec, c2s)
if hook != nil {
drop, err := hook(rec)
if err != nil {
m.cancel()
return
}
if drop {
continue
}
}
if err := writeRecord(writer, rec); err != nil {
m.cancel()
return
}
if rec.recordType == recordTypeAlert {
m.cancel()
return
}
}
if len(task.raw) > 0 {
if _, err := writer.Write(task.raw); err != nil {
m.cancel()
return
}
if err := writer.Flush(); err != nil {
m.cancel()
return
}
}
if task.fallback != nil {
_ = copyFlush(writer, task.fallback)
m.cancel()
return
}
}
}
}
func (m *mirrorConn) fillExplicitNonce(rec *record, c2s bool) {
if !rec.inserted || len(rec.fragment) < 8 {
return
}
select {
case <-m.explicitReady:
default:
return
}
if !m.tls12Explicit {
return
}
if rec.recordType != recordTypeApplicationData && rec.recordType != recordTypeAlert {
return
}
// recordWriter owns each direction's insert queue, so the generators do not
// need additional synchronization here.
nonce := m.s2cExplicitNonce.Next()
if c2s {
nonce = m.c2sExplicitNonce.Next()
}
copy(rec.fragment[:8], nonce)
}
+26
View File
@@ -0,0 +1,26 @@
package tlsmirror
import "encoding/binary"
func packPadding(data []byte, paddingLength int) []byte {
dataLength := len(data)
data = append(data, make([]byte, paddingLength)...)
data = binary.BigEndian.AppendUint32(data, uint32(dataLength))
return data
}
func unpackPadding(data []byte) ([]byte, int) {
dataLength := len(data)
if dataLength < 4 {
return nil, dataLength
}
payloadLength := int(binary.BigEndian.Uint32(data[dataLength-4:]))
if payloadLength > dataLength-4 {
return nil, 0
}
paddingLength := dataLength - payloadLength - 4
if paddingLength < 0 {
return nil, paddingLength
}
return data[:payloadLength], paddingLength
}
+106
View File
@@ -0,0 +1,106 @@
package tlsmirror
import (
"bufio"
"encoding/binary"
"errors"
"io"
)
const (
recordTypeChangeCipherSpec = 20
recordTypeAlert = 21
recordTypeHandshake = 22
recordTypeApplicationData = 23
maxTLSRecordPayload = 16384
)
type record struct {
recordType byte
version [2]byte
fragment []byte
inserted bool
}
func readRecord(reader *bufio.Reader) (*record, []byte, error) {
header := make([]byte, 5)
n, err := io.ReadFull(reader, header)
if err != nil {
return nil, header[:n], err
}
length := int(binary.BigEndian.Uint16(header[3:5]))
if length > maxTLSRecordPayload {
return nil, header, errors.New("tlsmirror: tls record is too large")
}
fragment := make([]byte, length)
n, err = io.ReadFull(reader, fragment)
raw := append(append([]byte(nil), header...), fragment[:n]...)
if err != nil {
return nil, raw, err
}
return &record{
recordType: header[0],
version: [2]byte{header[1], header[2]},
fragment: fragment,
}, raw, nil
}
func writeRecord(writer *bufio.Writer, rec *record) error {
if len(rec.fragment) > maxTLSRecordPayload {
return errors.New("tlsmirror: tls record is too large")
}
var header [5]byte
header[0] = rec.recordType
header[1] = rec.version[0]
header[2] = rec.version[1]
binary.BigEndian.PutUint16(header[3:5], uint16(len(rec.fragment)))
if _, err := writer.Write(header[:]); err != nil {
return err
}
if _, err := writer.Write(rec.fragment); err != nil {
return err
}
return writer.Flush()
}
func duplicateRecord(rec *record) *record {
dup := *rec
dup.fragment = append([]byte(nil), rec.fragment...)
return &dup
}
func parseClientRandom(fragment []byte) ([32]byte, error) {
var random [32]byte
if len(fragment) < 38 || fragment[0] != 1 {
return random, errors.New("tlsmirror: invalid client hello")
}
copy(random[:], fragment[6:38])
return random, nil
}
func parseServerHello(fragment []byte) ([32]byte, uint16, error) {
var random [32]byte
if len(fragment) < 41 || fragment[0] != 2 {
return random, 0, errors.New("tlsmirror: invalid server hello")
}
copy(random[:], fragment[6:38])
sessionIDLen := int(fragment[38])
cipherSuiteOffset := 39 + sessionIDLen
if len(fragment) < cipherSuiteOffset+2 {
return random, 0, errors.New("tlsmirror: invalid server hello session id")
}
return random, binary.BigEndian.Uint16(fragment[cipherSuiteOffset : cipherSuiteOffset+2]), nil
}
func hasZeroExplicitNonce(fragment []byte) bool {
if len(fragment) < 8 {
return false
}
for _, b := range fragment[:8] {
if b != 0 {
return false
}
}
return true
}
+81
View File
@@ -0,0 +1,81 @@
package tlsmirror
import (
"context"
"net"
)
func ServeConnReady(ctx context.Context, carrierConn net.Conn, forwardConn net.Conn, cfg ServerConfig) (net.Conn, error) {
ready := make(chan *Conn, 1)
mirror, err := serveConn(ctx, carrierConn, forwardConn, cfg, func(hidden *Conn) {
select {
case ready <- hidden:
default:
}
})
if err != nil {
return nil, err
}
select {
case hidden := <-ready:
return hidden, nil
case <-mirror.ctx.Done():
return nil, mirror.ctx.Err()
case <-ctx.Done():
_ = mirror.Close()
return nil, ctx.Err()
}
}
func serveConn(ctx context.Context, carrierConn net.Conn, forwardConn net.Conn, cfg ServerConfig, onReady func(*Conn)) (*mirrorConn, error) {
key, err := DecodePrimaryKey(cfg.PrimaryKey)
if err != nil {
_ = carrierConn.Close()
_ = forwardConn.Close()
return nil, err
}
var hidden *Conn
var activated bool
var enrolled bool
mirror := newMirrorConn(ctx, carrierConn, forwardConn,
cfg,
func(rec *record) (bool, error) {
drop, err := hidden.handleInboundRecord(rec)
if drop {
if !activated {
activated = true
onReady(hidden)
}
}
return drop, err
},
func(rec *record) (bool, error) {
if cfg.ConnectionEnrolment != nil && rec.recordType == recordTypeHandshake && !enrolled {
clientRandom, serverRandom, randomErr := hidden.mirror.handshakeRandom()
if randomErr != nil {
return false, nil
}
remove, addErr := enrollmentProcessorFor(key).add(clientRandom, serverRandom, hidden)
if addErr != nil {
return false, nil
}
hidden.setEnrollmentRemove(remove)
enrolled = true
}
return false, nil
},
nil,
nil,
)
hidden, err = newHiddenConn(ctx, mirror, key, true, cfg)
if err != nil {
_ = carrierConn.Close()
_ = forwardConn.Close()
return nil, err
}
mirror.onClose = hidden.removeEnrollment
mirror.onS2CMessageTx = hidden.handleOutboundRecordTx
mirror.start()
return mirror, nil
}
+728
View File
@@ -0,0 +1,728 @@
package tlsmirror
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"net"
"os"
"sync/atomic"
"testing"
"time"
"github.com/metacubex/http"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/tls"
"github.com/stretchr/testify/require"
)
var testPrimaryKey = GeneratePrimaryKey()
func TestTLSMirrorConnDeadline(t *testing.T) {
for _, tc := range []struct {
name string
set func(*Conn, time.Time) error
do func(*Conn) error
}{
{
name: "read",
set: (*Conn).SetReadDeadline,
do: func(conn *Conn) error {
_, err := conn.Read(make([]byte, 1))
return err
},
},
{
name: "write",
set: (*Conn).SetWriteDeadline,
do: func(conn *Conn) error {
_, err := conn.Write([]byte{1})
return err
},
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
client, server := net.Pipe()
t.Cleanup(func() { _ = client.Close() })
t.Cleanup(func() { _ = server.Close() })
mirror := newMirrorConn(ctx, client, server, Config{}, nil, nil, nil, nil)
conn, err := newHiddenConn(ctx, mirror, make([]byte, 32), false, Config{})
require.NoError(t, err)
require.NoError(t, tc.set(conn, time.Now().Add(10*time.Millisecond)))
err = tc.do(conn)
require.True(t, errors.Is(err, os.ErrDeadlineExceeded), "unexpected error: %v", err)
})
}
}
func TestTLSMirrorRoundTrip(t *testing.T) {
for _, tc := range []struct {
name string
cfg Config
firstWriteDelayAtLeast time.Duration
configureForwardTLS []func(*tls.Config)
}{
{name: "default"},
{name: "transport layer padding", cfg: Config{
TransportLayerPadding: TransportLayerPadding{Enabled: true},
}},
{name: "sequence watermark", cfg: Config{
SequenceWatermarkingEnabled: true,
}},
{
name: "first write delay",
cfg: Config{
DeferInstanceDerivedWrite: TimeSpec{BaseNanoseconds: uint64((50 * time.Millisecond).Nanoseconds())},
},
firstWriteDelayAtLeast: 40 * time.Millisecond,
},
{
name: "tls12 explicit nonce",
cfg: Config{
ExplicitNonceCipherSuites: RecommendedExplicitNonceCipherSuites,
},
configureForwardTLS: []func(*tls.Config){func(config *tls.Config) {
config.MinVersion = tls.VersionTLS12
config.MaxVersion = tls.VersionTLS12
config.CipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
}},
},
} {
t.Run(tc.name, func(t *testing.T) {
testTLSMirrorRoundTrip(t, tc.cfg, tc.firstWriteDelayAtLeast, tc.configureForwardTLS...)
})
}
}
func TestTLSMirrorFallback(t *testing.T) {
type fallbackPipe struct {
client net.Conn
forward net.Conn
writeErr chan error
}
newPipe := func(t *testing.T, c2sHook func(*record) (bool, error)) fallbackPipe {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
client, mirrorClient := net.Pipe()
mirrorServer, forward := net.Pipe()
t.Cleanup(func() { _ = client.Close() })
t.Cleanup(func() { _ = mirrorClient.Close() })
t.Cleanup(func() { _ = mirrorServer.Close() })
t.Cleanup(func() { _ = forward.Close() })
deadline := time.Now().Add(time.Second)
require.NoError(t, client.SetDeadline(deadline))
require.NoError(t, forward.SetDeadline(deadline))
mirror := newMirrorConn(ctx, mirrorClient, mirrorServer, Config{}, c2sHook, nil, nil, nil)
mirror.start()
return fallbackPipe{client: client, forward: forward, writeErr: make(chan error, 4)}
}
writeAsync := func(p fallbackPipe, conn net.Conn, payload []byte) {
go func() {
_, err := conn.Write(payload)
p.writeErr <- err
}()
}
expectRead := func(t *testing.T, conn net.Conn, want []byte) {
t.Helper()
got := make([]byte, len(want))
_, err := io.ReadFull(conn, got)
require.NoError(t, err)
require.Equal(t, want, got)
}
expectReadError := func(t *testing.T, conn net.Conn) {
t.Helper()
_, err := conn.Read(make([]byte, 1))
require.Error(t, err)
}
forwardClientHello := func(t *testing.T, p fallbackPipe) {
t.Helper()
clientHello := encodeTestRecord(recordTypeHandshake, [2]byte{0x03, 0x03}, testClientHelloFragment())
writeAsync(p, p.client, clientHello)
expectRead(t, p.forward, clientHello)
require.NoError(t, <-p.writeErr)
}
forwardServerHello := func(t *testing.T, p fallbackPipe) {
t.Helper()
serverHello := encodeTestRecord(recordTypeHandshake, [2]byte{0x03, 0x03}, testServerHelloFragment())
writeAsync(p, p.forward, serverHello)
expectRead(t, p.client, serverHello)
require.NoError(t, <-p.writeErr)
}
t.Run("non tls", func(t *testing.T) {
p := newPipe(t, nil)
request := []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
writeAsync(p, p.client, request)
expectRead(t, p.forward, request)
require.NoError(t, <-p.writeErr)
response := []byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
writeAsync(p, p.forward, response)
expectRead(t, p.client, response)
require.NoError(t, <-p.writeErr)
})
t.Run("closes after malformed first client hello", func(t *testing.T) {
p := newPipe(t, nil)
malformedClientHello := encodeTestRecord(recordTypeHandshake, [2]byte{0x03, 0x03}, []byte{1})
writeAsync(p, p.client, malformedClientHello)
expectRead(t, p.forward, malformedClientHello)
expectReadError(t, p.forward)
<-p.writeErr
})
t.Run("after c2s hook error", func(t *testing.T) {
hookErr := errors.New("probe fallback")
p := newPipe(t, func(rec *record) (bool, error) {
if rec.recordType == recordTypeApplicationData {
return false, hookErr
}
return false, nil
})
forwardClientHello(t, p)
forwardServerHello(t, p)
applicationData := encodeTestRecord(recordTypeApplicationData, [2]byte{0x03, 0x03}, []byte("probe-application-data"))
rawTail := []byte("raw-tail-after-fallback")
writeAsync(p, p.client, append(applicationData, rawTail...))
expectRead(t, p.forward, applicationData)
expectRead(t, p.forward, rawTail)
require.NoError(t, <-p.writeErr)
})
t.Run("keeps invalid record header", func(t *testing.T) {
p := newPipe(t, nil)
forwardClientHello(t, p)
forwardServerHello(t, p)
invalidHeader := []byte{recordTypeApplicationData, 0x03, 0x03, 0x40, 0x01}
rawTail := []byte("tail-after-invalid-header")
writeAsync(p, p.client, append(invalidHeader, rawTail...))
expectRead(t, p.forward, invalidHeader)
expectRead(t, p.forward, rawTail)
require.NoError(t, <-p.writeErr)
})
t.Run("c2s change cipher spec before server hello", func(t *testing.T) {
p := newPipe(t, nil)
forwardClientHello(t, p)
changeCipherSpec := encodeTestRecord(recordTypeChangeCipherSpec, [2]byte{0x03, 0x03}, []byte{1})
rawTail := []byte("raw-tail-after-early-ccs")
writeAsync(p, p.client, append(changeCipherSpec, rawTail...))
expectRead(t, p.forward, changeCipherSpec)
expectRead(t, p.forward, rawTail)
require.NoError(t, <-p.writeErr)
})
t.Run("forwards alert before close", func(t *testing.T) {
p := newPipe(t, nil)
forwardClientHello(t, p)
forwardServerHello(t, p)
alert := encodeTestRecord(recordTypeAlert, [2]byte{0x03, 0x03}, []byte{1, 0})
writeAsync(p, p.client, alert)
expectRead(t, p.forward, alert)
require.NoError(t, <-p.writeErr)
})
}
func TestTLSMirrorCaptureFirstHandshakeRecordForwardsFragments(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
src, srcWriter := net.Pipe()
dstReader, dst := net.Pipe()
t.Cleanup(func() { _ = src.Close() })
t.Cleanup(func() { _ = srcWriter.Close() })
t.Cleanup(func() { _ = dstReader.Close() })
t.Cleanup(func() { _ = dst.Close() })
deadline := time.Now().Add(time.Second)
require.NoError(t, src.SetDeadline(deadline))
require.NoError(t, srcWriter.SetDeadline(deadline))
require.NoError(t, dstReader.SetDeadline(deadline))
require.NoError(t, dst.SetDeadline(deadline))
mirror := newMirrorConn(ctx, src, dst, Config{}, nil, nil, nil, nil)
type captureResult struct {
rec *record
reader *bufio.Reader
raw []byte
err error
}
captured := make(chan captureResult, 1)
go func() {
rec, reader, raw, err := mirror.captureFirstHandshakeRecord(src, bufio.NewWriterSize(dst, 65536))
captured <- captureResult{rec: rec, reader: reader, raw: raw, err: err}
}()
clientHello := encodeTestRecord(recordTypeHandshake, [2]byte{0x03, 0x03}, testClientHelloFragment())
firstPart := clientHello[:8]
restPart := clientHello[8:]
nextRecord := encodeTestRecord(recordTypeApplicationData, [2]byte{0x03, 0x03}, []byte("coalesced-next-record"))
writeErr := make(chan error, 2)
go func() {
_, err := srcWriter.Write(firstPart)
writeErr <- err
}()
gotFirstPart := make([]byte, len(firstPart))
_, err := io.ReadFull(dstReader, gotFirstPart)
require.NoError(t, err)
require.Equal(t, firstPart, gotFirstPart)
require.NoError(t, <-writeErr)
go func() {
_, err := srcWriter.Write(append(append([]byte(nil), restPart...), nextRecord...))
writeErr <- err
}()
require.NoError(t, <-writeErr)
result := <-captured
require.NoError(t, result.err)
require.Equal(t, byte(recordTypeHandshake), result.rec.recordType)
require.Equal(t, testClientHelloFragment(), result.rec.fragment)
require.Equal(t, restPart, result.raw)
go func() {
writeErr <- writeRawFlush(bufio.NewWriterSize(dst, 65536), result.raw)
}()
gotRestPart := make([]byte, len(restPart))
_, err = io.ReadFull(dstReader, gotRestPart)
require.NoError(t, err)
require.Equal(t, restPart, gotRestPart)
require.NoError(t, <-writeErr)
rec, raw, err := readRecord(result.reader)
require.NoError(t, err)
require.Equal(t, byte(recordTypeApplicationData), rec.recordType)
require.Equal(t, []byte("coalesced-next-record"), rec.fragment)
require.Equal(t, nextRecord, raw)
}
func TestTLSMirrorRoundTripWithEmbeddedTrafficGenerator(t *testing.T) {
for _, tc := range []struct {
name string
startForward func(*testing.T, *atomic.Int32) string
alpn []string
h2DoNotWaitForDownloadFinish bool
}{
{
name: "http1",
startForward: startTestForwardHTTPS,
},
{
name: "h2",
startForward: startTestForwardHTTP2,
alpn: []string{"h2"},
h2DoNotWaitForDownloadFinish: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
var requests atomic.Int32
forwardAddr := tc.startForward(t, &requests)
testTLSMirrorRoundTripWithEmbeddedTrafficGenerator(t, forwardAddr, &requests, tc.alpn, tc.h2DoNotWaitForDownloadFinish)
})
}
}
func TestTLSMirrorTrafficGenerator(t *testing.T) {
t.Run("http1 does not add connection header", func(t *testing.T) {
var got *http.Request
err := runTrafficStep(context.Background(), roundTripFunc(func(req *http.Request) (*http.Response, error) {
got = req
return &http.Response{
StatusCode: http.StatusNoContent,
Body: io.NopCloser(bytes.NewReader(nil)),
}, nil
}), TrafficStep{
Host: "example.com",
Path: "/",
Method: http.MethodGet,
}, "http/1.1")
require.NoError(t, err)
require.NotNil(t, got)
require.Empty(t, got.Header.Values("Connection"))
})
t.Run("wait time includes request time", func(t *testing.T) {
start := time.Now()
err := runTrafficStep(context.Background(), roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusNoContent,
Body: delayedEOFBody{delay: 120 * time.Millisecond},
}, nil
}), TrafficStep{
Host: "example.com",
Path: "/",
Method: http.MethodGet,
WaitTime: TimeSpec{
BaseNanoseconds: uint64((200 * time.Millisecond).Nanoseconds()),
},
}, "http/1.1")
require.NoError(t, err)
elapsed := time.Since(start)
require.GreaterOrEqual(t, elapsed, 200*time.Millisecond)
require.Less(t, elapsed, 300*time.Millisecond)
})
t.Run("uses v2ray host semantics", func(t *testing.T) {
var got *http.Request
err := runTrafficStep(context.Background(), roundTripFunc(func(req *http.Request) (*http.Response, error) {
got = req
return &http.Response{
StatusCode: http.StatusNoContent,
Body: io.NopCloser(bytes.NewReader(nil)),
}, nil
}), TrafficStep{
Host: "example.com:8443",
Path: "/carrier",
Method: http.MethodGet,
}, "http/1.1")
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, "example.com:8443", got.URL.Host)
require.Equal(t, "example.com", got.Host)
})
t.Run("invalid next step weight", func(t *testing.T) {
_, _, err := chooseNextTrafficStep(TrafficStep{
NextStep: []TrafficTransferCandidate{{Weight: 0, GotoLocation: 0}},
}, 0)
require.Error(t, err)
})
}
func testTLSMirrorRoundTripWithEmbeddedTrafficGenerator(t *testing.T, forwardAddr string, requests *atomic.Int32, alpn []string, h2DoNotWaitForDownloadFinish bool) {
t.Helper()
serverLn, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = serverLn.Close() })
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
serverDone := make(chan error, 1)
go func() {
carrier, err := serverLn.Accept()
if err != nil {
serverDone <- err
return
}
forward, err := (&net.Dialer{}).DialContext(ctx, "tcp", forwardAddr)
if err != nil {
serverDone <- err
return
}
conn, err := ServeConnReady(ctx, carrier, forward, ServerConfig{PrimaryKey: testPrimaryKey})
if err != nil {
serverDone <- err
return
}
buf := make([]byte, 1024)
_, err = io.ReadFull(conn, buf)
if err != nil {
serverDone <- err
return
}
_, err = conn.Write(buf)
serverDone <- err
}()
raw, err := net.Dial("tcp", serverLn.Addr().String())
require.NoError(t, err)
client, err := Dial(ctx, raw, ClientConfig{
Config: Config{
PrimaryKey: testPrimaryKey,
EmbeddedTrafficGenerator: &TrafficGenerator{Steps: []TrafficStep{{
Host: "localhost",
Path: "/carrier",
Method: "GET",
ConnectionReady: true,
ConnectionRecallExit: true,
WaitTime: TimeSpec{
BaseNanoseconds: uint64((10 * time.Millisecond).Nanoseconds()),
},
NextStep: []TrafficTransferCandidate{{
Weight: 1,
GotoLocation: 0,
}},
Headers: []TrafficHeader{{
Name: "User-Agent",
Value: "tlsmirror-test",
}},
H2DoNotWaitForDownloadFinish: h2DoNotWaitForDownloadFinish,
}}},
},
ServerName: "localhost",
SkipCertVerify: true,
ALPN: alpn,
})
require.NoError(t, err)
require.GreaterOrEqual(t, requests.Load(), int32(1))
payload := bytes.Repeat([]byte{7}, 1024)
_, err = client.Write(payload)
require.NoError(t, err)
got := make([]byte, len(payload))
_, err = io.ReadFull(client, got)
require.NoError(t, err)
require.Equal(t, payload, got)
select {
case err := <-serverDone:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("server handler timeout")
}
}
func testTLSMirrorRoundTrip(t *testing.T, cfg Config, firstWriteDelayAtLeast time.Duration, configureForwardTLS ...func(*tls.Config)) {
cfg.PrimaryKey = testPrimaryKey
forwardAddr := startTestForwardTLS(t, configureForwardTLS...)
serverLn, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = serverLn.Close() })
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
serverDone := make(chan error, 1)
go func() {
carrier, err := serverLn.Accept()
if err != nil {
serverDone <- err
return
}
dialer := net.Dialer{}
forward, err := dialer.DialContext(ctx, "tcp", forwardAddr)
if err != nil {
serverDone <- err
return
}
conn, err := ServeConnReady(ctx, carrier, forward, cfg)
if err != nil {
serverDone <- err
return
}
for i := 0; i < 8; i++ {
size := 4 + i*8192
buf := make([]byte, size)
_, err := io.ReadFull(conn, buf)
if err != nil {
serverDone <- err
return
}
if !bytes.Equal(buf, bytes.Repeat([]byte{byte(i)}, size)) {
serverDone <- bytes.ErrTooLarge
return
}
_, err = conn.Write(bytes.Repeat([]byte{byte(255 - i)}, size))
if err != nil {
serverDone <- err
return
}
}
serverDone <- nil
}()
raw, err := net.Dial("tcp", serverLn.Addr().String())
require.NoError(t, err)
client, err := Dial(ctx, raw, ClientConfig{
Config: cfg,
ServerName: "localhost",
SkipCertVerify: true,
})
require.NoError(t, err)
for i := 0; i < 8; i++ {
size := 4 + i*8192
start := time.Now()
_, err = client.Write(bytes.Repeat([]byte{byte(i)}, size))
require.NoError(t, err)
if i == 0 && firstWriteDelayAtLeast > 0 {
require.GreaterOrEqual(t, time.Since(start), firstWriteDelayAtLeast)
}
buf := make([]byte, size)
_, err = io.ReadFull(client, buf)
require.NoError(t, err)
require.Equal(t, bytes.Repeat([]byte{byte(255 - i)}, size), buf)
}
select {
case err := <-serverDone:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("server handler timeout")
}
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
type delayedEOFBody struct {
delay time.Duration
done bool
}
func (b delayedEOFBody) Read([]byte) (int, error) {
if !b.done {
time.Sleep(b.delay)
b.done = true
}
return 0, io.EOF
}
func (b delayedEOFBody) Close() error {
return nil
}
func testClientHelloFragment() []byte {
fragment := make([]byte, 38)
fragment[0] = 1
for i := 6; i < 38; i++ {
fragment[i] = byte(i)
}
return fragment
}
func testServerHelloFragment() []byte {
fragment := make([]byte, 41)
fragment[0] = 2
for i := 6; i < 38; i++ {
fragment[i] = byte(0x80 + i)
}
fragment[38] = 0
fragment[39] = 0x13
fragment[40] = 0x01
return fragment
}
func encodeTestRecord(recordType byte, version [2]byte, fragment []byte) []byte {
record := make([]byte, 5+len(fragment))
record[0] = recordType
record[1] = version[0]
record[2] = version[1]
binary.BigEndian.PutUint16(record[3:5], uint16(len(fragment)))
copy(record[5:], fragment)
return record
}
func startTestForwardHTTPS(t *testing.T, requests *atomic.Int32) string {
t.Helper()
certPEM, keyPEM, _, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
require.NoError(t, err)
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
require.NoError(t, err)
ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
})
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}
if line == "\r\n" {
break
}
}
requests.Add(1)
_, _ = conn.Write([]byte("HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"))
_, _ = io.Copy(io.Discard, reader)
}()
}
}()
return ln.Addr().String()
}
func startTestForwardHTTP2(t *testing.T, requests *atomic.Int32) string {
t.Helper()
certPEM, keyPEM, _, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
require.NoError(t, err)
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
require.NoError(t, err)
ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
NextProtos: []string{"h2"},
})
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
w.WriteHeader(http.StatusOK)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
time.Sleep(200 * time.Millisecond)
_, _ = w.Write([]byte("ok"))
}),
Protocols: new(http.Protocols),
}
server.Protocols.SetHTTP2(true)
server.Protocols.SetUnencryptedHTTP2(true)
go func() { _ = server.Serve(ln) }()
t.Cleanup(func() { _ = server.Close() })
return ln.Addr().String()
}
func startTestForwardTLS(t *testing.T, configure ...func(*tls.Config)) string {
t.Helper()
certPEM, keyPEM, _, err := ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256)
require.NoError(t, err)
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
require.NoError(t, err)
config := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
for _, configure := range configure {
configure(config)
}
ln, err := tls.Listen("tcp", "127.0.0.1:0", config)
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
defer conn.Close()
_, _ = io.Copy(io.Discard, conn)
}()
}
}()
return ln.Addr().String()
}
+224
View File
@@ -0,0 +1,224 @@
package tlsmirror
import (
"bufio"
"context"
"crypto/rand"
"fmt"
"io"
"math/big"
"net"
"net/url"
"sync"
"time"
"github.com/metacubex/http"
)
type trafficHTTPTransport interface {
RoundTrip(req *http.Request) (*http.Response, error)
}
type trafficHTTP1Transport struct {
conn net.Conn
reader *bufio.Reader
}
func (t *trafficHTTP1Transport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Proto = "HTTP/1.1"
req.ProtoMajor = 1
req.ProtoMinor = 1
if err := req.Write(t.conn); err != nil {
return nil, err
}
return http.ReadResponse(t.reader, req)
}
type trafficHTTP2Transport struct {
*http.ClientConn
}
func (t *trafficHTTP2Transport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Proto = "HTTP/2"
req.ProtoMajor = 2
req.ProtoMinor = 0
return t.ClientConn.RoundTrip(req)
}
func newTrafficHTTPTransport(ctx context.Context, conn net.Conn, alpn string) (trafficHTTPTransport, error) {
switch alpn {
case "h2":
protocols := new(http.Protocols)
protocols.SetUnencryptedHTTP2(true)
transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
},
Protocols: protocols,
DisableCompression: true,
}
clientConn, err := transport.NewClientConn(ctx, "https", ":0")
if err != nil {
return nil, err
}
return &trafficHTTP2Transport{ClientConn: clientConn}, nil
case "http/1.1", "":
return &trafficHTTP1Transport{
conn: conn,
reader: bufio.NewReader(conn),
}, nil
default:
return nil, fmt.Errorf("tlsmirror: unknown carrier ALPN %q", alpn)
}
}
func runTrafficGenerator(ctx context.Context, conn net.Conn, cfg *TrafficGenerator, alpn string, ready func(), recall <-chan struct{}) {
if cfg == nil || len(cfg.Steps) == 0 {
_, _ = io.Copy(io.Discard, conn)
return
}
transport, err := newTrafficHTTPTransport(ctx, conn, alpn)
if err != nil {
_ = conn.Close()
return
}
var readyOnce sync.Once
markReady := func() {
readyOnce.Do(ready)
}
for current := 0; ctx.Err() == nil; {
if current < 0 || current >= len(cfg.Steps) {
_ = conn.Close()
return
}
step := cfg.Steps[current]
if err := runTrafficStep(ctx, transport, step, alpn); err != nil {
return
}
if step.ConnectionReady {
markReady()
}
if step.ConnectionRecallExit {
select {
case <-recall:
_ = conn.Close()
return
case <-ctx.Done():
_ = conn.Close()
return
default:
}
}
next, ok, err := chooseNextTrafficStep(step, current)
if err != nil {
return
}
if !ok {
current++
} else {
current = next
}
}
}
func trafficGeneratorWaitsForReady(cfg *TrafficGenerator) bool {
if cfg == nil {
return false
}
for _, step := range cfg.Steps {
if step.ConnectionReady {
return true
}
}
return false
}
func runTrafficStep(ctx context.Context, transport trafficHTTPTransport, step TrafficStep, alpn string) error {
requestURL := &url.URL{
Scheme: "https",
Host: step.Host,
Path: step.Path,
}
req := &http.Request{
Method: step.Method,
URL: requestURL,
Host: requestURL.Hostname(),
Header: make(http.Header, len(step.Headers)),
}
if len(step.Headers) > 0 {
for _, header := range step.Headers {
if header.Name == "" {
continue
}
if header.Value != "" {
req.Header.Add(header.Name, header.Value)
}
for _, value := range header.Values {
req.Header.Add(header.Name, value)
}
}
}
start := time.Now()
resp, err := transport.RoundTrip(req)
if err != nil {
return err
}
finishRequest := func() error {
_, copyErr := io.Copy(io.Discard, resp.Body)
closeErr := resp.Body.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
if step.H2DoNotWaitForDownloadFinish && alpn == "h2" {
go func() { _ = finishRequest() }()
} else if err := finishRequest(); err != nil {
return err
}
elapsed := time.Since(start)
if delay, err := step.WaitTime.Duration(); err != nil {
return err
} else if delay > elapsed {
timer := time.NewTimer(delay - elapsed)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
return nil
}
func chooseNextTrafficStep(step TrafficStep, current int) (int, bool, error) {
if len(step.NextStep) == 0 {
return 0, false, nil
}
total := int32(0)
for _, candidate := range step.NextStep {
total += candidate.Weight
}
if total <= 0 {
return 0, false, fmt.Errorf("tlsmirror: invalid next-step weight total %d", total)
}
n, err := rand.Int(rand.Reader, big.NewInt(int64(total)))
if err != nil {
return 0, false, err
}
selected := int32(n.Int64())
cursor := int32(0)
for _, candidate := range step.NextStep {
if cursor >= selected {
return candidate.GotoLocation, true, nil
}
cursor += candidate.Weight
}
return current + 1, true, nil
}
+19
View File
@@ -8,6 +8,7 @@ import (
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
"github.com/metacubex/mihomo/transport/tlsmirror"
"github.com/metacubex/tls"
)
@@ -22,6 +23,8 @@ type TLSConfig struct {
NextProtos []string
ECH *ech.Config
Reality *tlsC.RealityConfig
TLSMirror *tlsmirror.Config
TLSMirrorDialer tlsmirror.EnrollmentDialer
}
func (cfg *TLSConfig) ToStdConfig() (*tls.Config, error) {
@@ -38,6 +41,22 @@ func (cfg *TLSConfig) ToStdConfig() (*tls.Config, error) {
}
func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
if cfg.TLSMirror != nil {
return tlsmirror.Dial(ctx, conn, tlsmirror.ClientConfig{
Config: *cfg.TLSMirror,
ServerName: cfg.Host,
SkipCertVerify: cfg.SkipCertVerify,
ALPN: cfg.NextProtos,
Fingerprint: cfg.FingerPrint,
Certificate: cfg.Certificate,
PrivateKey: cfg.PrivateKey,
ClientFingerprint: cfg.ClientFingerprint,
ForwardAddressHint: cfg.Host,
ECH: cfg.ECH,
EnrollmentDialer: cfg.TLSMirrorDialer,
})
}
tlsConfig, err := cfg.ToStdConfig()
if err != nil {
return nil, err