feat: support trusttunnel inbound and outbound

This commit is contained in:
wwqgtxx
2026-02-25 11:49:29 +08:00
parent 836c972c54
commit 4ca515896b
24 changed files with 1881 additions and 37 deletions

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

@@ -166,6 +166,13 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
break break
} }
proxy, err = outbound.NewMasque(*masqueOption) 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: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }

View File

@@ -1,7 +1,6 @@
package buf package buf
import ( import (
"github.com/metacubex/sing/common"
"github.com/metacubex/sing/common/buf" "github.com/metacubex/sing/common/buf"
) )
@@ -9,14 +8,52 @@ const BufferSize = buf.BufferSize
type Buffer = buf.Buffer type Buffer = buf.Buffer
var New = buf.New func New() *Buffer {
var NewPacket = buf.NewPacket return buf.New()
var NewSize = buf.NewSize }
var With = buf.With
var As = buf.As
var ReleaseMulti = buf.ReleaseMulti
var ( func NewPacket() *Buffer {
Must = common.Must return buf.NewPacket()
Error = common.Error }
)
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

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

View File

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

View File

@@ -1115,6 +1115,23 @@ proxies: # socks5
# - http/1.1 # - http/1.1
# skip-cert-verify: true # 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 模块,所有请求均在内部处理 # dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out" - name: "dns-out"
type: dns type: dns
@@ -1731,6 +1748,30 @@ listeners:
# masquerade: http://127.0.0.1:8080 #作为反向代理 # masquerade: http://127.0.0.1:8080 #作为反向代理
# masquerade: https://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 # 注意listeners中的tun仅提供给高级用户使用普通用户应使用顶层配置中的tun
- name: tun-in-1 - name: tun-in-1
type: tun type: tun

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

@@ -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

@@ -141,6 +141,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
return nil, err return nil, err
} }
listener, err = IN.NewSudoku(sudokuOption) 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: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) 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

@@ -40,10 +40,10 @@ var defaultHeader = http.Header{
type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error) type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error)
type Conn struct { type Conn struct {
initFn func() (io.ReadCloser, netAddr, error) initFn func() (io.ReadCloser, NetAddr, error)
writer io.Writer // writer must not nil writer io.Writer // writer must not nil
closer io.Closer closer io.Closer
netAddr NetAddr
initOnce sync.Once initOnce sync.Once
initErr error initErr error
@@ -73,7 +73,7 @@ func (g *Conn) initReader() {
} }
return return
} }
g.netAddr = addr g.NetAddr = addr
g.closeMutex.Lock() g.closeMutex.Lock()
defer g.closeMutex.Unlock() defer g.closeMutex.Unlock()
@@ -339,12 +339,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
request = request.WithContext(transport.ctx) request = request.WithContext(transport.ctx)
conn := &Conn{ conn := &Conn{
initFn: func() (io.ReadCloser, netAddr, error) { initFn: func() (io.ReadCloser, NetAddr, error) {
nAddr := netAddr{} nAddr := NetAddr{}
trace := &httptrace.ClientTrace{ trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) { GotConn: func(connInfo httptrace.GotConnInfo) {
nAddr.localAddr = connInfo.Conn.LocalAddr() nAddr.SetLocalAddr(connInfo.Conn.LocalAddr())
nAddr.remoteAddr = connInfo.Conn.RemoteAddr() nAddr.SetRemoteAddr(connInfo.Conn.RemoteAddr())
}, },
} }
request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace)) request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))

View File

@@ -9,7 +9,6 @@ import (
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/http" "github.com/metacubex/http"
"github.com/metacubex/http/h2c" "github.com/metacubex/http/h2c"
@@ -42,17 +41,9 @@ func NewServerHandler(options ServerOption) http.Handler {
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
conn := &Conn{ conn := &Conn{
initFn: func() (io.ReadCloser, netAddr, error) { initFn: func() (io.ReadCloser, NetAddr, error) {
nAddr := netAddr{} nAddr := NetAddr{}
if request.RemoteAddr != "" { nAddr.SetAddrFromRequest(request)
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
}
return request.Body, nAddr, nil return request.Body, nAddr, nil
}, },
writer: writer, writer: writer,

View File

@@ -5,6 +5,8 @@ import (
"net" "net"
"sync" "sync"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/http" "github.com/metacubex/http"
) )
@@ -18,20 +20,40 @@ type TransportWrap struct {
func (tw *TransportWrap) Close() error { func (tw *TransportWrap) Close() error {
tw.closeOnce.Do(func() { tw.closeOnce.Do(func() {
tw.cancel() tw.cancel()
closeTransport(tw.Http2Transport) CloseTransport(tw.Http2Transport)
}) })
return nil return nil
} }
type netAddr struct { type NetAddr struct {
remoteAddr net.Addr remoteAddr net.Addr
localAddr net.Addr localAddr net.Addr
} }
func (addr netAddr) RemoteAddr() net.Addr { func (addr NetAddr) RemoteAddr() net.Addr {
return addr.remoteAddr return addr.remoteAddr
} }
func (addr netAddr) LocalAddr() net.Addr { func (addr NetAddr) LocalAddr() net.Addr {
return addr.localAddr 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() _ = cc.Close()
} }
func closeTransport(tr *http.Http2Transport) { func CloseTransport(tr *http.Http2Transport) {
connPool := transportConnPool(tr) connPool := transportConnPool(tr)
p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data)
p.mu.Lock() p.mu.Lock()

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