Compare commits

..

20 Commits

Author SHA1 Message Date
Hung-I Wang
3035ae89e3 fix: correct typo in ProxyGroup health check log message (#2575) 2026-02-27 11:00:47 +08:00
enfein
c251e411e5 feat: support mieru traffic pattern configuration (#2585) 2026-02-27 10:31:35 +08:00
wwqgtxx
f6722ab79b action: Upgrade loongarch golang version 2026-02-25 13:08:18 +08:00
wwqgtxx
4ca515896b feat: support trusttunnel inbound and outbound 2026-02-25 12:26:08 +08:00
wwqgtxx
836c972c54 chore: cleanup unreachable code 2026-02-23 21:03:22 +08:00
wwqgtxx
43509da1a9 chore: simplify gun code 2026-02-23 21:03:22 +08:00
wwqgtxx
3752cb044f fix: CVE-2026-26958 of filippo.io/edwards25519 2026-02-21 17:18:15 +08:00
wwqgtxx
30391b40c4 chore: unified UA settings method 2026-02-13 18:11:46 +08:00
Panda
05fbf552ec fix: make User-Agent check case-insensitive (#2566) 2026-02-13 17:34:06 +08:00
wwqgtxx
e4143cf1ad fix: tun doesn't clean up the DNS setting in systemd-resolved when closed 2026-02-13 11:51:11 +08:00
wwqgtxx
5eaf5d16ce fix: quic gso maybe not working with pppoe 2026-02-13 11:43:20 +08:00
wwqgtxx
9dee264f13 fix: udp/icmp not work on gso with system stack 2026-02-13 00:07:39 +08:00
wwqgtxx
50480406cf fix: rollback sing-tun commit 2026-02-12 17:42:01 +08:00
wwqgtxx
6eb27ac3dc chore: align with legacy behavior 2026-02-12 16:43:45 +08:00
wwqgtxx
a949ad883c chore: update golang to 1.26 2026-02-11 17:13:19 +08:00
wwqgtxx
20bf57c117 feat: add disable-reuse params for DoT 2026-02-10 15:03:15 +08:00
wwqgtxx
60a9312057 chore: structure support remain-tagged field 2026-02-10 15:03:15 +08:00
wwqgtxx
9fda032a28 chore: structure unifies the way to handle top-level and sub structs 2026-02-10 15:03:15 +08:00
wwqgtxx
c3399fd346 chore: better logging for removed configurations 2026-02-10 01:16:24 +08:00
wwqgtxx
445083b624 fix: override interface-name broken
https://github.com/MetaCubeX/mihomo/issues/2558
2026-02-09 19:55:18 +08:00
47 changed files with 2293 additions and 426 deletions

View File

@@ -59,8 +59,8 @@ jobs:
- { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x }
- { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le }
# Go 1.25 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# Go 1.26 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
- { goos: windows, goarch: '386', output: '386' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
@@ -82,6 +82,13 @@ jobs:
- { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
- { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
# Go 1.25 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
- { goos: windows, goarch: '386', output: '386-go125', goversion: '1.25' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go125, goversion: '1.25' }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go125, goversion: '1.25' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go125, goversion: '1.25' }
# Go 1.24 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
- { goos: windows, goarch: '386', output: '386-go124', goversion: '1.24' }
@@ -154,7 +161,7 @@ jobs:
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
uses: actions/setup-go@v6
with:
go-version: '1.25'
go-version: '1.26'
check-latest: true # Always check for the latest patch release
- name: Set up Go
@@ -164,14 +171,30 @@ jobs:
go-version: ${{ matrix.jobs.goversion }}
check-latest: true # Always check for the latest patch release
- name: Set up Go1.24 loongarch abi1
- name: Set up Go1.25 loongarch abi1
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/go1.24.0.linux-amd64-abi1.tar.gz
sudo tar zxf go1.24.0.linux-amd64-abi1.tar.gz -C /usr/local
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.25.5/go1.25.5.linux-amd64-abi1.tar.gz
sudo tar zxf go1.25.5.linux-amd64-abi1.tar.gz -C /usr/local
echo "/usr/local/go/bin" >> $GITHUB_PATH
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.26.x
# that means after golang1.27 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
# f0894a00f4b756d4b9b4078af2e686b359493583: "os: remove 5ms sleep on Windows in (*Process).Wait"
# sepical fix:
# - os.RemoveAll not working on Windows7
- name: Revert Golang1.26 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.26.patch
# this patch file only works on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
@@ -184,12 +207,11 @@ jobs:
# sepical fix:
# - os.RemoveAll not working on Windows7
- name: Revert Golang1.25 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.25.patch
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.24.x
# that means after golang1.25 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
@@ -204,7 +226,6 @@ jobs:
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.24.patch
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x
# that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
@@ -219,7 +240,6 @@ jobs:
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.23.patch
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.22.x
# that means after golang1.23 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/

View File

@@ -24,7 +24,7 @@ jobs:
- 'ubuntu-24.04-arm' # arm64 linux
- 'macos-15-intel' # amd64 macos
go-version:
- '1.26.0-rc.3'
- '1.26'
- '1.25'
- '1.24'
- '1.23'
@@ -51,17 +51,11 @@ jobs:
check-latest: true # Always check for the latest patch release
- name: Revert Golang commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version != '1.20' && matrix.go-version != '1.26.0-rc.3' }}
if: ${{ runner.os == 'Windows' && matrix.go-version != '1.20' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go${{matrix.go-version}}.patch
- name: Revert Golang commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.26.0-rc.3' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.26.patch
- name: Remove inbound test for macOS
if: ${{ runner.os == 'macOS' }}
run: |

View File

@@ -15,6 +15,7 @@ import (
mieruclient "github.com/enfein/mieru/v3/apis/client"
mierucommon "github.com/enfein/mieru/v3/apis/common"
mierumodel "github.com/enfein/mieru/v3/apis/model"
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
"google.golang.org/protobuf/proto"
)
@@ -28,16 +29,17 @@ type Mieru struct {
type MieruOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
PortRange string `proxy:"port-range,omitempty"`
Transport string `proxy:"transport"`
UDP bool `proxy:"udp,omitempty"`
UserName string `proxy:"username"`
Password string `proxy:"password"`
Multiplexing string `proxy:"multiplexing,omitempty"`
HandshakeMode string `proxy:"handshake-mode,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
PortRange string `proxy:"port-range,omitempty"`
Transport string `proxy:"transport"`
UDP bool `proxy:"udp,omitempty"`
UserName string `proxy:"username"`
Password string `proxy:"password"`
Multiplexing string `proxy:"multiplexing,omitempty"`
HandshakeMode string `proxy:"handshake-mode,omitempty"`
TrafficPattern string `proxy:"traffic-pattern,omitempty"`
}
type mieruPacketDialer struct {
@@ -291,6 +293,10 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro
if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok {
config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode)
}
if option.TrafficPattern != "" {
trafficPattern, _ := mierutp.Decode(option.TrafficPattern)
config.Profile.TrafficPattern = trafficPattern
}
return config, nil
}
@@ -345,6 +351,15 @@ func validateMieruOption(option MieruOption) error {
return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode)
}
}
if option.TrafficPattern != "" {
trafficPattern, err := mierutp.Decode(option.TrafficPattern)
if err != nil {
return fmt.Errorf("failed to decode traffic pattern %q: %w", option.TrafficPattern, err)
}
if err := mierutp.Validate(trafficPattern); err != nil {
return fmt.Errorf("invalid traffic pattern %q: %w", option.TrafficPattern, err)
}
}
return nil
}

View File

@@ -31,12 +31,13 @@ func TestNewMieru(t *testing.T) {
},
{
option: MieruOption{
Name: "test",
Server: "example.com",
Port: 10003,
Transport: "UDP",
UserName: "test",
Password: "test",
Name: "test",
Server: "example.com",
Port: 10003,
Transport: "UDP",
UserName: "test",
Password: "test",
TrafficPattern: "GgQIARAK",
},
wantBaseAddr: "example.com:10003",
},

View File

@@ -12,9 +12,9 @@ import (
type RealityOptions struct {
PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"`
ShortID string `proxy:"short-id,omitempty"`
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768,omitempty"`
}
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {

View File

@@ -27,9 +27,8 @@ type Trojan struct {
hexPassword [trojan.KeyLength]byte
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
gunTransport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -66,7 +65,6 @@ type TrojanSSOption struct {
Password string `proxy:"password,omitempty"`
}
// StreamConnContext implements C.ProxyAdapter
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
switch t.option.Network {
case "ws":
@@ -118,7 +116,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc":
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
break // already handle in gun transport
default:
// default tcp network
// handle TLS
@@ -179,27 +177,14 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if t.transport != nil {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
}
return NewConn(c, t), nil
if t.gunTransport != nil {
c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig)
} else {
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
}
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
@@ -219,35 +204,19 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
}
var c net.Conn
// grpc transport
if t.transport != nil {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
}
pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err
if t.gunTransport != nil {
c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig)
} else {
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
}
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
@@ -271,8 +240,8 @@ func (t *Trojan) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (t *Trojan) Close() error {
if t.transport != nil {
return t.transport.Close()
if t.gunTransport != nil {
return t.gunTransport.Close()
}
return nil
}
@@ -336,29 +305,24 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return c, nil
}
tlsConfig, err := ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
NextProtos: option.ALPN,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: option.SkipCertVerify,
ServerName: option.SNI,
},
Fingerprint: option.Fingerprint,
Certificate: option.Certificate,
PrivateKey: option.PrivateKey,
})
if err != nil {
return nil, err
tlsConfig := &vmess.TLSConfig{
Host: option.SNI,
SkipCertVerify: option.SkipCertVerify,
FingerPrint: option.Fingerprint,
Certificate: option.Certificate,
PrivateKey: option.PrivateKey,
ClientFingerprint: option.ClientFingerprint,
NextProtos: []string{"h2"},
ECH: t.echConfig,
Reality: t.realityConfig,
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
t.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig)
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName,
UserAgent: option.GrpcOpts.GrpcUserAgent,
Host: option.SNI,
ClientFingerprint: option.ClientFingerprint,
ServiceName: option.GrpcOpts.GrpcServiceName,
UserAgent: option.GrpcOpts.GrpcUserAgent,
Host: option.SNI,
}
}

View File

@@ -0,0 +1,144 @@
package outbound
import (
"context"
"net"
"net/netip"
"strconv"
N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/trusttunnel"
"github.com/metacubex/mihomo/transport/vmess"
)
type TrustTunnel struct {
*Base
client *trusttunnel.Client
option *TrustTunnelOption
}
type TrustTunnelOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
PrivateKey string `proxy:"private-key,omitempty"`
UDP bool `proxy:"udp,omitempty"`
HealthCheck bool `proxy:"health-check,omitempty"`
Quic bool `proxy:"quic,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
}
func (t *TrustTunnel) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := t.client.Dial(ctx, metadata.RemoteAddress())
if err != nil {
return nil, err
}
return NewConn(c, t), nil
}
func (t *TrustTunnel) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := t.client.ListenPacket(ctx)
if err != nil {
return nil, err
}
return newPacketConn(N.NewThreadSafePacketConn(pc), t), nil
}
// SupportUOT implements C.ProxyAdapter
func (t *TrustTunnel) SupportUOT() bool {
return true
}
// ProxyInfo implements C.ProxyAdapter
func (t *TrustTunnel) ProxyInfo() C.ProxyInfo {
info := t.Base.ProxyInfo()
info.DialerProxy = t.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (t *TrustTunnel) Close() error {
return t.client.Close()
}
func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
outbound := &TrustTunnel{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.TrustTunnel,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: option.IPVersion,
},
option: &option,
}
outbound.dialer = option.NewDialer(outbound.DialOptions())
tOption := trusttunnel.ClientOptions{
Dialer: outbound.dialer,
ResolvUDP: func(ctx context.Context, server string) (netip.AddrPort, error) {
udpAddr, err := resolveUDPAddr(ctx, "udp", server, option.IPVersion)
if err != nil {
return netip.AddrPort{}, err
}
return udpAddr.AddrPort(), nil
},
Server: addr,
Username: option.UserName,
Password: option.Password,
QUIC: option.Quic,
QUICCongestionControl: option.CongestionController,
QUICCwnd: option.CWND,
HealthCheck: option.HealthCheck,
}
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsConfig := &vmess.TLSConfig{
Host: option.SNI,
SkipCertVerify: option.SkipCertVerify,
NextProtos: option.ALPN,
FingerPrint: option.Fingerprint,
Certificate: option.Certificate,
PrivateKey: option.PrivateKey,
ClientFingerprint: option.ClientFingerprint,
ECH: echConfig,
}
if tlsConfig.Host == "" {
tlsConfig.Host = option.Server
}
tOption.TLSConfig = tlsConfig
client, err := trusttunnel.NewClient(context.TODO(), tOption)
if err != nil {
return nil, err
}
outbound.client = client
return outbound, nil
}

View File

@@ -33,9 +33,8 @@ type Vless struct {
encryption *encryption.ClientInstance
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
gunTransport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -151,7 +150,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
break // already handle in gun transport
default:
// default tcp network
// handle TLS
@@ -234,23 +233,11 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if v.transport != nil {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
}
return NewConn(c, v), nil
if v.gunTransport != nil {
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
} else {
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -272,28 +259,11 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
}
var c net.Conn
// gun transport
if v.transport != nil {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err)
}
return v.ListenPacketOnStreamConn(ctx, c, metadata)
if v.gunTransport != nil {
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
} else {
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -348,8 +318,8 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
if v.transport != nil {
return v.transport.Close()
if v.gunTransport != nil {
return v.gunTransport.Close()
}
return nil
}
@@ -461,38 +431,35 @@ func NewVless(option VlessOption) (*Vless, error) {
}
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
ServiceName: option.GrpcOpts.GrpcServiceName,
UserAgent: option.GrpcOpts.GrpcUserAgent,
Host: option.ServerName,
}
if option.ServerName == "" {
gunConfig.Host = v.addr
}
var tlsConfig *tls.Config
var tlsConfig *vmess.TLSConfig
if option.TLS {
tlsConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
},
Fingerprint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
})
if err != nil {
return nil, err
tlsConfig = &vmess.TLSConfig{
Host: option.ServerName,
SkipCertVerify: option.SkipCertVerify,
FingerPrint: option.Fingerprint,
Certificate: option.Certificate,
PrivateKey: option.PrivateKey,
ClientFingerprint: option.ClientFingerprint,
NextProtos: []string{"h2"},
ECH: v.echConfig,
Reality: v.realityConfig,
}
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
tlsConfig.Host = host
}
}
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
return v, nil

View File

@@ -34,9 +34,8 @@ type Vmess struct {
option *VmessOption
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
gunTransport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -98,7 +97,6 @@ type WSOptions struct {
V2rayHttpUpgradeFastOpen bool `proxy:"v2ray-http-upgrade-fast-open,omitempty"`
}
// StreamConnContext implements C.ProxyAdapter
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
switch v.option.Network {
case "ws":
@@ -205,7 +203,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
break // already handle in gun transport
default:
// handle TLS
if v.option.TLS {
@@ -296,23 +294,11 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if v.transport != nil {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
}
return NewConn(c, v), nil
if v.gunTransport != nil {
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
} else {
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -331,27 +317,11 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
}
var c net.Conn
// gun transport
if v.transport != nil {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return v.ListenPacketOnStreamConn(ctx, c, metadata)
if v.gunTransport != nil {
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
} else {
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -375,8 +345,8 @@ func (v *Vmess) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vmess) Close() error {
if v.transport != nil {
return v.transport.Close()
if v.gunTransport != nil {
return v.gunTransport.Close()
}
return nil
}
@@ -467,38 +437,35 @@ func NewVmess(option VmessOption) (*Vmess, error) {
}
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
ServiceName: option.GrpcOpts.GrpcServiceName,
UserAgent: option.GrpcOpts.GrpcUserAgent,
Host: option.ServerName,
}
if option.ServerName == "" {
gunConfig.Host = v.addr
}
var tlsConfig *tls.Config
var tlsConfig *mihomoVMess.TLSConfig
if option.TLS {
tlsConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
},
Fingerprint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
})
if err != nil {
return nil, err
tlsConfig = &mihomoVMess.TLSConfig{
Host: option.ServerName,
SkipCertVerify: option.SkipCertVerify,
FingerPrint: option.Fingerprint,
Certificate: option.Certificate,
PrivateKey: option.PrivateKey,
ClientFingerprint: option.ClientFingerprint,
NextProtos: []string{"h2"},
ECH: v.echConfig,
Reality: v.realityConfig,
}
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
tlsConfig.Host = host
}
}
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
return v, nil

View File

@@ -77,8 +77,8 @@ type WireGuardOption struct {
}
type WireGuardPeerOption struct {
Server string `proxy:"server"`
Port int `proxy:"port"`
Server string `proxy:"server,omitempty"`
Port int `proxy:"port,omitempty"`
PublicKey string `proxy:"public-key,omitempty"`
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
Reserved []uint8 `proxy:"reserved,omitempty"`

View File

@@ -272,7 +272,7 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func(
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes {
log.Warnln("because %s failed multiple times, active health check", gb.Name())
log.Warnln("because %s failed multiple times, activate health check", gb.Name())
fn()
}
}

View File

@@ -42,10 +42,6 @@ type GroupCommonOption struct {
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
Hidden bool `group:"hidden,omitempty"`
Icon string `group:"icon,omitempty"`
// removed configs, only for error logging
Interface string `group:"interface-name,omitempty"`
RoutingMark int `group:"routing-mark,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
@@ -62,12 +58,15 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, errFormat
}
if groupOption.RoutingMark != 0 {
if _, ok := config["routing-mark"]; ok {
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
}
if groupOption.Interface != "" {
if _, ok := config["interface-name"]; ok {
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
}
if _, ok := config["dialer-proxy"]; ok {
log.Errorln("The group [%s] with dialer-proxy configuration is not allowed, please set it directly on the proxy instead", groupOption.Name)
}
groupName := groupOption.Name

View File

@@ -166,6 +166,13 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
break
}
proxy, err = outbound.NewMasque(*masqueOption)
case "trusttunnel":
trustTunnelOption := &outbound.TrustTunnelOption{BasicOption: basicOption}
err = decoder.Decode(mapping, trustTunnelOption)
if err != nil {
break
}
proxy, err = outbound.NewTrustTunnel(*trustTunnelOption)
default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
}

View File

@@ -60,7 +60,7 @@ func (o *overrideSchema) Apply(mapping map[string]any) error {
mapping["skip-cert-verify"] = *o.SkipCertVerify
}
if o.Interface != nil {
mapping["interface"] = *o.Interface
mapping["interface-name"] = *o.Interface
}
if o.RoutingMark != nil {
mapping["routing-mark"] = *o.RoutingMark

View File

@@ -1,7 +1,6 @@
package buf
import (
"github.com/metacubex/sing/common"
"github.com/metacubex/sing/common/buf"
)
@@ -9,14 +8,52 @@ const BufferSize = buf.BufferSize
type Buffer = buf.Buffer
var New = buf.New
var NewPacket = buf.NewPacket
var NewSize = buf.NewSize
var With = buf.With
var As = buf.As
var ReleaseMulti = buf.ReleaseMulti
func New() *Buffer {
return buf.New()
}
var (
Must = common.Must
Error = common.Error
)
func NewPacket() *Buffer {
return buf.NewPacket()
}
func NewSize(size int) *Buffer {
return buf.NewSize(size)
}
func With(data []byte) *Buffer {
return buf.With(data)
}
func As(data []byte) *Buffer {
return buf.As(data)
}
func ReleaseMulti(buffers []*Buffer) {
buf.ReleaseMulti(buffers)
}
func Error(_ any, err error) error {
return err
}
func Must(errs ...error) {
for _, err := range errs {
if err != nil {
panic(err)
}
}
}
func Must1[T any](result T, err error) T {
if err != nil {
panic(err)
}
return result
}
func Must2[T any, T2 any](result T, result2 T2, err error) (T, T2) {
if err != nil {
panic(err)
}
return result, result2
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/base64"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
@@ -38,58 +39,7 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("decode must recive a ptr struct")
}
t := reflect.TypeOf(dst).Elem()
v := reflect.ValueOf(dst).Elem()
for idx := 0; idx < v.NumField(); idx++ {
field := t.Field(idx)
if field.Anonymous {
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
return err
}
continue
}
tag := field.Tag.Get(d.option.TagName)
key, omitKey, found := strings.Cut(tag, ",")
omitempty := found && omitKey == "omitempty"
// As a special case, if the field tag is "-", the field is always omitted.
// Note that a field with name "-" can still be generated using the tag "-,".
if key == "-" {
continue
}
value, ok := src[key]
if !ok {
if d.option.KeyReplacer != nil {
key = d.option.KeyReplacer.Replace(key)
}
for _strKey := range src {
strKey := _strKey
if d.option.KeyReplacer != nil {
strKey = d.option.KeyReplacer.Replace(strKey)
}
if strings.EqualFold(key, strKey) {
value = src[_strKey]
ok = true
break
}
}
}
if !ok || value == nil {
if omitempty {
continue
}
return fmt.Errorf("key '%s' missing", key)
}
err := d.decode(key, value, v.Field(idx))
if err != nil {
return err
}
}
return nil
return d.decode("", src, reflect.ValueOf(dst).Elem())
}
// isNil returns true if the input is nil or a typed nil pointer.
@@ -456,6 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
}
targetValKeysUnused := make(map[any]struct{})
errors := make([]string, 0)
// This slice will keep track of all the structs we'll be decoding.
@@ -470,6 +421,11 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField
val reflect.Value
}
// remainField is set to a valid field set with the "remain" tag if
// we are keeping track of remaining values.
var remainField *field
var fields []field
for len(structs) > 0 {
structVal := structs[0]
@@ -479,30 +435,47 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
for i := 0; i < structType.NumField(); i++ {
fieldType := structType.Field(i)
fieldKind := fieldType.Type.Kind()
fieldVal := structVal.Field(i)
if fieldVal.Kind() == reflect.Ptr && fieldVal.Elem().Kind() == reflect.Struct {
// Handle embedded struct pointers as embedded structs.
fieldVal = fieldVal.Elem()
}
// If "squash" is specified in the tag, we squash the field down.
squash := false
squash := fieldVal.Kind() == reflect.Struct && fieldType.Anonymous
remain := false
// We always parse the tags cause we're looking for other tags too
tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",")
for _, tag := range tagParts[1:] {
if tag == "squash" {
squash = true
break
}
if tag == "remain" {
remain = true
break
}
}
if squash {
if fieldKind != reflect.Struct {
if fieldVal.Kind() != reflect.Struct {
errors = append(errors,
fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind).Error())
fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind()).Error())
} else {
structs = append(structs, structVal.FieldByName(fieldType.Name))
structs = append(structs, fieldVal)
}
continue
}
// Normal struct field, store it away
fields = append(fields, field{fieldType, structVal.Field(i)})
// Build our field
if remain {
remainField = &field{fieldType, fieldVal}
} else {
// Normal struct field, store it away
fields = append(fields, field{fieldType, fieldVal})
}
}
}
@@ -511,8 +484,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field, fieldValue := f.field, f.val
fieldName := field.Name
tagValue := field.Tag.Get(d.option.TagName)
tagValue = strings.SplitN(tagValue, ",", 2)[0]
tagParts := strings.Split(field.Tag.Get(d.option.TagName), ",")
tagValue := tagParts[0]
if tagValue != "" {
fieldName = tagValue
}
@@ -521,6 +494,13 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
continue
}
omitempty := false
for _, tag := range tagParts[1:] {
if tag == "omitempty" {
omitempty = true
}
}
rawMapKey := reflect.ValueOf(fieldName)
rawMapVal := dataVal.MapIndex(rawMapKey)
if !rawMapVal.IsValid() {
@@ -548,7 +528,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !rawMapVal.IsValid() {
// There was no matching key in the map for the value in
// the struct. Just ignore.
// the struct. Remember it for potential errors and metadata.
if !omitempty {
targetValKeysUnused[fieldName] = struct{}{}
}
continue
}
}
@@ -570,7 +553,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
// If the name is empty string, then we're at the root, and we
// don't dot-join the fields.
if name != "" {
fieldName = fmt.Sprintf("%s.%s", name, fieldName)
fieldName = name + "." + fieldName
}
if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil {
@@ -578,6 +561,36 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
}
}
// If we have a "remain"-tagged field and we have unused keys then
// we put the unused keys directly into the remain field.
if remainField != nil && len(dataValKeysUnused) > 0 {
// Build a map of only the unused values
remain := map[interface{}]interface{}{}
for key := range dataValKeysUnused {
remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface()
}
// Decode it as-if we were just decoding this map onto our map.
if err := d.decodeMap(name, remain, remainField.val); err != nil {
errors = append(errors, err.Error())
}
// Set the map to nil so we have none so that the next check will
// not error (ErrorUnused)
dataValKeysUnused = nil
}
if len(targetValKeysUnused) > 0 {
keys := make([]string, 0, len(targetValKeysUnused))
for rawKey := range targetValKeysUnused {
keys = append(keys, rawKey.(string))
}
sort.Strings(keys)
err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", "))
errors = append(errors, err.Error())
}
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, ","))
}

View File

@@ -139,6 +139,49 @@ func TestStructure_Nest(t *testing.T) {
assert.Equal(t, s.BazOptional, goal)
}
func TestStructure_DoubleNest(t *testing.T) {
rawMap := map[string]any{
"bar": map[string]any{
"foo": 1,
},
}
goal := BazOptional{
Foo: 1,
}
s := &struct {
Bar struct {
BazOptional
} `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, s.Bar.BazOptional, goal)
}
func TestStructure_Remain(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": "test",
"extra": false,
}
goal := &Baz{
Foo: 1,
Bar: "test",
}
s := &struct {
Baz
Remain map[string]any `test:",remain"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, *goal, s.Baz)
assert.Equal(t, map[string]any{"extra": false}, s.Remain)
}
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
@@ -228,6 +271,23 @@ func TestStructure_Pointer(t *testing.T) {
assert.Nil(t, s.Bar)
}
func TestStructure_PointerStruct(t *testing.T) {
rawMap := map[string]any{
"foo": "foo",
}
s := &struct {
Foo *string `test:"foo,omitempty"`
Bar *Baz `test:"bar,omitempty"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.NotNil(t, s.Foo)
assert.Equal(t, "foo", *s.Foo)
assert.Nil(t, s.Bar)
}
type num struct {
a int
}

View File

@@ -50,7 +50,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
}
}
if _, ok := header["User-Agent"]; !ok {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", UA())
}

View File

@@ -135,6 +135,8 @@ func UEncryptedClientHelloKey(it tls.EncryptedClientHelloKey) utls.EncryptedClie
}
}
type ConnectionState = utls.ConnectionState
type Config = utls.Config
var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset

View File

@@ -46,6 +46,7 @@ const (
AnyTLS
Sudoku
Masque
TrustTunnel
)
const (
@@ -215,6 +216,8 @@ func (at AdapterType) String() string {
return "Sudoku"
case Masque:
return "Masque"
case TrustTunnel:
return "TrustTunnel"
case Relay:
return "Relay"
case Selector:

View File

@@ -40,6 +40,7 @@ const (
ANYTLS
MIERU
SUDOKU
TRUSTTUNNEL
INNER
)
@@ -115,6 +116,8 @@ func (t Type) String() string {
return "Mieru"
case SUDOKU:
return "Sudoku"
case TRUSTTUNNEL:
return "TrustTunnel"
case INNER:
return "Inner"
default:
@@ -159,6 +162,8 @@ func ParseType(t string) (*Type, error) {
res = MIERU
case "SUDOKU":
res = SUDOKU
case "TRUSTTUNNEL":
res = TRUSTTUNNEL
case "INNER":
res = INNER
default:

View File

@@ -23,6 +23,7 @@ type dnsOverTLS struct {
host string
dialer *dnsDialer
skipCertVerify bool
disableReuse bool
access sync.Mutex
connections deque.Deque[net.Conn] // LIFO
@@ -57,11 +58,13 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
var conn net.Conn
isOldConn := true
t.access.Lock()
if t.connections.Len() > 0 {
conn = t.connections.PopBack()
if !t.disableReuse {
t.access.Lock()
if t.connections.Len() > 0 {
conn = t.connections.PopBack()
}
t.access.Unlock()
}
t.access.Unlock()
if conn == nil {
conn, err = t.dialContext(ctx)
@@ -90,13 +93,17 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
return
}
t.access.Lock()
if t.connections.Len() >= maxOldDotConns {
oldConn := t.connections.PopFront()
go oldConn.Close() // close in a new goroutine, not blocking the current task
if !t.disableReuse {
t.access.Lock()
if t.connections.Len() >= maxOldDotConns {
oldConn := t.connections.PopFront()
go oldConn.Close() // close in a new goroutine, not blocking the current task
}
t.connections.PushBack(conn)
t.access.Unlock()
} else {
_ = conn.Close()
}
t.connections.PushBack(conn)
t.access.Unlock()
return
}
}()
@@ -134,12 +141,14 @@ func (t *dnsOverTLS) dialContext(ctx context.Context) (net.Conn, error) {
}
func (t *dnsOverTLS) ResetConnection() {
t.access.Lock()
for t.connections.Len() > 0 {
oldConn := t.connections.PopFront()
go oldConn.Close() // close in a new goroutine, not blocking the current task
if !t.disableReuse {
t.access.Lock()
for t.connections.Len() > 0 {
oldConn := t.connections.PopFront()
go oldConn.Close() // close in a new goroutine, not blocking the current task
}
t.access.Unlock()
}
t.access.Unlock()
}
func (t *dnsOverTLS) Close() error {
@@ -159,6 +168,9 @@ func newDoTClient(addr string, resolver *Resolver, params map[string]string, pro
if params["skip-cert-verify"] == "true" {
c.skipCertVerify = true
}
if params["disable-reuse"] == "true" {
c.disableReuse = true
}
runtime.SetFinalizer(c, (*dnsOverTLS).Close)
return c
}

View File

@@ -1077,6 +1077,8 @@ proxies: # socks5
# multiplexing: MULTIPLEXING_LOW
# 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD
# handshake-mode: HANDSHAKE_STANDARD
# 一个 base64 字符串用于微调网络行为
# traffic-pattern: ""
# sudoku
- name: sudoku
@@ -1115,6 +1117,23 @@ proxies: # socks5
# - http/1.1
# skip-cert-verify: true
# trusttunnel
- name: trusttunnel
type: trusttunnel
server: 1.2.3.4
port: 443
username: username
password: password
# client-fingerprint: chrome
health-check: true
udp: true
# sni: "example.com"
# alpn:
# - h2
# skip-cert-verify: true
# quic: true # 默认为false
# congestion-controller: bbr
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out"
type: dns
@@ -1628,6 +1647,8 @@ listeners:
users:
username1: password1
username2: password2
# 一个 base64 字符串用于微调网络行为
# traffic-pattern: ""
- name: sudoku-in-1
type: sudoku
@@ -1731,6 +1752,30 @@ listeners:
# masquerade: http://127.0.0.1:8080 #作为反向代理
# masquerade: https://127.0.0.1:8080 #作为反向代理
- name: trusttunnel-in-1
type: trusttunnel
port: 10821 # 支持使用ports格式例如200,302 or 200,204,401-429,501-503
listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
users:
- username: 1
password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径
private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径
network: ["tcp", "udp"] # http2+http3
congestion-controller: bbr
# 下面两项为mTLS配置项如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空
# client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify"
# client-auth-cert: string # 证书 PEM 格式,或者 证书的路径
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 注意listeners中的tun仅提供给高级用户使用普通用户应使用顶层配置中的tun
- name: tun-in-1
type: tun

8
go.mod
View File

@@ -3,11 +3,10 @@ module github.com/metacubex/mihomo
go 1.20
require (
filippo.io/edwards25519 v1.1.0
github.com/bahlo/generic-list-go v0.2.0
github.com/coreos/go-iptables v0.8.0
github.com/dlclark/regexp2 v1.11.5
github.com/enfein/mieru/v3 v3.26.2
github.com/enfein/mieru/v3 v3.28.0
github.com/gobwas/ws v1.4.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/golang/snappy v1.0.0
@@ -19,12 +18,13 @@ require (
github.com/metacubex/chi v0.1.0
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727
github.com/metacubex/cpu v0.1.0
github.com/metacubex/edwards25519 v1.2.0
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/http v0.1.0
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
github.com/metacubex/mlkem v0.1.0
github.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af
github.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56
github.com/metacubex/randv2 v0.2.0
github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.7
@@ -33,7 +33,7 @@ require (
github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-tun v0.4.15
github.com/metacubex/sing-tun v0.4.16-0.20260213034958-b09e3f5bbae6
github.com/metacubex/sing-vmess v0.2.5
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141

16
go.sum
View File

@@ -1,5 +1,3 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
@@ -22,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/enfein/mieru/v3 v3.26.2 h1:U/2XJc+3vrJD9r815FoFdwToQFEcqSOzzzWIPPhjfEU=
github.com/enfein/mieru/v3 v3.26.2/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/enfein/mieru/v3 v3.28.0 h1:4OsFPUIjKfQ6ymfyX1Laqz7h+zB8TxuK1m0isnYJ8ww=
github.com/enfein/mieru/v3 v3.28.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -93,6 +91,8 @@ github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727 h1:qbZQ0sO
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
github.com/metacubex/cpu v0.1.0 h1:8PeTdV9j6UKbN1K5Jvtbi/Jock7dknvzyYuLb8Conmk=
github.com/metacubex/cpu v0.1.0/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=
github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4=
github.com/metacubex/edwards25519 v1.2.0/go.mod h1:NCQF3J/Ki7382FJuokwsywEIIEI/gro/3smyXgQJsx0=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
@@ -113,8 +113,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
github.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af h1:do5o1rzn64NEN5oGswo7VruDkbz2055fhVT3rXehA8E=
github.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
github.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56 h1:7yfF31COW2hiCovb5+3uSxRl3UKWOXjpS0j4N5U0qZ8=
github.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
@@ -131,8 +131,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.15 h1:0uOO8kCpodgs4Op8L7sn+C4J6a/lQagmeRTrzHxn+mo=
github.com/metacubex/sing-tun v0.4.15/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-tun v0.4.16-0.20260213034958-b09e3f5bbae6 h1:3yeZyDHGBmI/1XLsWBhr1sLhifWHkJa5J5Kf8djIbqs=
github.com/metacubex/sing-tun v0.4.16-0.20260213034958-b09e3f5bbae6/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=
github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=

View File

@@ -0,0 +1,24 @@
package config
import (
"encoding/json"
)
type TrustTunnelServer struct {
Enable bool `yaml:"enable" json:"enable"`
Listen string `yaml:"listen" json:"listen"`
Users map[string]string `yaml:"users" json:"users,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"`
ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"`
EchKey string `yaml:"ech-key" json:"ech-key"`
Network []string `yaml:"network" json:"network,omitempty"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
}
func (t TrustTunnelServer) String() string {
b, _ := json.Marshal(t)
return string(b)
}

View File

@@ -14,6 +14,7 @@ import (
"google.golang.org/protobuf/proto"
mieruserver "github.com/enfein/mieru/v3/apis/server"
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
)
@@ -26,8 +27,9 @@ type Mieru struct {
type MieruOption struct {
BaseOption
Transport string `inbound:"transport"`
Users map[string]string `inbound:"users"`
Transport string `inbound:"transport"`
Users map[string]string `inbound:"users"`
TrafficPattern string `inbound:"traffic-pattern"`
}
type mieruListenerFactory struct{}
@@ -154,10 +156,13 @@ func buildMieruServerConfig(option *MieruOption, ports utils.IntRanges[uint16])
Password: proto.String(password),
})
}
var trafficPattern *mierupb.TrafficPattern
trafficPattern, _ = mierutp.Decode(option.TrafficPattern)
return &mieruserver.ServerConfig{
Config: &mierupb.ServerConfig{
PortBindings: portBindings,
Users: users,
PortBindings: portBindings,
Users: users,
TrafficPattern: trafficPattern,
},
StreamListenerFactory: mieruListenerFactory{},
PacketListenerFactory: mieruListenerFactory{},
@@ -179,5 +184,14 @@ func validateMieruOption(option *MieruOption) error {
return fmt.Errorf("password is empty")
}
}
if option.TrafficPattern != "" {
trafficPattern, err := mierutp.Decode(option.TrafficPattern)
if err != nil {
return fmt.Errorf("failed to decode traffic pattern %q: %w", option.TrafficPattern, err)
}
if err := mierutp.Validate(trafficPattern); err != nil {
return fmt.Errorf("invalid traffic pattern %q: %w", option.TrafficPattern, err)
}
}
return nil
}

View File

@@ -61,6 +61,20 @@ func TestNewMieru(t *testing.T) {
},
wantErr: false,
},
{
name: "valid traffic pattern",
args: args{
option: &inbound.MieruOption{
BaseOption: inbound.BaseOption{
Port: "8080",
},
Transport: "TCP",
Users: map[string]string{"user": "pass"},
TrafficPattern: "GgQIARAK",
},
},
wantErr: false,
},
{
name: "invalid - no port",
args: args{
@@ -135,6 +149,20 @@ func TestNewMieru(t *testing.T) {
},
wantErr: true,
},
{
name: "invalid traffic pattern",
args: args{
option: &inbound.MieruOption{
BaseOption: inbound.BaseOption{
Port: "8080",
},
Transport: "TCP",
Users: map[string]string{"user": "pass"},
TrafficPattern: "1212ababXYYX",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -0,0 +1,96 @@
package inbound
import (
"strings"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/trusttunnel"
"github.com/metacubex/mihomo/log"
)
type TrustTunnelOption struct {
BaseOption
Users AuthUsers `inbound:"users,omitempty"`
Certificate string `inbound:"certificate"`
PrivateKey string `inbound:"private-key"`
ClientAuthType string `inbound:"client-auth-type,omitempty"`
ClientAuthCert string `inbound:"client-auth-cert,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
Network []string `inbound:"network,omitempty"`
CongestionController string `inbound:"congestion-controller,omitempty"`
CWND int `inbound:"cwnd,omitempty"`
}
func (o TrustTunnelOption) Equal(config C.InboundConfig) bool {
return optionToString(o) == optionToString(config)
}
type TrustTunnel struct {
*Base
config *TrustTunnelOption
l C.MultiAddrListener
vs LC.TrustTunnelServer
}
func NewTrustTunnel(options *TrustTunnelOption) (*TrustTunnel, error) {
base, err := NewBase(&options.BaseOption)
if err != nil {
return nil, err
}
users := make(map[string]string)
for _, user := range options.Users {
users[user.Username] = user.Password
}
return &TrustTunnel{
Base: base,
config: options,
vs: LC.TrustTunnelServer{
Enable: true,
Listen: base.RawAddress(),
Users: users,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
ClientAuthType: options.ClientAuthType,
ClientAuthCert: options.ClientAuthCert,
EchKey: options.EchKey,
Network: options.Network,
CongestionController: options.CongestionController,
CWND: options.CWND,
},
}, nil
}
// Config implements constant.InboundListener
func (v *TrustTunnel) Config() C.InboundConfig {
return v.config
}
// Address implements constant.InboundListener
func (v *TrustTunnel) Address() string {
var addrList []string
if v.l != nil {
for _, addr := range v.l.AddrList() {
addrList = append(addrList, addr.String())
}
}
return strings.Join(addrList, ",")
}
// Listen implements constant.InboundListener
func (v *TrustTunnel) Listen(tunnel C.Tunnel) error {
var err error
v.l, err = trusttunnel.New(v.vs, tunnel, v.Additions()...)
if err != nil {
return err
}
log.Infoln("TrustTunnel[%s] proxy listening at: %s", v.Name(), v.Address())
return nil
}
// Close implements constant.InboundListener
func (v *TrustTunnel) Close() error {
return v.l.Close()
}
var _ C.InboundListener = (*TrustTunnel)(nil)

View File

@@ -0,0 +1,109 @@
package inbound_test
import (
"net/netip"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/stretchr/testify/assert"
)
func testInboundTrustTunnel(t *testing.T, inboundOptions inbound.TrustTunnelOption, outboundOptions outbound.TrustTunnelOption) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "trusttunnel_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Users = []inbound.AuthUser{{Username: "test", Password: userUUID}}
in, err := inbound.NewTrustTunnel(&inboundOptions)
if !assert.NoError(t, err) {
return
}
tunnel := NewHttpTestTunnel()
defer tunnel.Close()
err = in.Listen(tunnel)
if !assert.NoError(t, err) {
return
}
defer in.Close()
addrPort, err := netip.ParseAddrPort(in.Address())
if !assert.NoError(t, err) {
return
}
outboundOptions.Name = "trusttunnel_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.UserName = "test"
outboundOptions.Password = userUUID
out, err := outbound.NewTrustTunnel(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
}
func testInboundTrustTunnelTLS(t *testing.T, quic bool) {
inboundOptions := inbound.TrustTunnelOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
}
outboundOptions := outbound.TrustTunnelOption{
Fingerprint: tlsFingerprint,
HealthCheck: true,
}
if quic {
inboundOptions.Network = []string{"udp"}
inboundOptions.CongestionController = "bbr"
outboundOptions.Quic = true
}
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
t.Run("ECH", func(t *testing.T) {
inboundOptions := inboundOptions
outboundOptions := outboundOptions
inboundOptions.EchKey = echKeyPem
outboundOptions.ECHOpts = outbound.ECHOptions{
Enable: true,
Config: echConfigBase64,
}
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
})
t.Run("mTLS", func(t *testing.T) {
inboundOptions := inboundOptions
outboundOptions := outboundOptions
inboundOptions.ClientAuthCert = tlsAuthCertificate
outboundOptions.Certificate = tlsAuthCertificate
outboundOptions.PrivateKey = tlsAuthPrivateKey
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
})
t.Run("mTLS+ECH", func(t *testing.T) {
inboundOptions := inboundOptions
outboundOptions := outboundOptions
inboundOptions.ClientAuthCert = tlsAuthCertificate
outboundOptions.Certificate = tlsAuthCertificate
outboundOptions.PrivateKey = tlsAuthPrivateKey
inboundOptions.EchKey = echKeyPem
outboundOptions.ECHOpts = outbound.ECHOptions{
Enable: true,
Config: echConfigBase64,
}
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrustTunnel_H2(t *testing.T) {
testInboundTrustTunnelTLS(t, true)
}
func TestInboundTrustTunnel_QUIC(t *testing.T) {
testInboundTrustTunnelTLS(t, true)
}

View File

@@ -26,7 +26,7 @@ type VmessOption struct {
type VmessUser struct {
Username string `inbound:"username,omitempty"`
UUID string `inbound:"uuid"`
AlterID int `inbound:"alterId"`
AlterID int `inbound:"alterId,omitempty"`
}
func (o VmessOption) Equal(config C.InboundConfig) bool {

View File

@@ -141,6 +141,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
return nil, err
}
listener, err = IN.NewSudoku(sudokuOption)
case "trusttunnel":
trusttunnelOption := &IN.TrustTunnelOption{}
err = decoder.Decode(mapping, trusttunnelOption)
if err != nil {
return nil, err
}
listener, err = IN.NewTrustTunnel(trusttunnelOption)
default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
}

View File

@@ -0,0 +1,188 @@
package trusttunnel
import (
"context"
"errors"
"net"
"strings"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/sockopt"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp"
"github.com/metacubex/mihomo/transport/trusttunnel"
"github.com/metacubex/tls"
)
type Listener struct {
closed bool
config LC.TrustTunnelServer
listeners []net.Listener
udpListeners []net.PacketConn
tlsConfig *tls.Config
services []*trusttunnel.Service
}
func New(config LC.TrustTunnelServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
if len(additions) == 0 {
additions = []inbound.Addition{
inbound.WithInName("DEFAULT-TRUSTTUNNEL"),
inbound.WithSpecialRules(""),
}
}
tlsConfig := &tls.Config{Time: ntp.Now}
if config.Certificate != "" && config.PrivateKey != "" {
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return certLoader()
}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig)
if err != nil {
return nil, err
}
}
}
tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType)
if len(config.ClientAuthCert) > 0 {
if tlsConfig.ClientAuth == tls.NoClientCert {
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
pool, err := ca.LoadCertificates(config.ClientAuthCert)
if err != nil {
return nil, err
}
tlsConfig.ClientCAs = pool
}
sl = &Listener{
config: config,
tlsConfig: tlsConfig,
}
h, err := sing.NewListenerHandler(sing.ListenerConfig{
Tunnel: tunnel,
Type: C.TRUSTTUNNEL,
Additions: additions,
})
if err != nil {
return nil, err
}
if tlsConfig.GetCertificate == nil {
return nil, errors.New("disallow using TrustTunnel without certificates config")
}
if len(config.Network) == 0 {
config.Network = []string{"tcp"}
}
listenTCP, listenUDP := false, false
for _, network := range config.Network {
network = strings.ToLower(network)
switch {
case strings.HasPrefix(network, "tcp"):
listenTCP = true
case strings.HasPrefix(network, "udp"):
listenUDP = true
}
}
for _, addr := range strings.Split(config.Listen, ",") {
addr := addr
var (
tcpListener net.Listener
udpConn net.PacketConn
)
if listenTCP {
tcpListener, err = inbound.Listen("tcp", addr)
if err != nil {
_ = sl.Close()
return nil, err
}
sl.listeners = append(sl.listeners, tcpListener)
}
if listenUDP {
udpConn, err = inbound.ListenPacket("udp", addr)
if err != nil {
_ = sl.Close()
return nil, err
}
if err := sockopt.UDPReuseaddr(udpConn); err != nil {
log.Warnln("Failed to Reuse UDP Address: %s", err)
}
sl.udpListeners = append(sl.udpListeners, udpConn)
}
service := trusttunnel.NewService(trusttunnel.ServiceOptions{
Ctx: context.Background(),
Logger: log.SingLogger,
Handler: h,
ICMPHandler: nil,
QUICCongestionControl: config.CongestionController,
QUICCwnd: config.CWND,
})
service.UpdateUsers(config.Users)
err = service.Start(tcpListener, udpConn, tlsConfig)
if err != nil {
_ = sl.Close()
return nil, err
}
sl.services = append(sl.services, service)
}
return sl, nil
}
func (l *Listener) Close() error {
l.closed = true
var retErr error
for _, lis := range l.services {
err := lis.Close()
if err != nil {
retErr = err
}
}
for _, lis := range l.listeners {
err := lis.Close()
if err != nil {
retErr = err
}
}
for _, lis := range l.udpListeners {
err := lis.Close()
if err != nil {
retErr = err
}
}
return retErr
}
func (l *Listener) Config() string {
return l.config.String()
}
func (l *Listener) AddrList() (addrList []net.Addr) {
for _, lis := range l.listeners {
addrList = append(addrList, lis.Addr())
}
for _, lis := range l.udpListeners {
addrList = append(addrList, lis.LocalAddr())
}
return
}

View File

@@ -18,9 +18,9 @@ import (
"github.com/metacubex/mihomo/common/buf"
"github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/vmess"
"github.com/metacubex/http"
"github.com/metacubex/http/httptrace"
@@ -40,10 +40,10 @@ var defaultHeader = http.Header{
type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error)
type Conn struct {
initFn func() (io.ReadCloser, netAddr, error)
initFn func() (io.ReadCloser, NetAddr, error)
writer io.Writer // writer must not nil
closer io.Closer
netAddr
NetAddr
initOnce sync.Once
initErr error
@@ -59,10 +59,9 @@ type Conn struct {
}
type Config struct {
ServiceName string
UserAgent string
Host string
ClientFingerprint string
ServiceName string
UserAgent string
Host string
}
func (g *Conn) initReader() {
@@ -74,7 +73,7 @@ func (g *Conn) initReader() {
}
return
}
g.netAddr = addr
g.NetAddr = addr
g.closeMutex.Lock()
defer g.closeMutex.Unlock()
@@ -247,7 +246,7 @@ func (g *Conn) SetDeadline(t time.Time) error {
return nil
}
func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint string, echConfig *ech.Config, realityConfig *tlsC.RealityConfig) *TransportWrap {
func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap {
dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
@@ -260,65 +259,33 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri
return pconn, nil
}
if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok {
if realityConfig == nil {
tlsConfig := tlsC.UConfig(cfg)
err := echConfig.ClientHandleUTLS(ctx, tlsConfig)
if err != nil {
pconn.Close()
return nil, err
}
tlsConn := tlsC.UClient(pconn, tlsConfig, clientFingerprint)
if err := tlsConn.HandshakeContext(ctx); err != nil {
pconn.Close()
return nil, err
}
conn, err := vmess.StreamTLSConn(ctx, pconn, tlsConfig)
if err != nil {
_ = pconn.Close()
return nil, err
}
if tlsConfig.Reality == nil { // reality doesn't return the negotiated ALPN
switch tlsConn := conn.(type) {
case interface{ ConnectionState() tls.ConnectionState }:
state := tlsConn.ConnectionState()
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
tlsConn.Close()
_ = conn.Close()
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
}
return tlsConn, nil
} else {
realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, cfg.ServerName, realityConfig)
if err != nil {
pconn.Close()
return nil, err
case interface{ ConnectionState() tlsC.ConnectionState }:
state := tlsConn.ConnectionState()
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
_ = conn.Close()
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
}
//state := realityConn.(*utls.UConn).ConnectionState()
//if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
// realityConn.Close()
// return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
//}
return realityConn, nil
}
}
if realityConfig != nil {
return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint")
}
err = echConfig.ClientHandle(ctx, cfg)
if err != nil {
pconn.Close()
return nil, err
}
conn := tls.Client(pconn, cfg)
if err := conn.HandshakeContext(ctx); err != nil {
pconn.Close()
return nil, err
}
state := conn.ConnectionState()
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
conn.Close()
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
}
return conn, nil
}
transport := &http.Http2Transport{
DialTLSContext: dialFunc,
TLSClientConfig: tlsConfig,
AllowHTTP: false,
DisableCompression: true,
PingTimeout: 0,
@@ -351,7 +318,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
header := defaultHeader.Clone()
if cfg.UserAgent != "" {
header["user-agent"] = []string{cfg.UserAgent}
header.Set("User-Agent", cfg.UserAgent)
}
request := &http.Request{
@@ -372,12 +339,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
request = request.WithContext(transport.ctx)
conn := &Conn{
initFn: func() (io.ReadCloser, netAddr, error) {
nAddr := netAddr{}
initFn: func() (io.ReadCloser, NetAddr, error) {
nAddr := NetAddr{}
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
nAddr.localAddr = connInfo.Conn.LocalAddr()
nAddr.remoteAddr = connInfo.Conn.RemoteAddr()
nAddr.SetLocalAddr(connInfo.Conn.LocalAddr())
nAddr.SetRemoteAddr(connInfo.Conn.RemoteAddr())
},
}
request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))
@@ -394,12 +361,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
return conn, nil
}
func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, echConfig *ech.Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) {
func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, cfg *Config) (net.Conn, error) {
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil
}
transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, echConfig, realityConfig)
transport := NewHTTP2Client(dialFn, tlsConfig)
c, err := StreamGunWithTransport(transport, cfg)
if err != nil {
return nil, err

View File

@@ -9,7 +9,6 @@ import (
"github.com/metacubex/mihomo/common/buf"
N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/http"
"github.com/metacubex/http/h2c"
@@ -42,17 +41,9 @@ func NewServerHandler(options ServerOption) http.Handler {
writer.WriteHeader(http.StatusOK)
conn := &Conn{
initFn: func() (io.ReadCloser, netAddr, error) {
nAddr := netAddr{}
if request.RemoteAddr != "" {
metadata := C.Metadata{}
if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil {
nAddr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort())
}
}
if addr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
nAddr.localAddr = addr
}
initFn: func() (io.ReadCloser, NetAddr, error) {
nAddr := NetAddr{}
nAddr.SetAddrFromRequest(request)
return request.Body, nAddr, nil
},
writer: writer,

View File

@@ -5,6 +5,8 @@ import (
"net"
"sync"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/http"
)
@@ -18,20 +20,40 @@ type TransportWrap struct {
func (tw *TransportWrap) Close() error {
tw.closeOnce.Do(func() {
tw.cancel()
closeTransport(tw.Http2Transport)
CloseTransport(tw.Http2Transport)
})
return nil
}
type netAddr struct {
type NetAddr struct {
remoteAddr net.Addr
localAddr net.Addr
}
func (addr netAddr) RemoteAddr() net.Addr {
func (addr NetAddr) RemoteAddr() net.Addr {
return addr.remoteAddr
}
func (addr netAddr) LocalAddr() net.Addr {
func (addr NetAddr) LocalAddr() net.Addr {
return addr.localAddr
}
func (addr *NetAddr) SetAddrFromRequest(request *http.Request) {
if request.RemoteAddr != "" {
metadata := C.Metadata{}
if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil {
addr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort())
}
}
if netAddr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
addr.localAddr = netAddr
}
}
func (addr *NetAddr) SetRemoteAddr(remoteAddr net.Addr) {
addr.remoteAddr = remoteAddr
}
func (addr *NetAddr) SetLocalAddr(localAddr net.Addr) {
addr.localAddr = localAddr
}

View File

@@ -44,7 +44,7 @@ func closeClientConn(cc *http.Http2ClientConn) { // like forceCloseConn() in htt
_ = cc.Close()
}
func closeTransport(tr *http.Http2Transport) {
func CloseTransport(tr *http.Http2Transport) {
connPool := transportConnPool(tr)
p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data)
p.mu.Lock()

View File

@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"filippo.io/edwards25519"
"github.com/metacubex/edwards25519"
)
// KeyPair holds the scalar private key and point public key

View File

@@ -0,0 +1,269 @@
package trusttunnel
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/netip"
"net/url"
"sync"
"time"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/vmess"
"github.com/metacubex/http"
"github.com/metacubex/http/httptrace"
"github.com/metacubex/tls"
"golang.org/x/exp/slices"
)
type RoundTripper interface {
http.RoundTripper
CloseIdleConnections()
}
type ResolvUDPFunc func(ctx context.Context, server string) (netip.AddrPort, error)
type ClientOptions struct {
Dialer C.Dialer
ResolvUDP ResolvUDPFunc
Server string
Username string
Password string
TLSConfig *vmess.TLSConfig
QUIC bool
QUICCongestionControl string
QUICCwnd int
HealthCheck bool
}
type Client struct {
ctx context.Context
dialer C.Dialer
resolv ResolvUDPFunc
server string
auth string
roundTripper RoundTripper
startOnce sync.Once
healthCheck bool
healthCheckTimer *time.Timer
}
func NewClient(ctx context.Context, options ClientOptions) (client *Client, err error) {
client = &Client{
ctx: ctx,
dialer: options.Dialer,
resolv: options.ResolvUDP,
server: options.Server,
auth: buildAuth(options.Username, options.Password),
}
if options.QUIC {
if len(options.TLSConfig.NextProtos) == 0 {
options.TLSConfig.NextProtos = []string{"h3"}
} else if !slices.Contains(options.TLSConfig.NextProtos, "h3") {
return nil, errors.New("require alpn h3")
}
err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd)
if err != nil {
return nil, err
}
} else {
if len(options.TLSConfig.NextProtos) == 0 {
options.TLSConfig.NextProtos = []string{"h2"}
} else if !slices.Contains(options.TLSConfig.NextProtos, "h2") {
return nil, errors.New("require alpn h2")
}
client.h2RoundTripper(options.TLSConfig)
}
if options.HealthCheck {
client.healthCheck = true
}
return client, nil
}
func (c *Client) h2RoundTripper(tlsConfig *vmess.TLSConfig) {
c.roundTripper = &http.Http2Transport{
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
conn, err := c.dialer.DialContext(ctx, network, c.server)
if err != nil {
return nil, err
}
tlsConn, err := vmess.StreamTLSConn(ctx, conn, tlsConfig)
if err != nil {
_ = conn.Close()
return nil, err
}
return tlsConn, nil
},
AllowHTTP: false,
IdleConnTimeout: DefaultSessionTimeout,
}
}
func (c *Client) start() {
if c.healthCheck {
c.healthCheckTimer = time.NewTimer(DefaultHealthCheckTimeout)
go c.loopHealthCheck()
}
}
func (c *Client) loopHealthCheck() {
for {
select {
case <-c.healthCheckTimer.C:
case <-c.ctx.Done():
c.healthCheckTimer.Stop()
return
}
ctx, cancel := context.WithTimeout(c.ctx, DefaultHealthCheckTimeout)
_ = c.HealthCheck(ctx)
cancel()
}
}
func (c *Client) resetHealthCheckTimer() {
if c.healthCheckTimer == nil {
return
}
c.healthCheckTimer.Reset(DefaultHealthCheckTimeout)
}
func (c *Client) dial(ctx context.Context, request *http.Request, conn *httpConn, pipeReader *io.PipeReader, pipeWriter *io.PipeWriter) {
c.startOnce.Do(c.start)
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
conn.SetLocalAddr(connInfo.Conn.LocalAddr())
conn.SetRemoteAddr(connInfo.Conn.RemoteAddr())
},
}
request = request.WithContext(httptrace.WithClientTrace(ctx, trace))
response, err := c.roundTripper.RoundTrip(request)
if err != nil {
_ = pipeWriter.CloseWithError(err)
_ = pipeReader.CloseWithError(err)
conn.setUp(nil, err)
} else if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
err = fmt.Errorf("unexpected status code: %d", response.StatusCode)
_ = pipeWriter.CloseWithError(err)
_ = pipeReader.CloseWithError(err)
conn.setUp(nil, err)
} else {
c.resetHealthCheckTimer()
conn.setUp(response.Body, nil)
}
}
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
pipeReader, pipeWriter := io.Pipe()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: host,
},
Header: make(http.Header),
Body: pipeReader,
Host: host,
}
request.Header.Add("User-Agent", TCPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
conn := &tcpConn{
httpConn: httpConn{
writer: pipeWriter,
created: make(chan struct{}),
},
}
go c.dial(ctx, request, &conn.httpConn, pipeReader, pipeWriter)
return conn, nil
}
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
pipeReader, pipeWriter := io.Pipe()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: UDPMagicAddress,
},
Header: make(http.Header),
Body: pipeReader,
Host: UDPMagicAddress,
}
request.Header.Add("User-Agent", UDPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
conn := &clientPacketConn{
packetConn: packetConn{
httpConn: httpConn{
writer: pipeWriter,
created: make(chan struct{}),
},
},
}
go c.dial(ctx, request, &conn.httpConn, pipeReader, pipeWriter)
return conn, nil
}
func (c *Client) ListenICMP(ctx context.Context) (*IcmpConn, error) {
pipeReader, pipeWriter := io.Pipe()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: ICMPMagicAddress,
},
Header: make(http.Header),
Body: pipeReader,
Host: ICMPMagicAddress,
}
request.Header.Add("User-Agent", ICMPUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
conn := &IcmpConn{
httpConn{
writer: pipeWriter,
created: make(chan struct{}),
},
}
go c.dial(ctx, request, &conn.httpConn, pipeReader, pipeWriter)
return conn, nil
}
func (c *Client) Close() error {
forceCloseAllConnections(c.roundTripper)
if c.healthCheckTimer != nil {
c.healthCheckTimer.Stop()
}
return nil
}
func (c *Client) ResetConnections() {
forceCloseAllConnections(c.roundTripper)
c.resetHealthCheckTimer()
}
func (c *Client) HealthCheck(ctx context.Context) error {
defer c.resetHealthCheckTimer()
request := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Scheme: "https",
Host: HealthCheckMagicAddress,
},
Header: make(http.Header),
Host: HealthCheckMagicAddress,
}
request.Header.Add("User-Agent", HealthCheckUserAgent)
request.Header.Add("Proxy-Authorization", c.auth)
response, err := c.roundTripper.RoundTrip(request.WithContext(ctx))
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
return nil
}

View File

@@ -0,0 +1,4 @@
// Package trusttunnel copy and modify from:
// https://github.com/xchacha20-poly1305/sing-trusttunnel/tree/v0.1.1
// adopt for mihomo
package trusttunnel

View File

@@ -0,0 +1,18 @@
package trusttunnel
import (
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/http"
"github.com/metacubex/quic-go/http3"
)
func forceCloseAllConnections(roundTripper RoundTripper) {
roundTripper.CloseIdleConnections()
switch tr := roundTripper.(type) {
case *http.Http2Transport:
gun.CloseTransport(tr)
case *http3.Transport:
_ = tr.Close()
}
}

View File

@@ -0,0 +1,82 @@
package trusttunnel
import (
"encoding/binary"
"net/netip"
"github.com/metacubex/mihomo/common/buf"
)
type IcmpConn struct {
httpConn
}
func (i *IcmpConn) WritePing(id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16) error {
request := buf.NewSize(2 + 16 + 2 + 1 + 2)
defer request.Release()
buf.Must(binary.Write(request, binary.BigEndian, id))
destinationAddress := buildPaddingIP(destination)
buf.Must1(request.Write(destinationAddress[:]))
buf.Must(binary.Write(request, binary.BigEndian, sequenceNumber))
buf.Must(binary.Write(request, binary.BigEndian, ttl))
buf.Must(binary.Write(request, binary.BigEndian, size))
return buf.Error(i.writeFlush(request.Bytes()))
}
func (i *IcmpConn) ReadPing() (id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16, err error) {
err = i.waitCreated()
if err != nil {
return
}
response := buf.NewSize(2 + 16 + 1 + 1 + 2)
defer response.Release()
_, err = response.ReadFullFrom(i.body, response.Cap())
if err != nil {
return
}
buf.Must(binary.Read(response, binary.BigEndian, &id))
var sourceAddressBuffer [16]byte
buf.Must1(response.Read(sourceAddressBuffer[:]))
sourceAddress = parse16BytesIP(sourceAddressBuffer)
buf.Must(binary.Read(response, binary.BigEndian, &icmpType))
buf.Must(binary.Read(response, binary.BigEndian, &code))
buf.Must(binary.Read(response, binary.BigEndian, &sequenceNumber))
return
}
func (i *IcmpConn) Close() error {
return i.httpConn.Close()
}
func (i *IcmpConn) ReadPingRequest() (id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16, err error) {
err = i.waitCreated()
if err != nil {
return
}
request := buf.NewSize(2 + 16 + 2 + 1 + 2)
defer request.Release()
_, err = request.ReadFullFrom(i.body, request.Cap())
if err != nil {
return
}
buf.Must(binary.Read(request, binary.BigEndian, &id))
var destinationAddressBuffer [16]byte
buf.Must1(request.Read(destinationAddressBuffer[:]))
destination = parse16BytesIP(destinationAddressBuffer)
buf.Must(binary.Read(request, binary.BigEndian, &sequenceNumber))
buf.Must(binary.Read(request, binary.BigEndian, &ttl))
buf.Must(binary.Read(request, binary.BigEndian, &size))
return
}
func (i *IcmpConn) WritePingResponse(id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16) error {
response := buf.NewSize(2 + 16 + 1 + 1 + 2)
defer response.Release()
buf.Must(binary.Write(response, binary.BigEndian, id))
sourceAddressBytes := buildPaddingIP(sourceAddress)
buf.Must1(response.Write(sourceAddressBytes[:]))
buf.Must(binary.Write(response, binary.BigEndian, icmpType))
buf.Must(binary.Write(response, binary.BigEndian, code))
buf.Must(binary.Write(response, binary.BigEndian, sequenceNumber))
return buf.Error(i.writeFlush(response.Bytes()))
}

View File

@@ -0,0 +1,280 @@
package trusttunnel
import (
"encoding/binary"
"math"
"net"
"github.com/metacubex/sing/common"
"github.com/metacubex/sing/common/buf"
E "github.com/metacubex/sing/common/exceptions"
M "github.com/metacubex/sing/common/metadata"
N "github.com/metacubex/sing/common/network"
"github.com/metacubex/sing/common/rw"
)
type packetConn struct {
httpConn
readWaitOptions N.ReadWaitOptions
}
func (c *packetConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
c.readWaitOptions = options
return false
}
var (
_ N.NetPacketConn = (*clientPacketConn)(nil)
_ N.FrontHeadroom = (*clientPacketConn)(nil)
_ N.PacketReadWaiter = (*clientPacketConn)(nil)
)
type clientPacketConn struct {
packetConn
}
func (u *clientPacketConn) FrontHeadroom() int {
return 4 + 16 + 2 + 16 + 2 + 1 + math.MaxUint8
}
func (u *clientPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
buffer = u.readWaitOptions.NewPacketBuffer()
destination, err = u.ReadPacket(buffer)
if err != nil {
buffer.Release()
return nil, M.Socksaddr{}, err
}
u.readWaitOptions.PostReturn(buffer)
return buffer, destination, nil
}
func (u *clientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
err = u.waitCreated()
if err != nil {
return M.Socksaddr{}, err
}
return u.readPacketFromServer(buffer)
}
func (u *clientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
buffer := buf.With(p)
destination, err := u.ReadPacket(buffer)
if err != nil {
return 0, nil, err
}
return buffer.Len(), destination.UDPAddr(), nil
}
func (u *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return u.writePacketToServer(buffer, destination)
}
func (u *clientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
if err != nil {
return 0, err
}
return len(p), nil
}
func (u *clientPacketConn) readPacketFromServer(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
header := buf.NewSize(4 + 16 + 2 + 16 + 2)
defer header.Release()
_, err = header.ReadFullFrom(u.body, header.Cap())
if err != nil {
return
}
var length uint32
common.Must(binary.Read(header, binary.BigEndian, &length))
var sourceAddressBuffer [16]byte
common.Must1(header.Read(sourceAddressBuffer[:]))
destination.Addr = parse16BytesIP(sourceAddressBuffer)
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
common.Must(rw.SkipN(header, 16+2)) // To local address:port
payloadLen := int(length) - (16 + 2 + 16 + 2)
if payloadLen < 0 {
return M.Socksaddr{}, E.New("invalid udp length: ", length)
}
_, err = buffer.ReadFullFrom(u.body, payloadLen)
return
}
func (u *clientPacketConn) writePacketToServer(buffer *buf.Buffer, source M.Socksaddr) error {
defer buffer.Release()
if !source.IsIP() {
return E.New("only support IP")
}
appName := AppName
if len(appName) > math.MaxUint8 {
appName = appName[:math.MaxUint8]
}
payloadLen := buffer.Len()
headerLen := 4 + 16 + 2 + 16 + 2 + 1 + len(appName)
lengthField := uint32(16 + 2 + 16 + 2 + 1 + len(appName) + payloadLen)
destinationAddress := buildPaddingIP(source.Addr)
var (
header *buf.Buffer
headerInBuffer bool
)
if buffer.Start() >= headerLen {
headerBytes := buffer.ExtendHeader(headerLen)
header = buf.With(headerBytes)
headerInBuffer = true
} else {
header = buf.NewSize(headerLen)
defer header.Release()
}
common.Must(binary.Write(header, binary.BigEndian, lengthField))
common.Must(header.WriteZeroN(16 + 2)) // Source address:port (unknown)
common.Must1(header.Write(destinationAddress[:]))
common.Must(binary.Write(header, binary.BigEndian, source.Port))
common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName))))
common.Must1(header.WriteString(appName))
if !headerInBuffer {
_, err := u.writer.Write(header.Bytes())
if err != nil {
return err
}
}
_, err := u.writer.Write(buffer.Bytes())
if err != nil {
return err
}
if u.flusher != nil {
u.flusher.Flush()
}
return nil
}
var (
_ N.NetPacketConn = (*serverPacketConn)(nil)
_ N.FrontHeadroom = (*serverPacketConn)(nil)
_ N.PacketReadWaiter = (*serverPacketConn)(nil)
)
type serverPacketConn struct {
packetConn
}
func (u *serverPacketConn) FrontHeadroom() int {
return 4 + 16 + 2 + 16 + 2
}
func (u *serverPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
buffer = u.readWaitOptions.NewPacketBuffer()
destination, err = u.ReadPacket(buffer)
if err != nil {
buffer.Release()
return nil, M.Socksaddr{}, err
}
u.readWaitOptions.PostReturn(buffer)
return buffer, destination, nil
}
func (u *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
err = u.waitCreated()
if err != nil {
return M.Socksaddr{}, err
}
return u.readPacketFromClient(buffer)
}
func (u *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
buffer := buf.With(p)
destination, err := u.ReadPacket(buffer)
if err != nil {
return 0, nil, err
}
return buffer.Len(), destination.UDPAddr(), nil
}
func (u *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return u.writePacketToClient(buffer, destination)
}
func (u *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
if err != nil {
return 0, err
}
return len(p), nil
}
func (u *serverPacketConn) readPacketFromClient(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
header := buf.NewSize(4 + 16 + 2 + 16 + 2 + 1)
defer header.Release()
_, err = header.ReadFullFrom(u.body, header.Cap())
if err != nil {
return
}
var length uint32
common.Must(binary.Read(header, binary.BigEndian, &length))
var sourceAddressBuffer [16]byte
common.Must1(header.Read(sourceAddressBuffer[:]))
var sourcePort uint16
common.Must(binary.Read(header, binary.BigEndian, &sourcePort))
_ = sourcePort
var destinationAddressBuffer [16]byte
common.Must1(header.Read(destinationAddressBuffer[:]))
destination.Addr = parse16BytesIP(destinationAddressBuffer)
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
var appNameLen uint8
common.Must(binary.Read(header, binary.BigEndian, &appNameLen))
if appNameLen > 0 {
err = rw.SkipN(u.body, int(appNameLen))
if err != nil {
return M.Socksaddr{}, err
}
}
payloadLen := int(length) - (16 + 2 + 16 + 2 + 1 + int(appNameLen))
if payloadLen < 0 {
return M.Socksaddr{}, E.New("invalid udp length: ", length)
}
_, err = buffer.ReadFullFrom(u.body, payloadLen)
return
}
func (u *serverPacketConn) writePacketToClient(buffer *buf.Buffer, source M.Socksaddr) error {
defer buffer.Release()
if !source.IsIP() {
return E.New("only support IP")
}
payloadLen := buffer.Len()
headerLen := 4 + 16 + 2 + 16 + 2
lengthField := uint32(16 + 2 + 16 + 2 + payloadLen)
sourceAddress := buildPaddingIP(source.Addr)
var destinationAddress [16]byte
var destinationPort uint16
var (
header *buf.Buffer
headerInBuffer bool
)
if buffer.Start() >= headerLen {
headerBytes := buffer.ExtendHeader(headerLen)
header = buf.With(headerBytes)
headerInBuffer = true
} else {
header = buf.NewSize(headerLen)
defer header.Release()
}
common.Must(binary.Write(header, binary.BigEndian, lengthField))
common.Must1(header.Write(sourceAddress[:]))
common.Must(binary.Write(header, binary.BigEndian, source.Port))
common.Must1(header.Write(destinationAddress[:]))
common.Must(binary.Write(header, binary.BigEndian, destinationPort))
if !headerInBuffer {
_, err := u.writer.Write(header.Bytes())
if err != nil {
return err
}
}
_, err := u.writer.Write(buffer.Bytes())
if err != nil {
return err
}
if u.flusher != nil {
u.flusher.Flush()
}
return nil
}

View File

@@ -0,0 +1,178 @@
package trusttunnel
import (
"bytes"
"encoding/base64"
"errors"
"io"
"net"
"net/http"
"net/netip"
"runtime"
"strings"
"time"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/gun"
)
const (
UDPMagicAddress = "_udp2"
ICMPMagicAddress = "_icmp"
HealthCheckMagicAddress = "_check"
DefaultQuicStreamReceiveWindow = 131072 // Chrome's default
DefaultConnectionTimeout = 30 * time.Second
DefaultHealthCheckTimeout = 7 * time.Second
DefaultQuicMaxIdleTimeout = 2 * (DefaultConnectionTimeout + DefaultHealthCheckTimeout)
DefaultSessionTimeout = 30 * time.Second
)
var (
AppName = C.Name
Version = C.Version
// TCPUserAgent is user-agent for TCP connections.
// Format: <platform> <app_name>
TCPUserAgent = runtime.GOOS + " " + AppName + "/" + Version
// UDPUserAgent is user-agent for UDP multiplexinh.
// Format: <platform> _udp2
UDPUserAgent = runtime.GOOS + " " + UDPMagicAddress
// ICMPUserAgent is user-agent for ICMP multiplexinh.
// Format: <platform> _icmp
ICMPUserAgent = runtime.GOOS + " " + ICMPMagicAddress
HealthCheckUserAgent = runtime.GOOS
)
func buildAuth(username string, password string) string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
}
// parseBasicAuth parses an HTTP Basic Authentication strinh.
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
func parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
// Case insensitive prefix match. See Issue 22736.
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return "", "", false
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return "", "", false
}
cs := string(c)
username, password, ok = strings.Cut(cs, ":")
if !ok {
return "", "", false
}
return username, password, true
}
func parse16BytesIP(buffer [16]byte) netip.Addr {
var zeroPrefix [12]byte
isIPv4 := bytes.HasPrefix(buffer[:], zeroPrefix[:])
// Special: check ::1
isIPv4 = isIPv4 && !(buffer[12] == 0 && buffer[13] == 0 && buffer[14] == 0 && buffer[15] == 1)
if isIPv4 {
return netip.AddrFrom4([4]byte(buffer[12:16]))
}
return netip.AddrFrom16(buffer)
}
func buildPaddingIP(addr netip.Addr) (buffer [16]byte) {
if addr.Is6() {
return addr.As16()
}
ipv4 := addr.As4()
copy(buffer[12:16], ipv4[:])
return buffer
}
type httpConn struct {
writer io.Writer
flusher http.Flusher
body io.ReadCloser
created chan struct{}
createErr error
gun.NetAddr
// deadlines
deadline *time.Timer
}
func (h *httpConn) setUp(body io.ReadCloser, err error) {
h.body = body
h.createErr = err
close(h.created)
}
func (h *httpConn) waitCreated() error {
if h.body != nil || h.createErr != nil {
return h.createErr
}
<-h.created
return h.createErr
}
func (h *httpConn) Close() error {
var errorArr []error
if closer, ok := h.writer.(io.Closer); ok {
errorArr = append(errorArr, closer.Close())
}
if h.body != nil {
errorArr = append(errorArr, h.body.Close())
}
return errors.Join(errorArr...)
}
func (h *httpConn) writeFlush(p []byte) (n int, err error) {
n, err = h.writer.Write(p)
if h.flusher != nil {
h.flusher.Flush()
}
return n, err
}
func (h *httpConn) SetReadDeadline(t time.Time) error { return h.SetDeadline(t) }
func (h *httpConn) SetWriteDeadline(t time.Time) error { return h.SetDeadline(t) }
func (h *httpConn) SetDeadline(t time.Time) error {
if t.IsZero() {
if h.deadline != nil {
h.deadline.Stop()
h.deadline = nil
}
return nil
}
d := time.Until(t)
if h.deadline != nil {
h.deadline.Reset(d)
return nil
}
h.deadline = time.AfterFunc(d, func() {
h.Close()
})
return nil
}
var _ net.Conn = (*tcpConn)(nil)
type tcpConn struct {
httpConn
}
func (t *tcpConn) Read(b []byte) (n int, err error) {
err = t.waitCreated()
if err != nil {
return 0, err
}
n, err = t.body.Read(b)
return
}
func (t *tcpConn) Write(b []byte) (int, error) {
return t.writeFlush(b)
}

View File

@@ -0,0 +1,85 @@
package trusttunnel
import (
"context"
"errors"
"net"
"runtime"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/mihomo/transport/vmess"
"github.com/metacubex/http"
"github.com/metacubex/quic-go"
"github.com/metacubex/quic-go/http3"
"github.com/metacubex/tls"
)
func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int) error {
stdConfig, err := tlsConfig.ToStdConfig()
if err != nil {
return err
}
c.roundTripper = &http3.Transport{
TLSClientConfig: stdConfig,
QUICConfig: &quic.Config{
Versions: []quic.Version{quic.Version1},
MaxIdleTimeout: DefaultQuicMaxIdleTimeout,
InitialStreamReceiveWindow: DefaultQuicStreamReceiveWindow,
DisablePathMTUDiscovery: !(runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "darwin"),
Allow0RTT: false,
},
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
addrPort, err := c.resolv(ctx, c.server)
if err != nil {
return nil, err
}
err = tlsConfig.ECH.ClientHandle(ctx, tlsCfg)
if err != nil {
return nil, err
}
packetConn, err := c.dialer.ListenPacket(ctx, "udp", "", addrPort)
if err != nil {
return nil, err
}
quicConn, err := quic.DialEarly(ctx, packetConn, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
if err != nil {
_ = packetConn.Close()
return nil, err
}
common.SetCongestionController(quicConn, congestionControlName, cwnd)
return quicConn, nil
},
}
return nil
}
func (s *Service) configHTTP3Server(tlsConfig *tls.Config, udpConn net.PacketConn) error {
tlsConfig = http3.ConfigureTLSConfig(tlsConfig)
quicListener, err := quic.ListenEarly(udpConn, tlsConfig, &quic.Config{
Versions: []quic.Version{quic.Version1},
MaxIdleTimeout: DefaultQuicMaxIdleTimeout,
MaxIncomingStreams: 1 << 60,
Allow0RTT: true,
})
if err != nil {
return err
}
h3Server := &http3.Server{
Handler: s,
IdleTimeout: DefaultSessionTimeout,
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd)
return ctx
},
}
s.h3Server = h3Server
s.udpConn = udpConn
go func() {
sErr := h3Server.ServeListener(quicListener)
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
s.logger.ErrorContext(s.ctx, "HTTP3 server close: ", sErr)
}
}()
return nil
}

View File

@@ -0,0 +1,250 @@
package trusttunnel
import (
"context"
"errors"
"net"
"time"
"github.com/metacubex/http"
"github.com/metacubex/http/h2c"
"github.com/metacubex/quic-go/http3"
"github.com/metacubex/sing/common"
"github.com/metacubex/sing/common/auth"
"github.com/metacubex/sing/common/buf"
"github.com/metacubex/sing/common/bufio"
E "github.com/metacubex/sing/common/exceptions"
"github.com/metacubex/sing/common/logger"
M "github.com/metacubex/sing/common/metadata"
N "github.com/metacubex/sing/common/network"
"github.com/metacubex/tls"
)
type Handler interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
}
type ICMPHandler interface {
NewICMPConnection(ctx context.Context, conn *IcmpConn)
}
type ServiceOptions struct {
Ctx context.Context
Logger logger.ContextLogger
Handler Handler
ICMPHandler ICMPHandler
QUICCongestionControl string
QUICCwnd int
}
type Service struct {
ctx context.Context
logger logger.ContextLogger
users map[string]string
handler Handler
icmpHandler ICMPHandler
quicCongestionControl string
quicCwnd int
httpServer *http.Server
h2Server *http.Http2Server
h3Server *http3.Server
tcpListener net.Listener
tlsListener net.Listener
udpConn net.PacketConn
}
func NewService(options ServiceOptions) *Service {
return &Service{
ctx: options.Ctx,
logger: options.Logger,
handler: options.Handler,
icmpHandler: options.ICMPHandler,
quicCongestionControl: options.QUICCongestionControl,
quicCwnd: options.QUICCwnd,
}
}
func (s *Service) Start(tcpListener net.Listener, udpConn net.PacketConn, tlsConfig *tls.Config) error {
if tcpListener != nil {
h2Server := &http.Http2Server{}
s.httpServer = &http.Server{
Handler: h2c.NewHandler(s, h2Server),
IdleTimeout: DefaultSessionTimeout,
BaseContext: func(net.Listener) context.Context {
return s.ctx
},
}
err := http.Http2ConfigureServer(s.httpServer, h2Server)
if err != nil {
return err
}
s.h2Server = h2Server
listener := tcpListener
s.tcpListener = tcpListener
if tlsConfig != nil {
listener = tls.NewListener(listener, tlsConfig)
s.tlsListener = listener
}
go func() {
sErr := s.httpServer.Serve(listener)
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
s.logger.ErrorContext(s.ctx, "HTTP server close: ", sErr)
}
}()
}
if udpConn != nil {
err := s.configHTTP3Server(tlsConfig, udpConn)
if err != nil {
return err
}
}
return nil
}
func (s *Service) UpdateUsers(users map[string]string) {
s.users = users
}
func (s *Service) Close() error {
var shutdownErr error
if s.httpServer != nil {
const shutdownTimeout = 5 * time.Second
ctx, cancel := context.WithTimeout(s.ctx, shutdownTimeout)
shutdownErr = s.httpServer.Shutdown(ctx)
cancel()
if errors.Is(shutdownErr, http.ErrServerClosed) {
shutdownErr = nil
}
}
closeErr := common.Close(
common.PtrOrNil(s.httpServer),
s.tlsListener,
s.tcpListener,
common.PtrOrNil(s.h3Server),
s.udpConn,
)
return E.Errors(shutdownErr, closeErr)
}
func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
authorization := request.Header.Get("Proxy-Authorization")
username, loaded := s.verify(authorization)
if !loaded {
writer.WriteHeader(http.StatusProxyAuthRequired)
s.badRequest(request.Context(), request, E.New("authorization failed"))
return
}
if request.Method != http.MethodConnect {
writer.WriteHeader(http.StatusMethodNotAllowed)
s.badRequest(request.Context(), request, E.New("unexpected HTTP method ", request.Method))
return
}
ctx := request.Context()
ctx = auth.ContextWithUser(ctx, username)
s.logger.DebugContext(ctx, "[", username, "] ", "request from ", request.RemoteAddr)
s.logger.DebugContext(ctx, "[", username, "] ", "request to ", request.Host)
switch request.Host {
case UDPMagicAddress:
writer.WriteHeader(http.StatusOK)
flusher, isFlusher := writer.(http.Flusher)
if isFlusher {
flusher.Flush()
}
conn := &serverPacketConn{
packetConn: packetConn{
httpConn: httpConn{
writer: writer,
flusher: flusher,
created: make(chan struct{}),
},
},
}
conn.SetAddrFromRequest(request)
conn.setUp(request.Body, nil)
firstPacket := buf.NewPacket()
destination, err := conn.ReadPacket(firstPacket)
if err != nil {
firstPacket.Release()
_ = conn.Close()
s.logger.ErrorContext(ctx, E.Cause(err, "read first packet of ", request.RemoteAddr))
return
}
destination = destination.Unwrap()
cachedConn := bufio.NewCachedPacketConn(conn, firstPacket, destination)
_ = s.handler.NewPacketConnection(ctx, cachedConn, M.Metadata{
Protocol: "trusttunnel",
Source: M.ParseSocksaddr(request.RemoteAddr),
Destination: destination,
})
case ICMPMagicAddress:
flusher, isFlusher := writer.(http.Flusher)
if s.icmpHandler == nil {
writer.WriteHeader(http.StatusNotImplemented)
if isFlusher {
flusher.Flush()
}
_ = request.Body.Close()
} else {
writer.WriteHeader(http.StatusOK)
if isFlusher {
flusher.Flush()
}
conn := &IcmpConn{
httpConn{
writer: writer,
flusher: flusher,
created: make(chan struct{}),
},
}
conn.SetAddrFromRequest(request)
conn.setUp(request.Body, nil)
s.icmpHandler.NewICMPConnection(ctx, conn)
}
case HealthCheckMagicAddress:
writer.WriteHeader(http.StatusOK)
if flusher, isFlusher := writer.(http.Flusher); isFlusher {
flusher.Flush()
}
_ = request.Body.Close()
default:
writer.WriteHeader(http.StatusOK)
flusher, isFlusher := writer.(http.Flusher)
if isFlusher {
flusher.Flush()
}
conn := &tcpConn{
httpConn{
writer: writer,
flusher: flusher,
created: make(chan struct{}),
},
}
conn.SetAddrFromRequest(request)
conn.setUp(request.Body, nil)
_ = s.handler.NewConnection(ctx, conn, M.Metadata{
Protocol: "trusttunnel",
Source: M.ParseSocksaddr(request.RemoteAddr),
Destination: M.ParseSocksaddr(request.Host).Unwrap(),
})
}
}
func (s *Service) verify(authorization string) (username string, loaded bool) {
username, password, loaded := parseBasicAuth(authorization)
if !loaded {
return "", false
}
recordedPassword, loaded := s.users[username]
if !loaded {
return "", false
}
if password != recordedPassword {
return "", false
}
return username, true
}
func (s *Service) badRequest(ctx context.Context, request *http.Request, err error) {
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
}

View File

@@ -24,12 +24,8 @@ type TLSConfig struct {
Reality *tlsC.RealityConfig
}
type ECHConfig struct {
Enable bool
}
func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
tlsConfig, err := ca.GetTLSConfig(ca.Option{
func (cfg *TLSConfig) ToStdConfig() (*tls.Config, error) {
return ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
ServerName: cfg.Host,
InsecureSkipVerify: cfg.SkipCertVerify,
@@ -39,6 +35,10 @@ func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn
Certificate: cfg.Certificate,
PrivateKey: cfg.PrivateKey,
})
}
func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
tlsConfig, err := cfg.ToStdConfig()
if err != nil {
return nil, err
}