mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2025-12-19 08:20:05 +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
|
||||
}
|
||||
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:
|
||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ const (
|
||||
Mieru
|
||||
AnyTLS
|
||||
Sudoku
|
||||
ECHTunnel
|
||||
)
|
||||
|
||||
const (
|
||||
@ -212,6 +213,8 @@ func (at AdapterType) String() string {
|
||||
return "AnyTLS"
|
||||
case Sudoku:
|
||||
return "Sudoku"
|
||||
case ECHTunnel:
|
||||
return "ECHTunnel"
|
||||
case Relay:
|
||||
return "Relay"
|
||||
case Selector:
|
||||
|
||||
1
go.mod
1
go.mod
@ -12,6 +12,7 @@ require (
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.4.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/klauspost/compress v1.17.9 // lastest version compatible with golang1.20
|
||||
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/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
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/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=
|
||||
|
||||
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