From 4ca515896b4a67792a7b727c3c0288a3ca4831cb Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 25 Feb 2026 11:49:29 +0800 Subject: [PATCH] feat: support trusttunnel inbound and outbound --- adapter/outbound/trusttunnel.go | 144 ++++++++++++++ adapter/parser.go | 7 + common/buf/sing.go | 59 ++++-- constant/adapters.go | 3 + constant/metadata.go | 5 + docs/config.yaml | 41 ++++ listener/config/trusttunnel.go | 24 +++ listener/inbound/trusttunnel.go | 96 +++++++++ listener/inbound/trusttunnel_test.go | 109 +++++++++++ listener/parse.go | 7 + listener/trusttunnel/server.go | 188 ++++++++++++++++++ transport/gun/gun.go | 14 +- transport/gun/server.go | 15 +- transport/gun/transport.go | 30 ++- transport/gun/transport_close.go | 2 +- transport/trusttunnel/client.go | 269 +++++++++++++++++++++++++ transport/trusttunnel/doc.go | 4 + transport/trusttunnel/force_close.go | 18 ++ transport/trusttunnel/icmp.go | 82 ++++++++ transport/trusttunnel/packet.go | 280 +++++++++++++++++++++++++++ transport/trusttunnel/protocol.go | 178 +++++++++++++++++ transport/trusttunnel/quic.go | 85 ++++++++ transport/trusttunnel/service.go | 250 ++++++++++++++++++++++++ transport/vmess/tls.go | 8 +- 24 files changed, 1881 insertions(+), 37 deletions(-) create mode 100644 adapter/outbound/trusttunnel.go create mode 100644 listener/config/trusttunnel.go create mode 100644 listener/inbound/trusttunnel.go create mode 100644 listener/inbound/trusttunnel_test.go create mode 100644 listener/trusttunnel/server.go create mode 100644 transport/trusttunnel/client.go create mode 100644 transport/trusttunnel/doc.go create mode 100644 transport/trusttunnel/force_close.go create mode 100644 transport/trusttunnel/icmp.go create mode 100644 transport/trusttunnel/packet.go create mode 100644 transport/trusttunnel/protocol.go create mode 100644 transport/trusttunnel/quic.go create mode 100644 transport/trusttunnel/service.go diff --git a/adapter/outbound/trusttunnel.go b/adapter/outbound/trusttunnel.go new file mode 100644 index 00000000..1fc7ce91 --- /dev/null +++ b/adapter/outbound/trusttunnel.go @@ -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 +} diff --git a/adapter/parser.go b/adapter/parser.go index 290a4a8f..9c959106 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -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) } diff --git a/common/buf/sing.go b/common/buf/sing.go index 73f65f15..6df9df47 100644 --- a/common/buf/sing.go +++ b/common/buf/sing.go @@ -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 +} diff --git a/constant/adapters.go b/constant/adapters.go index e451dc92..3dcc7158 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -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: diff --git a/constant/metadata.go b/constant/metadata.go index 63f08d43..043285ee 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -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: diff --git a/docs/config.yaml b/docs/config.yaml index 9e3bbcf1..63e54dfe 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1115,6 +1115,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 @@ -1731,6 +1748,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 diff --git a/listener/config/trusttunnel.go b/listener/config/trusttunnel.go new file mode 100644 index 00000000..54415538 --- /dev/null +++ b/listener/config/trusttunnel.go @@ -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) +} diff --git a/listener/inbound/trusttunnel.go b/listener/inbound/trusttunnel.go new file mode 100644 index 00000000..b476db6e --- /dev/null +++ b/listener/inbound/trusttunnel.go @@ -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) diff --git a/listener/inbound/trusttunnel_test.go b/listener/inbound/trusttunnel_test.go new file mode 100644 index 00000000..5283d088 --- /dev/null +++ b/listener/inbound/trusttunnel_test.go @@ -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) +} diff --git a/listener/parse.go b/listener/parse.go index 35941141..cbd40676 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -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) } diff --git a/listener/trusttunnel/server.go b/listener/trusttunnel/server.go new file mode 100644 index 00000000..23939e17 --- /dev/null +++ b/listener/trusttunnel/server.go @@ -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 +} diff --git a/transport/gun/gun.go b/transport/gun/gun.go index aeddab2d..27c0cbbb 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -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 @@ -73,7 +73,7 @@ func (g *Conn) initReader() { } return } - g.netAddr = addr + g.NetAddr = addr g.closeMutex.Lock() defer g.closeMutex.Unlock() @@ -339,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)) diff --git a/transport/gun/server.go b/transport/gun/server.go index c240459f..6c292024 100644 --- a/transport/gun/server.go +++ b/transport/gun/server.go @@ -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, diff --git a/transport/gun/transport.go b/transport/gun/transport.go index 5925b352..4b9da971 100644 --- a/transport/gun/transport.go +++ b/transport/gun/transport.go @@ -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 +} diff --git a/transport/gun/transport_close.go b/transport/gun/transport_close.go index b9c76134..44fefd7c 100644 --- a/transport/gun/transport_close.go +++ b/transport/gun/transport_close.go @@ -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() diff --git a/transport/trusttunnel/client.go b/transport/trusttunnel/client.go new file mode 100644 index 00000000..d2d25ea0 --- /dev/null +++ b/transport/trusttunnel/client.go @@ -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 +} diff --git a/transport/trusttunnel/doc.go b/transport/trusttunnel/doc.go new file mode 100644 index 00000000..65e3d80d --- /dev/null +++ b/transport/trusttunnel/doc.go @@ -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 diff --git a/transport/trusttunnel/force_close.go b/transport/trusttunnel/force_close.go new file mode 100644 index 00000000..3253b6c6 --- /dev/null +++ b/transport/trusttunnel/force_close.go @@ -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() + } +} diff --git a/transport/trusttunnel/icmp.go b/transport/trusttunnel/icmp.go new file mode 100644 index 00000000..908887d4 --- /dev/null +++ b/transport/trusttunnel/icmp.go @@ -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())) +} diff --git a/transport/trusttunnel/packet.go b/transport/trusttunnel/packet.go new file mode 100644 index 00000000..b1a3af76 --- /dev/null +++ b/transport/trusttunnel/packet.go @@ -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 +} diff --git a/transport/trusttunnel/protocol.go b/transport/trusttunnel/protocol.go new file mode 100644 index 00000000..f541d687 --- /dev/null +++ b/transport/trusttunnel/protocol.go @@ -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: + TCPUserAgent = runtime.GOOS + " " + AppName + "/" + Version + + // UDPUserAgent is user-agent for UDP multiplexinh. + // Format: _udp2 + UDPUserAgent = runtime.GOOS + " " + UDPMagicAddress + + // ICMPUserAgent is user-agent for ICMP multiplexinh. + // Format: _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) +} diff --git a/transport/trusttunnel/quic.go b/transport/trusttunnel/quic.go new file mode 100644 index 00000000..011a01fb --- /dev/null +++ b/transport/trusttunnel/quic.go @@ -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 +} diff --git a/transport/trusttunnel/service.go b/transport/trusttunnel/service.go new file mode 100644 index 00000000..d248d2df --- /dev/null +++ b/transport/trusttunnel/service.go @@ -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)) +} diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index ff915b37..8add557f 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -24,8 +24,8 @@ type TLSConfig struct { Reality *tlsC.RealityConfig } -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, @@ -35,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 }