mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-02-26 16:57:08 +00:00
feat: support trusttunnel inbound and outbound
This commit is contained in:
144
adapter/outbound/trusttunnel.go
Normal file
144
adapter/outbound/trusttunnel.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
269
transport/trusttunnel/client.go
Normal file
269
transport/trusttunnel/client.go
Normal 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
|
||||||
|
}
|
||||||
4
transport/trusttunnel/doc.go
Normal file
4
transport/trusttunnel/doc.go
Normal 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
|
||||||
18
transport/trusttunnel/force_close.go
Normal file
18
transport/trusttunnel/force_close.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
82
transport/trusttunnel/icmp.go
Normal file
82
transport/trusttunnel/icmp.go
Normal 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()))
|
||||||
|
}
|
||||||
280
transport/trusttunnel/packet.go
Normal file
280
transport/trusttunnel/packet.go
Normal 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
|
||||||
|
}
|
||||||
178
transport/trusttunnel/protocol.go
Normal file
178
transport/trusttunnel/protocol.go
Normal 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)
|
||||||
|
}
|
||||||
85
transport/trusttunnel/quic.go
Normal file
85
transport/trusttunnel/quic.go
Normal 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
|
||||||
|
}
|
||||||
250
transport/trusttunnel/service.go
Normal file
250
transport/trusttunnel/service.go
Normal 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))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user