mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2025-12-19 16:30:07 +08:00
feat: Add ECH-Tunnel protocol support
- Add ECHTunnel adapter implementation - Add transport layer for WebSocket+ECH - Add protocol parser and constants - Add gorilla/websocket dependency
This commit is contained in:
parent
b753a57e6a
commit
86835c9be3
115
adapter/outbound/echtunnel.go
Normal file
115
adapter/outbound/echtunnel.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
"github.com/metacubex/mihomo/transport/echtunnel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ECHTunnel struct {
|
||||||
|
*Base
|
||||||
|
option *ECHTunnelOption
|
||||||
|
client *echtunnel.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type ECHTunnelOption struct {
|
||||||
|
BasicOption
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
WSPath string `proxy:"ws-path,omitempty"`
|
||||||
|
Token string `proxy:"token,omitempty"`
|
||||||
|
ECHDomain string `proxy:"ech-domain,omitempty"`
|
||||||
|
DNS string `proxy:"dns,omitempty"`
|
||||||
|
IP string `proxy:"ip,omitempty"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (e *ECHTunnel) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
|
// 使用 ECH-Tunnel 客户端建立连接
|
||||||
|
c, err := e.client.DialContext(ctx, metadata.RemoteAddress())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", e.addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, e), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (e *ECHTunnel) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
if !e.option.UDP {
|
||||||
|
return nil, fmt.Errorf("UDP not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECH-Tunnel 的 UDP 实现
|
||||||
|
c, err := e.client.DialContext(ctx, metadata.RemoteAddress())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", e.addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := echtunnel.NewPacketConn(c)
|
||||||
|
return newPacketConn(pc, e), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUOT implements C.ProxyAdapter
|
||||||
|
func (e *ECHTunnel) SupportUOT() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyInfo implements C.ProxyAdapter
|
||||||
|
func (e *ECHTunnel) ProxyInfo() C.ProxyInfo {
|
||||||
|
info := e.Base.ProxyInfo()
|
||||||
|
info.DialerProxy = e.option.DialerProxy
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements C.ProxyAdapter
|
||||||
|
func (e *ECHTunnel) Close() error {
|
||||||
|
if e.client != nil {
|
||||||
|
return e.client.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewECHTunnel(option ECHTunnelOption) (*ECHTunnel, error) {
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
|
||||||
|
client, err := echtunnel.NewClient(echtunnel.Config{
|
||||||
|
Server: option.Server,
|
||||||
|
Port: option.Port,
|
||||||
|
WSPath: option.WSPath,
|
||||||
|
Token: option.Token,
|
||||||
|
ECHDomain: option.ECHDomain,
|
||||||
|
DNS: option.DNS,
|
||||||
|
IP: option.IP,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &ECHTunnel{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.ECHTunnel,
|
||||||
|
pdName: option.ProviderName,
|
||||||
|
udp: option.UDP,
|
||||||
|
tfo: option.TFO,
|
||||||
|
mpTcp: option.MPTCP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
prefer: option.IPVersion,
|
||||||
|
},
|
||||||
|
option: &option,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
e.dialer = option.NewDialer(e.DialOptions())
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
@ -159,6 +159,13 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewSudoku(*sudokuOption)
|
proxy, err = outbound.NewSudoku(*sudokuOption)
|
||||||
|
case "echtunnel":
|
||||||
|
echTunnelOption := &outbound.ECHTunnelOption{BasicOption: basicOption}
|
||||||
|
err = decoder.Decode(mapping, echTunnelOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewECHTunnel(*echTunnelOption)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ const (
|
|||||||
Mieru
|
Mieru
|
||||||
AnyTLS
|
AnyTLS
|
||||||
Sudoku
|
Sudoku
|
||||||
|
ECHTunnel
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -212,6 +213,8 @@ func (at AdapterType) String() string {
|
|||||||
return "AnyTLS"
|
return "AnyTLS"
|
||||||
case Sudoku:
|
case Sudoku:
|
||||||
return "Sudoku"
|
return "Sudoku"
|
||||||
|
case ECHTunnel:
|
||||||
|
return "ECHTunnel"
|
||||||
case Relay:
|
case Relay:
|
||||||
return "Relay"
|
return "Relay"
|
||||||
case Selector:
|
case Selector:
|
||||||
|
|||||||
1
go.mod
1
go.mod
@ -12,6 +12,7 @@ require (
|
|||||||
github.com/gobwas/ws v1.4.0
|
github.com/gobwas/ws v1.4.0
|
||||||
github.com/gofrs/uuid/v5 v5.4.0
|
github.com/gofrs/uuid/v5 v5.4.0
|
||||||
github.com/golang/snappy v1.0.0
|
github.com/golang/snappy v1.0.0
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
||||||
github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20
|
github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20
|
||||||
github.com/mdlayher/netlink v1.7.2
|
github.com/mdlayher/netlink v1.7.2
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -68,6 +68,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||||
|
|||||||
92
transport/echtunnel/client.go
Normal file
92
transport/echtunnel/client.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package echtunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server string
|
||||||
|
Port int
|
||||||
|
WSPath string
|
||||||
|
Token string
|
||||||
|
ECHDomain string
|
||||||
|
DNS string
|
||||||
|
IP string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
dialer *websocket.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(config Config) (*Client, error) {
|
||||||
|
// 设置默认值
|
||||||
|
if config.WSPath == "" {
|
||||||
|
config.WSPath = "/tunnel"
|
||||||
|
}
|
||||||
|
if config.ECHDomain == "" {
|
||||||
|
config.ECHDomain = "cloudflare-ech.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS 配置
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: config.ECHDomain,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
// TODO: 添加 ECH 配置
|
||||||
|
// 可以使用 Mihomo 现有的 ECH 支持
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := &websocket.Dialer{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
HandshakeTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
config: config,
|
||||||
|
dialer: dialer,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DialContext(ctx context.Context, address string) (net.Conn, error) {
|
||||||
|
// 构建 WebSocket URL
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "wss",
|
||||||
|
Host: net.JoinHostPort(c.config.Server, strconv.Itoa(c.config.Port)),
|
||||||
|
Path: c.config.WSPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 Token
|
||||||
|
if c.config.Token != "" {
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("token", c.config.Token)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立 WebSocket 连接
|
||||||
|
conn, _, err := c.dialer.DialContext(ctx, u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("websocket dial failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送目标地址
|
||||||
|
err = conn.WriteMessage(websocket.TextMessage, []byte(address))
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("send target address failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回包装后的连接
|
||||||
|
return &WSConn{Conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
transport/echtunnel/conn.go
Normal file
76
transport/echtunnel/conn.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package echtunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WSConn struct {
|
||||||
|
*websocket.Conn
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WSConn) Read(b []byte) (int, error) {
|
||||||
|
if c.reader == nil {
|
||||||
|
msgType, r, err := c.NextReader()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if msgType != websocket.BinaryMessage {
|
||||||
|
return 0, fmt.Errorf("unexpected message type: %d", msgType)
|
||||||
|
}
|
||||||
|
c.reader = r
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := c.reader.Read(b)
|
||||||
|
if err == io.EOF {
|
||||||
|
c.reader = nil
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WSConn) Write(b []byte) (int, error) {
|
||||||
|
err := c.WriteMessage(websocket.BinaryMessage, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WSConn) SetDeadline(t time.Time) error {
|
||||||
|
if err := c.SetReadDeadline(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WSConn) LocalAddr() net.Addr {
|
||||||
|
return c.Conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WSConn) RemoteAddr() net.Addr {
|
||||||
|
return c.Conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketConn for UDP
|
||||||
|
type PacketConn struct {
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPacketConn(conn net.Conn) *PacketConn {
|
||||||
|
return &PacketConn{Conn: conn}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
|
n, err := pc.Read(b)
|
||||||
|
return n, pc.RemoteAddr(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
|
return pc.Write(b)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user