mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-02-27 01:07:10 +00:00
feat: support trusttunnel inbound and outbound
This commit is contained in:
24
listener/config/trusttunnel.go
Normal file
24
listener/config/trusttunnel.go
Normal 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)
|
||||
}
|
||||
96
listener/inbound/trusttunnel.go
Normal file
96
listener/inbound/trusttunnel.go
Normal 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)
|
||||
109
listener/inbound/trusttunnel_test.go
Normal file
109
listener/inbound/trusttunnel_test.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
188
listener/trusttunnel/server.go
Normal file
188
listener/trusttunnel/server.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user