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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 ¬ifyCloseConn{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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user