Compare commits

...

54 Commits

Author SHA1 Message Date
wwqgtxx
c0f452b540 chore: more unmap for 4in6 address 2025-05-29 10:14:06 +08:00
wwqgtxx
6c9abe16cc fix: vmess listener error 2025-05-28 21:33:44 +08:00
wwqgtxx
213d80c1e2 fix: quic sniffer should consider skipDomain 2025-05-28 10:06:53 +08:00
wwqgtxx
1db89da122 fix: quic sniffer should not replace domain when no valid host is read 2025-05-28 09:22:28 +08:00
wwqgtxx
689c58f661 chore: clear dstIP when overrideDest in sniffer 2025-05-27 22:47:21 +08:00
wwqgtxx
33590c4066 fix: destination should unmap before find interface 2025-05-27 18:26:35 +08:00
wwqgtxx
60ae9dce56 chore: recover log leval for preHandleMetadata 2025-05-27 18:10:44 +08:00
wwqgtxx
4741ac6702 fix: in-port not work with shadowsocks listener 2025-05-27 16:32:42 +08:00
wwqgtxx
ef3d7e4dd7 chore: remove unneeded dns resolve when proxydialer dial udp 2025-05-27 15:04:01 +08:00
wwqgtxx
a1c7881229 chore: rebuild udp dns resolve
The DNS resolution of the overall UDP part has been delayed to the connection initiation stage. During the rule matching process, it will only be triggered when the IP rule without no-resolve is matched.

For direct and wireguard outbound, the same logic as the TCP part will be followed, that is, when direct-nameserver (or DNS configured by wireguard) exists, the result of the matching process will be discarded and the domain name will be re-resolved. This re-resolution logic is only effective for fakeip.

For reject and DNS outbound, no resolution is required.

For other outbound, resolution will still be performed when the connection is initiated, and the domain name will not be sent directly to the remote server at present.
2025-05-27 10:45:26 +08:00
wwqgtxx
12e3952b74 chore: code cleanup 2025-05-26 12:33:24 +08:00
wwqgtxx
88419cbd12 chore: better parse remote dst 2025-05-26 01:12:35 +08:00
wwqgtxx
4ed830330e chore: remove confused code 2025-05-25 22:22:23 +08:00
wwqgtxx
3ed6ff9402 chore: export pipeDeadline 2025-05-25 22:07:29 +08:00
wwqgtxx
34de62d21d chore: better get localAddr 2025-05-24 23:19:38 +08:00
wwqgtxx
d2e255f257 fix: some error in tun 2025-05-24 22:23:10 +08:00
wwqgtxx
a0c46bb4b7 chore: remove the redundant layer of udpnat in sing-tun to reduce resource usage when processing udp 2025-05-24 15:57:49 +08:00
wwqgtxx
9e3bf14b1a chore: handle two interfaces have the same prefix but different address 2025-05-24 11:32:36 +08:00
wwqgtxx
28c387a9b6 chore: restore break change in sing-tun 2025-05-23 20:19:18 +08:00
wwqgtxx
15eda703b4 fix: hysteria2 panic 2025-05-23 20:12:38 +08:00
wwqgtxx
b1d12a15db chore: proxy's ech should fetch from proxy-nameserver 2025-05-22 17:42:40 +08:00
wwqgtxx
5a21bf3642 fix: listener close panic 2025-05-22 17:01:24 +08:00
wwqgtxx
199fb8fd5d chore: update quic-go to 0.52.0 2025-05-22 10:28:10 +08:00
wwqgtxx
fd959feff2 chore: update dependencies 2025-05-21 21:37:20 +08:00
wwqgtxx
d5a03901d2 fix: race in close grpc transport 2025-05-20 16:15:04 +08:00
wwqgtxx
257fead538 docs: update config.yaml follow 5cf0f18c 2025-05-20 11:08:42 +08:00
wwqgtxx
c489c5260b fix: hysteria2 hop ports init
https://github.com/MetaCubeX/mihomo/issues/2056
2025-05-20 10:56:14 +08:00
wwqgtxx
8f92b1de13 chore: simplify the single root decompression process 2025-05-20 09:48:05 +08:00
wwqgtxx
9f7a2a36c1 chore: unpack externalUI in a separate temporary directory to avoid malicious compressed packages from polluting workdir 2025-05-20 01:58:25 +08:00
wwqgtxx
a93479124c chore: stricter path checking when unpacking zip/tgz 2025-05-20 00:00:30 +08:00
wwqgtxx
ed42c4feb8 chore: disallow symlink in unzip 2025-05-19 23:42:39 +08:00
wwqgtxx
608ddb1b44 fix: external-ui-name must in local 2025-05-19 23:11:52 +08:00
wwqgtxx
d036d98128 fix: http server does not handle http2 logic correctly 2025-05-18 23:05:00 +08:00
wwqgtxx
d900c71214 fix: shadowtls v2 not work with X25519MLKEM768 2025-05-18 23:03:07 +08:00
wwqgtxx
1672750c47 chore: simplifying the old fingerprint processing method 2025-05-18 23:03:07 +08:00
wwqgtxx
41b57afb3f fix: grpc deadline implement 2025-05-18 23:03:07 +08:00
wwqgtxx
188372cb04 feat: add tls.ech-key for external-controller-tls 2025-05-17 21:21:02 +08:00
wwqgtxx
a1350d4985 feat: add ech-key for listeners 2025-05-17 20:50:21 +08:00
wwqgtxx
dc958e6a39 feat: add ech-opts for hysteria/hysteria2/tuic outbound 2025-05-17 18:41:39 +08:00
wwqgtxx
8a5f3b8909 chore: simplify port hop costs 2025-05-17 17:06:38 +08:00
wwqgtxx
c6d7ef8cb8 feat: add ech-opts for anytls/shadowsocks/trojan/vmess/vless outbound 2025-05-17 13:53:21 +08:00
wwqgtxx
bb8c47d83d fix: error typo 2025-05-15 18:07:55 +08:00
wwqgtxx
5cf0f18c29 feat: reality add support-x25519mlkem768, it only works with new version server 2025-05-15 14:54:43 +08:00
wwqgtxx
83213d493e chore: adjust min backoff from 1s to 10s 2025-05-14 21:51:18 +08:00
wwqgtxx
90ed01ed53 fix: backoff not reset when the file unchanged 2025-05-14 21:45:12 +08:00
wwqgtxx
f91a586da8 fix: inline proxy provider's healthcheck not work 2025-05-13 19:00:32 +08:00
wwqgtxx
266fb03838 chore: update dependencies 2025-05-13 12:09:38 +08:00
wwqgtxx
76e9607fd7 chore: move start healthcheck.process() from New to Initial in provider
avoid panic cause by build-in proxy have not set to tunnel
2025-05-13 01:12:06 +08:00
wwqgtxx
23e2d3a132 chore: rebuild provider load 2025-05-12 22:19:49 +08:00
wwqgtxx
6e35cf9399 fix: truncated UDP response in system dns
https://github.com/MetaCubeX/mihomo/issues/2031
2025-05-12 12:34:22 +08:00
wwqgtxx
2116640886 chore: the updateConfigs api also adds a check for SAFE_PATHS 2025-05-12 11:28:15 +08:00
wwqgtxx
a4fcd3af07 chore: rollback incompatible changes to updateConfigs api 2025-05-12 10:00:01 +08:00
wwqgtxx
d22a893060 fix: hysteria server port hopping compatibility issues 2025-05-11 11:44:42 +08:00
Anya Lin
00cceba890 docs: update config.yaml follow 7e7016b (#2022) 2025-05-10 13:12:45 +08:00
115 changed files with 2068 additions and 1049 deletions

View File

@@ -7,9 +7,7 @@ import (
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
@@ -316,15 +314,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return
}
}
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: uint16(uintPort),
}
err = addr.SetRemoteAddress(net.JoinHostPort(u.Hostname(), port))
return
}

View File

@@ -4,7 +4,6 @@ import (
"net"
"net/http"
"net/netip"
"strconv"
"strings"
C "github.com/metacubex/mihomo/constant"
@@ -41,23 +40,8 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737)
host = strings.TrimRight(host, ".")
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
metadata := &C.Metadata{
NetWork: C.TCP,
Host: host,
DstIP: netip.Addr{},
DstPort: uint16Port,
}
ip, err := netip.ParseAddr(host)
if err == nil {
metadata.DstIP = ip
}
metadata := &C.Metadata{}
_ = metadata.SetRemoteAddress(net.JoinHostPort(host, port))
return metadata
}

View File

@@ -2,7 +2,6 @@ package outbound
import (
"context"
"errors"
"net"
"strconv"
"time"
@@ -10,7 +9,6 @@ import (
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls"
"github.com/metacubex/mihomo/transport/vmess"
@@ -28,19 +26,20 @@ type AnyTLS struct {
type AnyTLSOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"`
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
MinIdleSession int `proxy:"min-idle-session,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
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"`
UDP bool `proxy:"udp,omitempty"`
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
MinIdleSession int `proxy:"min-idle-session,omitempty"`
}
func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
@@ -52,6 +51,10 @@ func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
}
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
// create tcp
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
if err != nil {
@@ -59,13 +62,6 @@ func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
}
// create uot on tcp
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
}
@@ -115,12 +111,17 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
MinIdleSession: option.MinIdleSession,
}
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,
ClientFingerprint: option.ClientFingerprint,
ECH: echConfig,
}
if tlsConfig.Host == "" {
tlsConfig.Host = option.Server

View File

@@ -3,15 +3,16 @@ package outbound
import (
"context"
"encoding/json"
"fmt"
"net"
"runtime"
"strings"
"sync"
"syscall"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
)
@@ -19,6 +20,7 @@ import (
type ProxyAdapter interface {
C.ProxyAdapter
DialOptions() []dialer.Option
ResolveUDP(ctx context.Context, metadata *C.Metadata) error
}
type Base struct {
@@ -160,6 +162,17 @@ func (b *Base) DialOptions() (opts []dialer.Option) {
return opts
}
func (b *Base) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
func (b *Base) Close() error {
return nil
}
@@ -203,12 +216,21 @@ func NewBase(opt BaseOption) *Base {
type conn struct {
N.ExtendedConn
chain C.Chain
actualRemoteDestination string
chain C.Chain
adapterAddr string
}
func (c *conn) RemoteDestination() string {
return c.actualRemoteDestination
if remoteAddr := c.RemoteAddr(); remoteAddr != nil {
m := C.Metadata{}
if err := m.SetRemoteAddr(remoteAddr); err != nil {
if m.Valid() {
return m.String()
}
}
}
host, _, _ := net.SplitHostPort(c.adapterAddr)
return host
}
// Chains implements C.Connection
@@ -241,19 +263,25 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
}
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
return &conn{N.NewExtendedConn(c), []string{a.Name()}, a.Addr()}
}
type packetConn struct {
N.EnhancePacketConn
chain C.Chain
adapterName string
connID string
actualRemoteDestination string
chain C.Chain
adapterName string
connID string
adapterAddr string
resolveUDP func(ctx context.Context, metadata *C.Metadata) error
}
func (c *packetConn) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
return c.resolveUDP(ctx, metadata)
}
func (c *packetConn) RemoteDestination() string {
return c.actualRemoteDestination
host, _, _ := net.SplitHostPort(c.adapterAddr)
return host
}
// Chains implements C.Connection
@@ -287,24 +315,12 @@ func (c *packetConn) AddRef(ref any) {
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
func newPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc)
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
}
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())}
}
func parseRemoteDestination(addr string) string {
if dst, _, err := net.SplitHostPort(addr); err == nil {
return dst
} else {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
return dst
} else {
return ""
}
}
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
}
type AddRef interface {

View File

@@ -2,7 +2,8 @@ package outbound
import (
"context"
"errors"
"fmt"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/loopback"
"github.com/metacubex/mihomo/component/resolver"
@@ -38,13 +39,8 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
return nil, err
}
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err := d.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
if err != nil {
@@ -53,6 +49,17 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
}
func (d *Direct) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if (!metadata.Resolved() || resolver.DirectHostResolver != resolver.DefaultResolver) && metadata.Host != "" {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
}

View File

@@ -3,6 +3,7 @@ package outbound
import (
"context"
"net"
"net/netip"
"time"
N "github.com/metacubex/mihomo/common/net"
@@ -31,6 +32,9 @@ func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, er
// ListenPacketContext implements C.ProxyAdapter
func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
if err := d.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
@@ -41,6 +45,13 @@ func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.
}, d), nil
}
func (d *Dns) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
metadata.DstIP = netip.AddrFrom4([4]byte{127, 0, 0, 2})
}
return nil
}
type dnsPacket struct {
data []byte
put func()

36
adapter/outbound/ech.go Normal file
View File

@@ -0,0 +1,36 @@
package outbound
import (
"context"
"encoding/base64"
"fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/resolver"
)
type ECHOptions struct {
Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"`
Config string `proxy:"config,omitempty" obfs:"config,omitempty"`
}
func (o ECHOptions) Parse() (*ech.Config, error) {
if !o.Enable {
return nil, nil
}
echConfig := &ech.Config{}
if o.Config != "" {
list, err := base64.StdEncoding.DecodeString(o.Config)
if err != nil {
return nil, fmt.Errorf("base64 decode ech config string failed: %v", err)
}
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
return list, nil
}
} else {
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
return resolver.ResolveECHWithResolver(ctx, serverName, resolver.ProxyServerHostResolver)
}
}
return echConfig, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
@@ -44,6 +45,9 @@ type Hysteria struct {
option *HysteriaOption
client *core.Client
tlsConfig *tlsC.Config
echConfig *ech.Config
}
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
@@ -56,6 +60,9 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if err := h.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
udpConn, err := h.client.DialUDP(h.genHdc(ctx))
if err != nil {
return nil, err
@@ -79,7 +86,15 @@ func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer {
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
},
remoteAddr: func(addr string) (net.Addr, error) {
return resolveUDPAddr(ctx, "udp", addr, h.prefer)
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer)
if err != nil {
return nil, err
}
err = h.echConfig.ClientHandle(ctx, h.tlsConfig)
if err != nil {
return nil, err
}
return udpAddr, nil
},
}
}
@@ -93,30 +108,31 @@ func (h *Hysteria) ProxyInfo() C.ProxyInfo {
type HysteriaOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
Protocol string `proxy:"protocol,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
Up string `proxy:"up"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
Down string `proxy:"down"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
Auth string `proxy:"auth,omitempty"`
AuthString string `proxy:"auth-str,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
Protocol string `proxy:"protocol,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
Up string `proxy:"up"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
Down string `proxy:"down"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
Auth string `proxy:"auth,omitempty"`
AuthString string `proxy:"auth-str,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
}
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
@@ -161,6 +177,13 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
} else {
tlsConfig.NextProtos = []string{DefaultALPN}
}
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
@@ -215,7 +238,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
down = uint64(option.DownSpeed * mbpsToBps)
}
client, err := core.NewClient(
addr, ports, option.Protocol, auth, tlsC.UConfig(tlsConfig), quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
addr, ports, option.Protocol, auth, tlsClientConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator, hopInterval, option.FastOpen,
)
@@ -233,8 +256,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
client: client,
option: &option,
client: client,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
}
return outbound, nil

View File

@@ -20,7 +20,6 @@ import (
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/quic-go"
"github.com/metacubex/randv2"
"github.com/metacubex/sing-quic/hysteria2"
M "github.com/metacubex/sing/common/metadata"
)
@@ -42,24 +41,25 @@ type Hysteria2 struct {
type Hysteria2Option struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
ObfsPassword string `proxy:"obfs-password,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
ObfsPassword string `proxy:"obfs-password,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
@@ -77,6 +77,9 @@ func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.
}
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = h.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := h.client.ListenPacket(ctx)
if err != nil {
return nil, err
@@ -154,6 +157,12 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
tlsConfig.NextProtos = option.ALPN
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.UdpMTU == 0 {
// "1200" from quic-go's MaxDatagramSize
// "-3" from quic-go's DatagramFrame.MaxDataLen
@@ -175,41 +184,46 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
ReceiveBPS: StringToBps(option.Down),
SalamanderPassword: salamanderPassword,
Password: option.Password,
TLSConfig: tlsC.UConfig(tlsConfig),
TLSConfig: tlsClientConfig,
QUICConfig: quicConfig,
UDPDisabled: false,
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
if err != nil {
return nil, err
}
err = echConfig.ClientHandle(ctx, tlsClientConfig)
if err != nil {
return nil, err
}
return udpAddr, nil
},
}
var ranges utils.IntRanges[uint16]
var serverAddress []string
var serverPorts []uint16
if option.Ports != "" {
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
if err != nil {
return nil, err
}
ranges.Range(func(port uint16) bool {
serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port))))
serverPorts = append(serverPorts, port)
return true
})
if len(serverAddress) > 0 {
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
}
if len(serverPorts) > 0 {
if option.HopInterval == 0 {
option.HopInterval = defaultHopInterval
} else if option.HopInterval < minHopInterval {
option.HopInterval = minHopInterval
}
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
clientOptions.ServerPorts = serverPorts
}
}
if option.Port == 0 && len(serverAddress) == 0 {
if option.Port == 0 && len(serverPorts) == 0 {
return nil, errors.New("invalid port")
}

View File

@@ -54,6 +54,9 @@ func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
// ListenPacketContext implements C.ProxyAdapter
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = m.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if err := m.ensureClientIsRunning(); err != nil {
return nil, err
}

View File

@@ -13,11 +13,14 @@ import (
type RealityOptions struct {
PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"`
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
}
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
if o.PublicKey != "" {
config := new(tlsC.RealityConfig)
config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768
const x25519ScalarSize = 32
publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)

View File

@@ -4,6 +4,7 @@ import (
"context"
"io"
"net"
"net/netip"
"time"
"github.com/metacubex/mihomo/common/buf"
@@ -29,9 +30,19 @@ func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if err := r.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
return newPacketConn(&nopPacketConn{}, r), nil
}
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
metadata.DstIP = netip.IPv4Unspecified()
}
return nil
}
func NewRejectWithOption(option RejectOption) *Reject {
return &Reject{
Base: &Base{

View File

@@ -2,7 +2,6 @@ package outbound
import (
"context"
"errors"
"fmt"
"net"
"strconv"
@@ -11,7 +10,6 @@ import (
"github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls"
@@ -64,6 +62,7 @@ type v2rayObfsOption struct {
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@@ -77,6 +76,7 @@ type gostObfsOption struct {
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@@ -200,6 +200,9 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
return nil, err
}
}
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil {
return nil, err
@@ -228,15 +231,9 @@ func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
@@ -303,6 +300,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.Fingerprint = opts.Fingerprint
echConfig, err := opts.ECHOpts.Parse()
if err != nil {
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
}
v2rayOption.ECHConfig = echConfig
}
} else if option.Plugin == "gost-plugin" {
opts := gostObfsOption{Host: "bing.com", Mux: true}
@@ -325,6 +328,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
gostOption.TLS = true
gostOption.SkipCertVerify = opts.SkipCertVerify
gostOption.Fingerprint = opts.Fingerprint
echConfig, err := opts.ECHOpts.Parse()
if err != nil {
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
}
gostOption.ECHConfig = echConfig
}
} else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode

View File

@@ -105,6 +105,9 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return nil, err
}
}
if err = ssr.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
if err != nil {
return nil, err

View File

@@ -2,12 +2,10 @@ package outbound
import (
"context"
"errors"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
@@ -53,16 +51,9 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if s.onlyTcp {
return s.ProxyAdapter.ListenPacketContext(ctx, metadata)
}
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = s.ProxyAdapter.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
if err != nil {
return nil, err

View File

@@ -127,6 +127,9 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return nil, err
}
}
if err = s.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil {
return nil, err

View File

@@ -109,6 +109,9 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err
}
}
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err)

View File

@@ -138,7 +138,7 @@ func NewSsh(option SshOption) (*Ssh, error) {
} else {
path := C.Path.Resolve(option.PrivateKey)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path)
return nil, C.Path.ErrNotSafePath(path)
}
b, err = os.ReadFile(path)
if err != nil {

View File

@@ -12,6 +12,7 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
@@ -32,6 +33,7 @@ type Trojan struct {
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
ssCipher core.Cipher
}
@@ -48,6 +50,7 @@ type TrojanOption struct {
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
@@ -77,6 +80,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint,
ECHConfig: t.echConfig,
Headers: http.Header{},
}
@@ -110,7 +114,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc":
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
default:
// default tcp network
// handle TLS
@@ -124,6 +128,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn,
ECH: t.echConfig,
Reality: t.realityConfig,
})
}
@@ -214,6 +219,10 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
// ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// grpc transport
@@ -245,6 +254,9 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
return nil, err
}
}
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
@@ -266,12 +278,6 @@ func (t *Trojan) SupportWithDialer() C.NetWork {
return C.ALLNet
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err
}
// SupportUOT implements C.ProxyAdapter
func (t *Trojan) SupportUOT() bool {
return true
@@ -321,6 +327,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err
}
t.echConfig, err = option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.SSOpts.Enabled {
if option.SSOpts.Password == "" {
return nil, errors.New("empty password")
@@ -365,7 +376,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig)
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{

View File

@@ -3,7 +3,6 @@ package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"math"
"net"
@@ -12,8 +11,8 @@ import (
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/tuic"
@@ -28,6 +27,9 @@ type Tuic struct {
*Base
option *TuicOption
client *tuic.PoolClient
tlsConfig *tlsC.Config
echConfig *ech.Config
}
type TuicOption struct {
@@ -48,18 +50,19 @@ type TuicOption struct {
DisableSni bool `proxy:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
@@ -86,6 +89,10 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if t.option.UDPOverStream {
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
uotMetadata := *metadata
@@ -97,13 +104,6 @@ func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, meta
}
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
@@ -135,6 +135,10 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
if err != nil {
return nil, nil, err
}
err = t.echConfig.ClientHandle(ctx, t.tlsConfig)
if err != nil {
return nil, nil, err
}
addr = udpAddr
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
@@ -249,6 +253,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.UDPOverStreamVersion {
case uot.Version, uot.LegacyVersion:
case 0:
@@ -268,7 +278,9 @@ func NewTuic(option TuicOption) (*Tuic, error) {
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
option: &option,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
}
clientMaxOpenStreams := int64(option.MaxOpenStreams)
@@ -285,7 +297,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsC.UConfig(tlsConfig),
TlsConfig: tlsClientConfig,
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
@@ -305,7 +317,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsC.UConfig(tlsConfig),
TlsConfig: tlsClientConfig,
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,

View File

@@ -17,8 +17,8 @@ import (
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/gun"
@@ -46,6 +46,7 @@ type Vless struct {
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
}
type VlessOption struct {
@@ -62,6 +63,7 @@ type VlessOption struct {
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@@ -88,6 +90,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{},
}
@@ -151,7 +154,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
default:
// default tcp network
// handle TLS
@@ -206,6 +209,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
@@ -272,13 +276,8 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// gun transport
@@ -310,13 +309,8 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
}
}
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
@@ -342,13 +336,8 @@ func (v *Vless) SupportWithDialer() C.NetWork {
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if v.option.XUDP {
@@ -563,6 +552,11 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err
}
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network {
case "h2":
if len(option.HTTP2Opts.Host) == 0 {
@@ -611,7 +605,7 @@ func NewVless(option VlessOption) (*Vless, error) {
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
}
return v, nil

View File

@@ -15,8 +15,8 @@ import (
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
@@ -41,6 +41,7 @@ type Vmess struct {
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
}
type VmessOption struct {
@@ -58,6 +59,7 @@ type VmessOption struct {
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@@ -109,6 +111,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{},
}
@@ -146,6 +149,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
@@ -195,7 +199,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
default:
// handle TLS
if v.option.TLS {
@@ -205,6 +209,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
@@ -324,13 +329,8 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// gun transport
@@ -361,13 +361,8 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
}
}
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
@@ -407,13 +402,8 @@ func (v *Vmess) Close() error {
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if pc, ok := c.(net.PacketConn); ok {
@@ -474,6 +464,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return nil, err
}
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network {
case "h2":
if len(option.HTTP2Opts.Host) == 0 {
@@ -522,7 +517,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
}
return v, nil

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
@@ -520,16 +519,8 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if err = w.init(ctx); err != nil {
return nil, err
}
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = w.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
if err != nil {
@@ -541,6 +532,21 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
return newPacketConn(pc, w), nil
}
func (w *WireGuard) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
// IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
return true

View File

@@ -32,7 +32,6 @@ type HealthCheck struct {
url string
extra map[string]*extraOption
mu sync.Mutex
started atomic.Bool
proxies []C.Proxy
interval time.Duration
lazy bool
@@ -43,13 +42,8 @@ type HealthCheck struct {
}
func (hc *HealthCheck) process() {
if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(hc.interval)
hc.start()
go hc.check()
for {
select {
case <-ticker.C:
@@ -62,13 +56,12 @@ func (hc *HealthCheck) process() {
}
case <-hc.ctx.Done():
ticker.Stop()
hc.stop()
return
}
}
}
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
func (hc *HealthCheck) setProxies(proxies []C.Proxy) {
hc.proxies = proxies
}
@@ -105,10 +98,6 @@ func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
}
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
@@ -131,14 +120,6 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now())
}
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
}
func (hc *HealthCheck) check() {
if len(hc.proxies) == 0 {
return

View File

@@ -17,7 +17,6 @@ import (
var (
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
type healthCheckSchema struct {
@@ -115,7 +114,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
if schema.Path != "" {
path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
return nil, C.Path.ErrNotSafePath(path)
}
}
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)

View File

@@ -57,6 +57,13 @@ func (bp *baseProvider) Version() uint32 {
return bp.version
}
func (bp *baseProvider) Initial() error {
if bp.healthCheck.auto() {
go bp.healthCheck.process()
}
return nil
}
func (bp *baseProvider) HealthCheck() {
bp.healthCheck.check()
}
@@ -88,7 +95,7 @@ func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
bp.proxies = proxies
bp.version += 1
bp.healthCheck.setProxy(proxies)
bp.healthCheck.setProxies(proxies)
if bp.healthCheck.auto() {
go bp.healthCheck.check()
}
@@ -133,6 +140,9 @@ func (pp *proxySetProvider) Update() error {
}
func (pp *proxySetProvider) Initial() error {
if err := pp.baseProvider.Initial(); err != nil {
return err
}
_, err := pp.Fetcher.Initial()
if err != nil {
return err
@@ -162,10 +172,6 @@ func (pp *proxySetProvider) Close() error {
}
func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
baseProvider: baseProvider{
name: name,
@@ -185,6 +191,8 @@ func NewProxySetProvider(name string, interval time.Duration, payload []map[stri
return nil, err
}
pd.proxies = proxies
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
hc.setProxies(proxies)
}
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
@@ -234,10 +242,6 @@ func (ip *inlineProvider) VehicleType() types.VehicleType {
return types.Inline
}
func (ip *inlineProvider) Initial() error {
return nil
}
func (ip *inlineProvider) Update() error {
// make api update happy
ip.updateAt = time.Now()
@@ -245,10 +249,6 @@ func (ip *inlineProvider) Update() error {
}
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
if hc.auto() {
go hc.process()
}
ps := ProxySchema{Proxies: payload}
buf, err := yaml.Marshal(ps)
if err != nil {
@@ -258,6 +258,8 @@ func NewInlineProvider(name string, payload []map[string]any, parser resource.Pa
if err != nil {
return nil, err
}
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
hc.setProxies(proxies)
ip := &inlineProvider{
baseProvider: baseProvider{
@@ -301,13 +303,6 @@ func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
cp.HealthCheck()
}
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible
}
@@ -317,10 +312,6 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
return nil, errors.New("provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
baseProvider: baseProvider{
name: name,

View File

@@ -20,7 +20,7 @@ type connReadResult struct {
type Conn struct {
network.ExtendedConn
deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline
pipeDeadline PipeDeadline
disablePipe atomic.Bool
inRead atomic.Bool
resultCh chan *connReadResult
@@ -34,7 +34,7 @@ func IsConn(conn any) bool {
func NewConn(conn net.Conn) *Conn {
c := &Conn{
ExtendedConn: bufio.NewExtendedConn(conn),
pipeDeadline: makePipeDeadline(),
pipeDeadline: MakePipeDeadline(),
resultCh: make(chan *connReadResult, 1),
}
c.resultCh <- nil
@@ -58,7 +58,7 @@ func (c *Conn) Read(p []byte) (n int, err error) {
c.resultCh <- nil
break
}
case <-c.pipeDeadline.wait():
case <-c.pipeDeadline.Wait():
return 0, os.ErrDeadlineExceeded
}
@@ -104,7 +104,7 @@ func (c *Conn) ReadBuffer(buffer *buf.Buffer) (err error) {
c.resultCh <- nil
break
}
case <-c.pipeDeadline.wait():
case <-c.pipeDeadline.Wait():
return os.ErrDeadlineExceeded
}
@@ -130,7 +130,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
return c.ExtendedConn.SetReadDeadline(t)
}
c.deadline.Store(t)
c.pipeDeadline.set(t)
c.pipeDeadline.Set(t)
return nil
}

View File

@@ -19,7 +19,7 @@ type readResult struct {
type NetPacketConn struct {
net.PacketConn
deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline
pipeDeadline PipeDeadline
disablePipe atomic.Bool
inRead atomic.Bool
resultCh chan any
@@ -28,7 +28,7 @@ type NetPacketConn struct {
func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
npc := &NetPacketConn{
PacketConn: pc,
pipeDeadline: makePipeDeadline(),
pipeDeadline: MakePipeDeadline(),
resultCh: make(chan any, 1),
}
npc.resultCh <- nil
@@ -83,7 +83,7 @@ FOR:
c.resultCh <- nil
break FOR
}
case <-c.pipeDeadline.wait():
case <-c.pipeDeadline.Wait():
return 0, nil, os.ErrDeadlineExceeded
}
}
@@ -122,7 +122,7 @@ func (c *NetPacketConn) SetReadDeadline(t time.Time) error {
return c.PacketConn.SetReadDeadline(t)
}
c.deadline.Store(t)
c.pipeDeadline.set(t)
c.pipeDeadline.Set(t)
return nil
}

View File

@@ -52,7 +52,7 @@ FOR:
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
case <-c.netPacketConn.pipeDeadline.Wait():
return nil, nil, nil, os.ErrDeadlineExceeded
}
}

View File

@@ -69,7 +69,7 @@ FOR:
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
case <-c.netPacketConn.pipeDeadline.Wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded
}
}
@@ -146,7 +146,7 @@ FOR:
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
case <-c.netPacketConn.pipeDeadline.Wait():
return nil, M.Socksaddr{}, os.ErrDeadlineExceeded
}
}

View File

@@ -9,24 +9,24 @@ import (
"time"
)
// pipeDeadline is an abstraction for handling timeouts.
type pipeDeadline struct {
// PipeDeadline is an abstraction for handling timeouts.
type PipeDeadline struct {
mu sync.Mutex // Guards timer and cancel
timer *time.Timer
cancel chan struct{} // Must be non-nil
}
func makePipeDeadline() pipeDeadline {
return pipeDeadline{cancel: make(chan struct{})}
func MakePipeDeadline() PipeDeadline {
return PipeDeadline{cancel: make(chan struct{})}
}
// set sets the point in time when the deadline will time out.
// Set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by waiter.
// Once a timeout has occurred, the deadline can be refreshed by specifying a
// t value in the future.
//
// A zero value for t prevents timeout.
func (d *pipeDeadline) set(t time.Time) {
func (d *PipeDeadline) Set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
@@ -61,8 +61,8 @@ func (d *pipeDeadline) set(t time.Time) {
}
}
// wait returns a channel that is closed when the deadline is exceeded.
func (d *pipeDeadline) wait() chan struct{} {
// Wait returns a channel that is closed when the deadline is exceeded.
func (d *PipeDeadline) Wait() chan struct{} {
d.mu.Lock()
defer d.mu.Unlock()
return d.cancel

View File

@@ -33,8 +33,8 @@ type pipe struct {
localDone chan struct{}
remoteDone <-chan struct{}
readDeadline pipeDeadline
writeDeadline pipeDeadline
readDeadline PipeDeadline
writeDeadline PipeDeadline
readWaitOptions N.ReadWaitOptions
}
@@ -56,15 +56,15 @@ func Pipe() (net.Conn, net.Conn) {
rdRx: cb1, rdTx: cn1,
wrTx: cb2, wrRx: cn2,
localDone: done1, remoteDone: done2,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
readDeadline: MakePipeDeadline(),
writeDeadline: MakePipeDeadline(),
}
p2 := &pipe{
rdRx: cb2, rdTx: cn2,
wrTx: cb1, wrRx: cn1,
localDone: done2, remoteDone: done1,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
readDeadline: MakePipeDeadline(),
writeDeadline: MakePipeDeadline(),
}
return p1, p2
}
@@ -86,7 +86,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe
case isClosedChan(p.remoteDone):
return 0, io.EOF
case isClosedChan(p.readDeadline.wait()):
case isClosedChan(p.readDeadline.Wait()):
return 0, os.ErrDeadlineExceeded
}
@@ -99,7 +99,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe
case <-p.remoteDone:
return 0, io.EOF
case <-p.readDeadline.wait():
case <-p.readDeadline.Wait():
return 0, os.ErrDeadlineExceeded
}
}
@@ -118,7 +118,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe
case isClosedChan(p.remoteDone):
return 0, io.ErrClosedPipe
case isClosedChan(p.writeDeadline.wait()):
case isClosedChan(p.writeDeadline.Wait()):
return 0, os.ErrDeadlineExceeded
}
@@ -134,7 +134,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
return n, io.ErrClosedPipe
case <-p.remoteDone:
return n, io.ErrClosedPipe
case <-p.writeDeadline.wait():
case <-p.writeDeadline.Wait():
return n, os.ErrDeadlineExceeded
}
}
@@ -145,8 +145,8 @@ func (p *pipe) SetDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe
}
p.readDeadline.set(t)
p.writeDeadline.set(t)
p.readDeadline.Set(t)
p.writeDeadline.Set(t)
return nil
}
@@ -154,7 +154,7 @@ func (p *pipe) SetReadDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe
}
p.readDeadline.set(t)
p.readDeadline.Set(t)
return nil
}
@@ -162,7 +162,7 @@ func (p *pipe) SetWriteDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe
}
p.writeDeadline.set(t)
p.writeDeadline.Set(t)
return nil
}
@@ -192,7 +192,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
return nil, io.ErrClosedPipe
case isClosedChan(p.remoteDone):
return nil, io.EOF
case isClosedChan(p.readDeadline.wait()):
case isClosedChan(p.readDeadline.Wait()):
return nil, os.ErrDeadlineExceeded
}
select {
@@ -211,7 +211,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
return nil, io.ErrClosedPipe
case <-p.remoteDone:
return nil, io.EOF
case <-p.readDeadline.wait():
case <-p.readDeadline.Wait():
return nil, os.ErrDeadlineExceeded
}
}

View File

@@ -34,8 +34,8 @@ func (l *handleContextListener) init() {
}
}
}()
if c, err := l.handle(l.ctx, c); err == nil {
l.conns <- c
if conn, err := l.handle(l.ctx, c); err == nil {
l.conns <- conn
} else {
// handle failed, close the underlying connection.
_ = c.Close()

View File

@@ -83,7 +83,7 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error)
if len(customCA) > 0 {
path := C.Path.Resolve(customCA)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path)
return nil, C.Path.ErrNotSafePath(path)
}
certificate, err = os.ReadFile(path)
if err != nil {

View File

@@ -17,6 +17,7 @@ import (
type Path interface {
Resolve(path string) string
IsSafePath(path string) bool
ErrNotSafePath(path string) error
}
// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution.
@@ -42,10 +43,12 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate,
certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey)
var loadErr error
if path.IsSafePath(certificate) && path.IsSafePath(privateKey) {
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
if !path.IsSafePath(certificate) {
loadErr = path.ErrNotSafePath(certificate)
} else if !path.IsSafePath(privateKey) {
loadErr = path.ErrNotSafePath(privateKey)
} else {
loadErr = fmt.Errorf("path is not subpath of home directory")
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
}
if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())

View File

@@ -73,7 +73,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
}
if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil {
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr().Unmap())
}
}
if rAddrPort.Addr().Unmap().IsLoopback() {

31
component/ech/ech.go Normal file
View File

@@ -0,0 +1,31 @@
package ech
import (
"context"
"fmt"
tlsC "github.com/metacubex/mihomo/component/tls"
)
type Config struct {
GetEncryptedClientHelloConfigList func(ctx context.Context, serverName string) ([]byte, error)
}
func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tlsC.Config) (err error) {
if cfg == nil {
return nil
}
echConfigList, err := cfg.GetEncryptedClientHelloConfigList(ctx, tlsConfig.ServerName)
if err != nil {
return fmt.Errorf("resolve ECH config error: %w", err)
}
tlsConfig.EncryptedClientHelloConfigList = echConfigList
if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tlsC.VersionTLS13 {
tlsConfig.MinVersion = tlsC.VersionTLS13
}
if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tlsC.VersionTLS13 {
tlsConfig.MaxVersion = tlsC.VersionTLS13
}
return nil
}

143
component/ech/key.go Normal file
View File

@@ -0,0 +1,143 @@
package ech
import (
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"github.com/metacubex/mihomo/component/ca"
tlsC "github.com/metacubex/mihomo/component/tls"
"golang.org/x/crypto/cryptobyte"
)
const (
AEAD_AES_128_GCM = 0x0001
AEAD_AES_256_GCM = 0x0002
AEAD_ChaCha20Poly1305 = 0x0003
)
const extensionEncryptedClientHello = 0xfe0d
const DHKEM_X25519_HKDF_SHA256 = 0x0020
const KDF_HKDF_SHA256 = 0x0001
// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS.
// We need this so that when we insert them into ECHConfigs the ordering
// is stable.
var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305}
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(pubKey)
})
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
for _, aeadID := range sortedSupportedAEADs {
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
builder.AddUint16(aeadID)
}
})
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes([]byte(publicName))
})
builder.AddUint16(0) // extensions
})
return builder.BytesOrPanic()
}
func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
echConfigList := builder.BytesOrPanic()
builder2 := cryptobyte.NewBuilder(nil)
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echKey.Bytes())
})
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
echConfigKeys := builder2.BytesOrPanic()
configBase64 = base64.StdEncoding.EncodeToString(echConfigList)
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys}))
return
}
func UnmarshalECHKeys(raw []byte) ([]tlsC.EncryptedClientHelloKey, error) {
var keys []tlsC.EncryptedClientHelloKey
rawString := cryptobyte.String(raw)
for !rawString.Empty() {
var key tlsC.EncryptedClientHelloKey
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, errors.New("error parsing private key")
}
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, errors.New("error parsing config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, errors.New("empty ECH keys")
}
return keys, nil
}
func LoadECHKey(key string, tlsConfig *tlsC.Config, path ca.Path) error {
if key == "" {
return nil
}
painTextErr := loadECHKey([]byte(key), tlsConfig)
if painTextErr == nil {
return nil
}
key = path.Resolve(key)
var loadErr error
if !path.IsSafePath(key) {
loadErr = path.ErrNotSafePath(key)
} else {
var echKey []byte
echKey, loadErr = os.ReadFile(key)
if loadErr == nil {
loadErr = loadECHKey(echKey, tlsConfig)
}
}
if loadErr != nil {
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
return nil
}
func loadECHKey(echKey []byte, tlsConfig *tlsC.Config) error {
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return errors.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return fmt.Errorf("parse ECH keys: %w", err)
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
}

View File

@@ -4,12 +4,14 @@ import (
"encoding/base64"
"fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/gofrs/uuid/v5"
)
func Main(args []string) {
if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair")
panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair")
}
switch args[0] {
case "uuid":
@@ -33,5 +35,15 @@ func Main(args []string) {
}
fmt.Println("PrivateKey: " + privateKey.String())
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
case "ech-keypair":
if len(args) < 2 {
panic("Using: generate ech-keypair <plain_server_name>")
}
configBase64, keyPem, err := ech.GenECHConfig(args[1])
if err != nil {
panic(err)
}
fmt.Println("Config:", configBase64)
fmt.Println("Key:", keyPem)
}
}

View File

@@ -26,8 +26,9 @@ var (
)
type ifaceCache struct {
ifMap map[string]*Interface
ifTable bart.Table[*Interface]
ifMapByName map[string]*Interface
ifMapByAddr map[netip.Addr]*Interface
ifTable bart.Table[*Interface]
}
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
@@ -40,7 +41,8 @@ func getCache() (*ifaceCache, error) {
}
cache := &ifaceCache{
ifMap: make(map[string]*Interface),
ifMapByName: make(map[string]*Interface),
ifMapByAddr: make(map[netip.Addr]*Interface),
}
for _, iface := range ifaces {
@@ -78,12 +80,13 @@ func getCache() (*ifaceCache, error) {
Flags: iface.Flags,
Addresses: ipNets,
}
cache.ifMap[iface.Name] = ifaceObj
cache.ifMapByName[iface.Name] = ifaceObj
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
for _, prefix := range ipNets {
cache.ifMapByAddr[prefix.Addr()] = ifaceObj
cache.ifTable.Insert(prefix, ifaceObj)
}
}
@@ -98,7 +101,7 @@ func Interfaces() (map[string]*Interface, error) {
if err != nil {
return nil, err
}
return cache.ifMap, nil
return cache.ifMapByName, nil
}
func ResolveInterface(name string) (*Interface, error) {
@@ -120,6 +123,11 @@ func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
if err != nil {
return nil, err
}
// maybe two interfaces have the same prefix but different address
// so direct check address equal before do a route lookup (longest prefix match)
if iface, ok := cache.ifMapByAddr[addr]; ok {
return iface, nil
}
iface, ok := cache.ifTable.Lookup(addr)
if !ok {
return nil, ErrIfaceNotFound
@@ -133,7 +141,8 @@ func IsLocalIp(addr netip.Addr) (bool, error) {
if err != nil {
return false, err
}
return cache.ifTable.Contains(addr), nil
_, ok := cache.ifMapByAddr[addr]
return ok, nil
}
func FlushCache() {

View File

@@ -2,7 +2,6 @@ package proxydialer
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
@@ -10,7 +9,6 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
@@ -40,17 +38,16 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
return nil, err
}
if strings.Contains(network, "udp") { // using in wireguard outbound
if !currentMeta.Resolved() {
ip, err := resolver.ResolveIP(ctx, currentMeta.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
currentMeta.DstIP = ip
}
pc, err := p.listenPacket(ctx, currentMeta)
if err != nil {
return nil, err
}
if !currentMeta.Resolved() { // should not happen, maybe by a wrongly implemented proxy, but we can handle this (:
err = pc.ResolveUDP(ctx, currentMeta)
if err != nil {
return nil, err
}
}
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
}
var conn C.Conn

View File

@@ -77,7 +77,7 @@ func NewHostValue(value any) (HostValue, error) {
isDomain = false
for _, str := range valueArr {
if ip, err := netip.ParseAddr(str); err == nil {
ips = append(ips, ip)
ips = append(ips, ip.Unmap())
} else {
return HostValue{}, err
}
@@ -85,7 +85,7 @@ func NewHostValue(value any) (HostValue, error) {
} else if len(valueArr) == 1 {
host := valueArr[0]
if ip, err := netip.ParseAddr(host); err == nil {
ips = append(ips, ip)
ips = append(ips, ip.Unmap())
isDomain = false
} else {
domain = host

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/netip"
"strings"
"time"
"github.com/metacubex/mihomo/common/utils"
@@ -49,6 +48,7 @@ type Resolver interface {
LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error)
ResolveECH(ctx context.Context, host string) ([]byte, error)
ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
Invalid() bool
ClearCache()
@@ -67,7 +67,8 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() || ip.Is4In6() {
ip = ip.Unmap()
if ip.Is4() {
return []netip.Addr{ip}, nil
}
return []netip.Addr{}, ErrIPVersion
@@ -116,7 +117,8 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net
}
if ip, err := netip.ParseAddr(host); err == nil {
if strings.Contains(host, ":") {
ip = ip.Unmap()
if ip.Is6() {
return []netip.Addr{ip}, nil
}
return nil, ErrIPVersion
@@ -165,6 +167,7 @@ func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip
}
if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
return []netip.Addr{ip}, nil
}
@@ -216,6 +219,17 @@ func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
}
func ResolveECHWithResolver(ctx context.Context, host string, r Resolver) ([]byte, error) {
if r != nil && r.Invalid() {
return r.ResolveECH(ctx, host)
}
return SystemResolver.ResolveECH(ctx, host)
}
func ResolveECH(ctx context.Context, host string) ([]byte, error) {
return ResolveECHWithResolver(ctx, host, DefaultResolver)
}
func ResetConnection() {
if DefaultResolver != nil {
go DefaultResolver.ResetConnection()

View File

@@ -105,6 +105,7 @@ func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (
_ = os.Chtimes(f.vehicle.Path(), now, now)
}
f.updatedAt = now
f.backoff.Reset() // no error, reset backoff
return lo.Empty[V](), true, nil
}
@@ -220,6 +221,10 @@ func (f *Fetcher[V]) updateWithLog() {
func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] {
ctx, cancel := context.WithCancel(context.Background())
minBackoff := 10 * time.Second
if interval < minBackoff {
minBackoff = interval
}
return &Fetcher[V]{
ctx: ctx,
ctxCancel: cancel,
@@ -231,7 +236,7 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
backoff: slowdown.Backoff{
Factor: 2,
Jitter: false,
Min: time.Second,
Min: minBackoff,
Max: interval,
},
}

View File

@@ -72,8 +72,16 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
overrideDest := config.OverrideDest
if inWhitelist {
replaceDomain := func(metadata *C.Metadata, host string) {
if sd.domainCanReplace(host) {
replaceDomain(metadata, host, overrideDest)
} else {
log.Debugln("[Sniffer] Skip sni[%s]", host)
}
}
if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
return wrapable.WrapperSender(packetSender, overrideDest)
return wrapable.WrapperSender(packetSender, replaceDomain)
}
host, err := current.SniffData(packet.Data())
@@ -81,7 +89,7 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
continue
}
replaceDomain(metadata, host, overrideDest)
replaceDomain(metadata, host)
return packetSender
}
}
@@ -128,11 +136,9 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
return false
}
for _, matcher := range sd.skipDomain {
if matcher.MatchDomain(host) {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return false
}
if !sd.domainCanReplace(host) {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return false
}
sd.skipList.Delete(dst)
@@ -152,10 +158,20 @@ func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
metadata.RemoteAddress(),
metadata.Host, host)
metadata.Host = host
metadata.DstIP = netip.Addr{}
}
metadata.DNSMode = C.DNSNormal
}
func (sd *Dispatcher) domainCanReplace(host string) bool {
for _, matcher := range sd.skipDomain {
if matcher.MatchDomain(host) {
return false
}
}
return true
}
func (sd *Dispatcher) Enable() bool {
return sd != nil && sd.enable
}

View File

@@ -74,24 +74,25 @@ func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) {
return "", ErrorUnsupportedSniffer
}
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender {
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, replaceDomain sniffer.ReplaceDomain) constant.PacketSender {
return &quicPacketSender{
sender: packetSender,
chClose: make(chan struct{}),
override: override,
PacketSender: packetSender,
replaceDomain: replaceDomain,
chClose: make(chan struct{}),
}
}
var _ constant.PacketSender = (*quicPacketSender)(nil)
type quicPacketSender struct {
lock sync.RWMutex
ranges utils.IntRanges[uint64]
buffer []byte
result string
override bool
lock sync.RWMutex
ranges utils.IntRanges[uint64]
buffer []byte
result *string
sender constant.PacketSender
replaceDomain sniffer.ReplaceDomain
constant.PacketSender
chClose chan struct{}
closed bool
@@ -100,7 +101,7 @@ type quicPacketSender struct {
// Send will send PacketAdapter nonblocking
// the implement must call UDPPacket.Drop() inside Send
func (q *quicPacketSender) Send(current constant.PacketAdapter) {
defer q.sender.Send(current)
defer q.PacketSender.Send(current)
q.lock.RLock()
if q.closed {
@@ -116,29 +117,27 @@ func (q *quicPacketSender) Send(current constant.PacketAdapter) {
}
}
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
func (q *quicPacketSender) Process(conn constant.PacketConn, proxy constant.WriteBackProxy) {
q.sender.Process(conn, proxy)
}
// ResolveUDP wait sniffer recv all fragments and update the domain
func (q *quicPacketSender) ResolveUDP(data *constant.Metadata) error {
// DoSniff wait sniffer recv all fragments and update the domain
func (q *quicPacketSender) DoSniff(metadata *constant.Metadata) error {
select {
case <-q.chClose:
q.lock.RLock()
replaceDomain(data, q.result, q.override)
if q.result != nil {
host := *q.result
q.replaceDomain(metadata, host)
}
q.lock.RUnlock()
break
case <-time.After(quicWaitConn):
q.close()
}
return q.sender.ResolveUDP(data)
return q.PacketSender.DoSniff(metadata)
}
// Close stop the Process loop
func (q *quicPacketSender) Close() {
q.sender.Close()
q.PacketSender.Close()
q.close()
}
@@ -433,7 +432,7 @@ func (q *quicPacketSender) tryAssemble() error {
}
q.lock.Lock()
q.result = *domain
q.result = domain
q.closeLocked()
q.lock.Unlock()

View File

@@ -12,7 +12,7 @@ import (
)
type fakeSender struct {
resultCh chan *constant.Metadata
constant.PacketSender
}
var _ constant.PacketSender = (*fakeSender)(nil)
@@ -22,18 +22,7 @@ func (e *fakeSender) Send(packet constant.PacketAdapter) {
packet.Drop()
}
func (e *fakeSender) Process(constant.PacketConn, constant.WriteBackProxy) {
panic("not implemented")
}
func (e *fakeSender) ResolveUDP(metadata *constant.Metadata) error {
e.resultCh <- metadata
return nil
}
func (e *fakeSender) Close() {
panic("not implemented")
}
func (e *fakeSender) DoSniff(metadata *constant.Metadata) error { return nil }
type fakeUDPPacket struct {
data []byte
@@ -78,23 +67,28 @@ func asPacket(data string) constant.PacketAdapter {
return pktAdp
}
func testQuicSniffer(data []string, async bool) (string, error) {
const fakeHost = "fake.host.com"
func testQuicSniffer(data []string, async bool) (string, string, error) {
q, err := NewQuicSniffer(SnifferConfig{})
if err != nil {
return "", err
return "", "", err
}
resultCh := make(chan *constant.Metadata, 1)
emptySender := &fakeSender{resultCh: resultCh}
emptySender := &fakeSender{}
sender := q.WrapperSender(emptySender, true)
sender := q.WrapperSender(emptySender, func(metadata *constant.Metadata, host string) {
replaceDomain(metadata, host, true)
})
go func() {
meta := constant.Metadata{}
err = sender.ResolveUDP(&meta)
meta := constant.Metadata{Host: fakeHost}
err := sender.DoSniff(&meta)
if err != nil {
panic(err)
}
resultCh <- &meta
}()
for _, d := range data {
@@ -106,14 +100,15 @@ func testQuicSniffer(data []string, async bool) (string, error) {
}
meta := <-resultCh
return meta.SniffHost, nil
return meta.SniffHost, meta.Host, nil
}
func TestQuicHeaders(t *testing.T) {
cases := []struct {
input []string
domain string
input []string
domain string
invalid bool
}{
//Normal domain quic sniff
{
@@ -171,16 +166,31 @@ func TestQuicHeaders(t *testing.T) {
},
domain: "www.google.com",
},
// invalid packet
{
input: []string{"00000000000000000000"},
invalid: true,
},
}
for _, test := range cases {
data, err := testQuicSniffer(test.input, true)
data, host, err := testQuicSniffer(test.input, true)
assert.NoError(t, err)
assert.Equal(t, test.domain, data)
if test.invalid {
assert.Equal(t, fakeHost, host)
} else {
assert.Equal(t, test.domain, host)
}
data, err = testQuicSniffer(test.input, false)
data, host, err = testQuicSniffer(test.input, false)
assert.NoError(t, err)
assert.Equal(t, test.domain, data)
if test.invalid {
assert.Equal(t, fakeHost, host)
} else {
assert.Equal(t, test.domain, host)
}
}
}

View File

@@ -0,0 +1,70 @@
package tls
import (
"context"
"net"
"net/http"
"runtime/debug"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/log"
"golang.org/x/net/http2"
)
func extractTlsHandshakeTimeoutFromServer(s *http.Server) time.Duration {
var ret time.Duration
for _, v := range [...]time.Duration{
s.ReadHeaderTimeout,
s.ReadTimeout,
s.WriteTimeout,
} {
if v <= 0 {
continue
}
if ret == 0 || v < ret {
ret = v
}
}
return ret
}
// NewListenerForHttps returns a net.Listener for (*http.Server).Serve()
// the "func (c *conn) serve(ctx context.Context)" in http\server.go
// only do tls handshake and check NegotiatedProtocol with std's *tls.Conn
// so we do the same logic to let http2 (not h2c) work fine
func NewListenerForHttps(l net.Listener, httpServer *http.Server, tlsConfig *Config) net.Listener {
http2Server := &http2.Server{}
_ = http2.ConfigureServer(httpServer, http2Server)
return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) {
c := Server(conn, tlsConfig)
tlsTO := extractTlsHandshakeTimeoutFromServer(httpServer)
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
_ = conn.SetReadDeadline(dl)
_ = conn.SetWriteDeadline(dl)
}
err := c.HandshakeContext(ctx)
if err != nil {
return nil, err
}
// Restore Conn-level deadlines.
if tlsTO > 0 {
_ = conn.SetReadDeadline(time.Time{})
_ = conn.SetWriteDeadline(time.Time{})
}
if c.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
http2Server.ServeConn(c, &http2.ServeConnOpts{BaseConfig: httpServer})
return nil, net.ErrClosed
}
return c, nil
}, func(a any) {
stack := debug.Stack()
log.Errorln("https server panic: %s\n%s", a, stack)
})
}

View File

@@ -26,7 +26,6 @@ import (
utls "github.com/metacubex/utls"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
"golang.org/x/exp/slices"
"golang.org/x/net/http2"
)
@@ -35,9 +34,11 @@ const RealityMaxShortIDLen = 8
type RealityConfig struct {
PublicKey *ecdh.PublicKey
ShortID [RealityMaxShortIDLen]byte
SupportX25519MLKEM768 bool
}
func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *Config, realityConfig *RealityConfig) (net.Conn, error) {
for retry := 0; ; retry++ {
verifier := &realityVerifier{
serverName: tlsConfig.ServerName,
@@ -48,39 +49,18 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
SessionTicketsDisabled: true,
VerifyPeerCertificate: verifier.VerifyPeerCertificate,
}
clientID := utls.ClientHelloID{
Client: fingerprint.Client,
Version: fingerprint.Version,
Seed: fingerprint.Seed,
if !realityConfig.SupportX25519MLKEM768 && fingerprint == HelloChrome_Auto {
fingerprint = HelloChrome_120 // old reality server doesn't work with X25519MLKEM768
}
uConn := utls.UClient(conn, uConfig, clientID)
uConn := utls.UClient(conn, uConfig, fingerprint)
verifier.UConn = uConn
err := uConn.BuildHandshakeState()
if err != nil {
return nil, err
}
// ------for X25519MLKEM768 does not work properly with reality-------
// Iterate over extensions and check
for _, extension := range uConn.Extensions {
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool {
return curveID == utls.X25519MLKEM768
})
}
if ks, ok := extension.(*utls.KeyShareExtension); ok {
ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool {
return share.Group == utls.X25519MLKEM768
})
}
}
// Rebuild the client hello
err = uConn.BuildHandshakeState()
if err != nil {
return nil, err
}
// --------------------------------------------------------------------
hello := uConn.HandshakeState.Hello
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
for i := range rawSessionID { // https://github.com/golang/go/issues/5373
@@ -144,7 +124,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher)
if !verifier.verified {
go realityClientFallback(uConn, uConfig.ServerName, clientID)
go realityClientFallback(uConn, uConfig.ServerName, fingerprint)
return nil, errors.New("REALITY authentication failed")
}

View File

@@ -16,6 +16,7 @@ type Conn = utls.Conn
type UConn = utls.UConn
type UClientHelloID = utls.ClientHelloID
const VersionTLS12 = utls.VersionTLS12
const VersionTLS13 = utls.VersionTLS13
func Client(c net.Conn, config *utls.Config) *Conn {
@@ -26,6 +27,14 @@ func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn
return utls.UClient(c, config, fingerprint)
}
func Server(c net.Conn, config *utls.Config) *Conn {
return utls.Server(c, config)
}
func NewListener(inner net.Listener, config *Config) net.Listener {
return utls.NewListener(inner, config)
}
func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) {
if len(clientFingerprint) == 0 {
clientFingerprint = globalFingerprint
@@ -65,21 +74,26 @@ var randomFingerprint = once.OnceValue(func() UClientHelloID {
return fingerprint
})
var HelloChrome_Auto = utls.HelloChrome_Auto
var HelloChrome_120 = utls.HelloChrome_120 // special fingerprint for some old protocols doesn't work with HelloChrome_Auto
var fingerprints = map[string]UClientHelloID{
"chrome": utls.HelloChrome_Auto,
"chrome": utls.HelloChrome_Auto,
"firefox": utls.HelloFirefox_Auto,
"safari": utls.HelloSafari_Auto,
"ios": utls.HelloIOS_Auto,
"android": utls.HelloAndroid_11_OkHttp,
"edge": utls.HelloEdge_Auto,
"360": utls.Hello360_Auto,
"qq": utls.HelloQQ_Auto,
"random": {},
// deprecated fingerprints should not be used
"chrome_psk": utls.HelloChrome_100_PSK,
"chrome_psk_shuffle": utls.HelloChrome_106_Shuffle,
"chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf,
"chrome_pq": utls.HelloChrome_115_PQ,
"chrome_pq_psk": utls.HelloChrome_115_PQ_PSK,
"firefox": utls.HelloFirefox_Auto,
"safari": utls.HelloSafari_Auto,
"ios": utls.HelloIOS_Auto,
"android": utls.HelloAndroid_11_OkHttp,
"edge": utls.HelloEdge_Auto,
"360": utls.Hello360_Auto,
"qq": utls.HelloQQ_Auto,
"random": {},
"randomized": utls.HelloRandomized,
}
@@ -93,7 +107,9 @@ func init() {
fingerprints["randomized"] = randomized
}
func UCertificates(it tls.Certificate) utls.Certificate {
type Certificate = utls.Certificate
func UCertificate(it tls.Certificate) utls.Certificate {
return utls.Certificate{
Certificate: it.Certificate,
PrivateKey: it.PrivateKey,
@@ -106,13 +122,15 @@ func UCertificates(it tls.Certificate) utls.Certificate {
}
}
type EncryptedClientHelloKey = utls.EncryptedClientHelloKey
type Config = utls.Config
func UConfig(config *tls.Config) *utls.Config {
return &utls.Config{
Rand: config.Rand,
Time: config.Time,
Certificates: utils.Map(config.Certificates, UCertificates),
Certificates: utils.Map(config.Certificates, UCertificate),
VerifyPeerCertificate: config.VerifyPeerCertificate,
RootCAs: config.RootCAs,
NextProtos: config.NextProtos,

View File

@@ -3,6 +3,7 @@ package updater
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"io"
@@ -32,6 +33,17 @@ const (
typeTarGzip
)
func (t compressionType) String() string {
switch t {
case typeZip:
return "zip"
case typeTarGzip:
return "tar.gz"
default:
return "unknown"
}
}
var DefaultUiUpdater = &UIUpdater{}
func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater {
@@ -99,48 +111,38 @@ func detectFileType(data []byte) compressionType {
}
func (u *UIUpdater) downloadUI() error {
err := u.prepareUIPath()
if err != nil {
return fmt.Errorf("prepare UI path failed: %w", err)
}
data, err := downloadForBytes(u.externalUIURL)
if err != nil {
return fmt.Errorf("can't download file: %w", err)
}
fileType := detectFileType(data)
if fileType == typeUnknown {
return fmt.Errorf("unknown or unsupported file type")
tmpDir := C.Path.Resolve("downloadUI.tmp")
defer os.RemoveAll(tmpDir)
os.RemoveAll(tmpDir) // cleanup tmp dir before extract
log.Debugln("extractedFolder: %s", tmpDir)
err = extract(data, tmpDir)
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
}
ext := ".zip"
if fileType == typeTarGzip {
ext = ".tgz"
}
saved := path.Join(C.Path.HomeDir(), "download"+ext)
log.Debugln("compression Type: %s", ext)
if err = saveFile(data, saved); err != nil {
return fmt.Errorf("can't save compressed file: %w", err)
}
defer os.Remove(saved)
err = cleanup(u.externalUIPath)
log.Debugln("cleanupFolder: %s", u.externalUIPath)
err = cleanup(u.externalUIPath) // cleanup files in dir don't remove dir itself
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("cleanup exist file error: %w", err)
}
}
extractedFolder, err := extract(saved, C.Path.HomeDir())
err = u.prepareUIPath()
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
return fmt.Errorf("prepare UI path failed: %w", err)
}
err = os.Rename(extractedFolder, u.externalUIPath)
log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath)
err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target
if err != nil {
return fmt.Errorf("rename UI folder failed: %w", err)
return fmt.Errorf("move UI folder failed: %w", err)
}
return nil
}
@@ -155,228 +157,109 @@ func (u *UIUpdater) prepareUIPath() error {
return nil
}
func unzip(src, dest string) (string, error) {
r, err := zip.OpenReader(src)
func unzip(data []byte, dest string) error {
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", err
return err
}
defer r.Close()
// check whether or not only exists singleRoot dir
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for _, f := range r.File {
parts := strings.Split(strings.Trim(f.Name, "/"), "/")
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := strings.HasSuffix(f.Name, "/")
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
// build the dir of extraction
var extractedFolder string
if isSingleRoot && rootDir != "" {
// if the singleRoot, use it directly
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
// or put the files/dirs into new dir
baseName := filepath.Base(src)
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
extractedFolder = filepath.Join(dest, baseName)
for i := 1; ; i++ {
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
break
}
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
}
log.Debugln("extractedFolder: %s", extractedFolder)
}
for _, f := range r.File {
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, f.Name)
} else {
fpath = filepath.Join(extractedFolder, f.Name)
}
fpath := filepath.Join(dest, f.Name)
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid file path: %s", fpath)
if !inDest(fpath, dest) {
return fmt.Errorf("invalid file path: %s", fpath)
}
if f.FileInfo().IsDir() {
info := f.FileInfo()
if info.IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if info.Mode()&os.ModeSymlink != 0 {
continue // disallow symlink
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return "", err
return err
}
rc, err := f.Open()
if err != nil {
return "", err
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return "", err
return err
}
}
return extractedFolder, nil
return nil
}
func untgz(src, dest string) (string, error) {
file, err := os.Open(src)
func untgz(data []byte, dest string) error {
gzr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return "", err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return "", err
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := header.Typeflag == tar.TypeDir
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
file.Seek(0, 0)
gzr, _ = gzip.NewReader(file)
_ = gzr.Reset(bytes.NewReader(data))
tr = tar.NewReader(gzr)
var extractedFolder string
if isSingleRoot && rootDir != "" {
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
baseName := filepath.Base(src)
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
baseName = strings.TrimSuffix(baseName, ".tar")
extractedFolder = filepath.Join(dest, baseName)
for i := 1; ; i++ {
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
break
}
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
}
log.Debugln("extractedFolder: %s", extractedFolder)
}
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
return err
}
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, cleanTarPath(header.Name))
} else {
fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name))
}
fpath := filepath.Join(dest, header.Name)
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid file path: %s", fpath)
if !inDest(fpath, dest) {
return fmt.Errorf("invalid file path: %s", fpath)
}
switch header.Typeflag {
case tar.TypeDir:
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
return "", err
return err
}
case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return "", err
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return "", err
return err
}
outFile.Close()
}
}
return extractedFolder, nil
return nil
}
func extract(src, dest string) (string, error) {
srcLower := strings.ToLower(src)
switch {
case strings.HasSuffix(srcLower, ".tar.gz") ||
strings.HasSuffix(srcLower, ".tgz"):
return untgz(src, dest)
case strings.HasSuffix(srcLower, ".zip"):
return unzip(src, dest)
func extract(data []byte, dest string) error {
fileType := detectFileType(data)
log.Debugln("compression Type: %s", fileType)
switch fileType {
case typeZip:
return unzip(data, dest)
case typeTarGzip:
return untgz(data, dest)
default:
return "", fmt.Errorf("unsupported file format: %s", src)
return fmt.Errorf("unknown or unsupported file type")
}
}
@@ -398,22 +281,49 @@ func cleanTarPath(path string) string {
}
func cleanup(root string) error {
if _, err := os.Stat(root); os.IsNotExist(err) {
return nil
dirEntryList, err := os.ReadDir(root)
if err != nil {
return err
}
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
for _, dirEntry := range dirEntryList {
err = os.RemoveAll(filepath.Join(root, dirEntry.Name()))
if err != nil {
return err
}
if info.IsDir() {
if err := os.RemoveAll(path); err != nil {
return err
}
} else {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
}
return nil
}
func moveDir(src string, dst string) error {
dirEntryList, err := os.ReadDir(src)
if err != nil {
return err
}
if len(dirEntryList) == 1 && dirEntryList[0].IsDir() {
src = filepath.Join(src, dirEntryList[0].Name())
log.Debugln("match the singleRoot: %s", src)
dirEntryList, err = os.ReadDir(src)
if err != nil {
return err
}
}
for _, dirEntry := range dirEntryList {
err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name()))
if err != nil {
return err
}
}
return nil
}
func inDest(fpath, dest string) bool {
if rel, err := filepath.Rel(dest, fpath); err == nil {
if filepath.IsLocal(rel) {
return true
}
}
return false
}

View File

@@ -7,6 +7,7 @@ import (
"net"
"net/netip"
"net/url"
"path/filepath"
"strings"
"time"
_ "unsafe"
@@ -174,6 +175,7 @@ type Profile struct {
type TLS struct {
Certificate string
PrivateKey string
EchKey string
CustomTrustCert []string
}
@@ -360,6 +362,7 @@ type RawSniffingConfig struct {
type RawTLS struct {
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"`
}
@@ -755,7 +758,10 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
func parseController(cfg *RawConfig) (*Controller, error) {
if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path)
return nil, C.Path.ErrNotSafePath(path)
}
if uiName := cfg.ExternalUIName; uiName != "" && !filepath.IsLocal(uiName) {
return nil, fmt.Errorf("external UI name is not local: %s", uiName)
}
return &Controller{
ExternalController: cfg.ExternalController,
@@ -814,6 +820,7 @@ func parseTLS(cfg *RawConfig) (*TLS, error) {
return &TLS{
Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey,
EchKey: cfg.TLS.EchKey,
CustomTrustCert: cfg.TLS.CustomTrustCert,
}, nil
}

View File

@@ -92,8 +92,7 @@ type Conn interface {
type PacketConn interface {
N.EnhancePacketConn
Connection
// Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed
// WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error)
ResolveUDP(ctx context.Context, metadata *Metadata) error
}
type Dialer interface {
@@ -319,10 +318,15 @@ type PacketSender interface {
Send(PacketAdapter)
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
Process(PacketConn, WriteBackProxy)
// ResolveUDP do a local resolve UDP dns blocking if metadata is not resolved
ResolveUDP(*Metadata) error
// Close stop the Process loop
Close()
// DoSniff will blocking after sniffer work done
DoSniff(*Metadata) error
// AddMapping add a destination NAT record
AddMapping(originMetadata *Metadata, metadata *Metadata)
// RestoreReadFrom restore destination NAT for ReadFrom
// the implement must ensure returned netip.Add is valid (or just return input addr)
RestoreReadFrom(addr netip.Addr) netip.Addr
}
type NatTable interface {

View File

@@ -261,6 +261,11 @@ func (m *Metadata) Pure() *Metadata {
return m
}
func (m *Metadata) Clone() *Metadata {
copyM := *m
return &copyM
}
func (m *Metadata) AddrPort() netip.AddrPort {
return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort)
}

View File

@@ -1,6 +1,7 @@
package constant
import (
"fmt"
"os"
P "path"
"path/filepath"
@@ -87,10 +88,8 @@ func (p *path) IsSafePath(path string) bool {
if p.allowUnsafePath || features.CMFA {
return true
}
homedir := p.HomeDir()
path = p.Resolve(path)
safePaths := append([]string{homedir}, p.safePaths...) // add homedir to safePaths
for _, safePath := range safePaths {
for _, safePath := range p.SafePaths() {
if rel, err := filepath.Rel(safePath, path); err == nil {
if filepath.IsLocal(rel) {
return true
@@ -100,6 +99,23 @@ func (p *path) IsSafePath(path string) bool {
return false
}
func (p *path) SafePaths() []string {
return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths
}
func (p *path) ErrNotSafePath(path string) error {
return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()}
}
type ErrNotSafePath struct {
Path string
SafePaths []string
}
func (e ErrNotSafePath) Error() string {
return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths)
}
func (p *path) GetPathByHash(prefix, name string) string {
hash := utils.MakeHash([]byte(name))
filename := hash.String()

View File

@@ -10,8 +10,10 @@ type Sniffer interface {
SupportPort(port uint16) bool
}
type ReplaceDomain func(metadata *constant.Metadata, host string)
type MultiPacketSniffer interface {
WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender
WrapperSender(packetSender constant.PacketSender, replaceDomain ReplaceDomain) constant.PacketSender
}
const (

View File

@@ -77,7 +77,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
msg, _, err := c.Client.ExchangeWithConn(m, dConn)
// Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)!
if msg != nil && msg.Truncated && c.Client.Net == "" {
if msg != nil && msg.Truncated && network == "udp" {
tcpClient := *c.Client // copy a client
tcpClient.Net = "tcp"
network = "tcp"

View File

@@ -127,6 +127,28 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool {
return false
}
func (r *Resolver) ResolveECH(ctx context.Context, host string) ([]byte, error) {
query := &D.Msg{}
query.SetQuestion(D.Fqdn(host), D.TypeHTTPS)
msg, err := r.ExchangeContext(ctx, query)
if err != nil {
return nil, err
}
for _, rr := range msg.Answer {
switch resource := rr.(type) {
case *D.HTTPS:
for _, value := range resource.Value {
if echConfig, ok := value.(*D.SVCBECHConfig); ok {
return echConfig.ECH, nil
}
}
}
}
return nil, errors.New("no ECH config found in DNS records")
}
// ExchangeContext a batch of dns request with context.Context, and it use cache
func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) == 0 {
@@ -326,7 +348,8 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er
func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) {
ip, err := netip.ParseAddr(host)
if err == nil {
isIPv4 := ip.Is4() || ip.Is4In6()
ip = ip.Unmap()
isIPv4 := ip.Is4()
if dnsType == D.TypeAAAA && !isIPv4 {
return []netip.Addr{ip}, nil
} else if dnsType == D.TypeA && isIPv4 {

View File

@@ -48,6 +48,13 @@ ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS
tls:
certificate: string # 证书 PEM 格式,或者 证书的路径
private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
custom-certifactes:
- |
-----BEGIN CERTIFICATE-----
@@ -427,6 +434,10 @@ proxies: # socks5
# 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
# 配置指纹将实现 SSL Pining 效果
# fingerprint: xxxx
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: true
# host: bing.com
# path: "/"
@@ -527,6 +538,10 @@ proxies: # socks5
# skip-cert-verify: true
# servername: example.com # priority over wss host
# network: ws
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# ws-opts:
# path: /path
# headers:
@@ -599,6 +614,10 @@ proxies: # socks5
# skip-cert-verify: true
# fingerprint: xxxx
# client-fingerprint: random # Available: "chrome","firefox","safari","random","none"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: "vless-vision"
type: vless
@@ -626,6 +645,7 @@ proxies: # socks5
reality-opts:
public-key: xxx
short-id: xxx # optional
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
client-fingerprint: chrome # cannot be empty
- name: "vless-reality-grpc"
@@ -645,6 +665,7 @@ proxies: # socks5
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
- name: "vless-ws"
type: vless
@@ -683,6 +704,10 @@ proxies: # socks5
# enabled: false
# method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305
# password: "example"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: trojan-grpc
server: server
@@ -740,6 +765,10 @@ proxies: # socks5
up: "30 Mbps" # 若不写单位,默认为 Mbps
down: "200 Mbps" # 若不写单位,默认为 Mbps
# sni: server.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: false
# recv-window-conn: 12582912
# recv-window: 52428800
@@ -763,6 +792,10 @@ proxies: # socks5
# obfs: salamander # 默认为空,如果填写则开启 obfs目前仅支持 salamander
# obfs-password: yourpassword
# sni: server.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: false
# fingerprint: xxxx
# alpn:
@@ -838,6 +871,10 @@ proxies: # socks5
# skip-cert-verify: true
# max-open-streams: 20 # default 100, too many open streams may hurt performance
# sni: example.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
#
# meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效
# 警告,与原版 tuic 不兼容!!!
@@ -906,6 +943,12 @@ proxies: # socks5
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out"
type: dns
# 配置指定 interface-name 和 fwmark 的 DIRECT
- name: en1-direct
type: direct
interface-name: en1
routing-mark: 6667
proxy-groups:
# 代理链,目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic
# wireguard 目前不支持在 relay 中使用,请使用 proxy 中的 dialer-proxy 配置项
@@ -962,14 +1005,6 @@ proxy-groups:
- vmess1
- auto
# 配置指定 interface-name 和 fwmark 的 DIRECT
- name: en1
type: select
interface-name: en1
routing-mark: 6667
proxies:
- DIRECT
- name: UseProvider
type: select
filter: "HK|TW" # 正则表达式,过滤 provider1 中节点名包含 HK 或 TW
@@ -1129,6 +1164,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: http-in-1
type: http
@@ -1142,6 +1184,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: mixed-in-1
type: mixed # HTTP(S) 和 SOCKS 代理混合
@@ -1156,6 +1205,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: reidr-in-1
type: redir
@@ -1205,6 +1261,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
# reality-config:
# dest: test.com:443
@@ -1227,6 +1290,13 @@ listeners:
# 00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# congestion-controller: bbr
# max-idle-time: 15000
# authentication-timeout: 1000
@@ -1258,6 +1328,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
reality-config:
dest: test.com:443
@@ -1278,6 +1355,13 @@ listeners:
# "certificate" and "private-key" are required
certificate: ./server.crt
private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme
- name: trojan-in-1
@@ -1294,6 +1378,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写
certificate: ./server.crt
private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
# reality-config:
# dest: test.com:443
@@ -1319,6 +1410,13 @@ listeners:
00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
## up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps

22
go.mod
View File

@@ -18,25 +18,25 @@ require (
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mdlayher/netlink v1.7.2
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab
github.com/metacubex/bart v0.19.0
github.com/metacubex/bart v0.20.5
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
github.com/metacubex/chacha v0.1.2
github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.51.1-0.20250423035655-e3948b36ce14
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639
github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7
github.com/metacubex/sing v0.5.3
github.com/metacubex/sing-mux v0.3.2
github.com/metacubex/sing-quic v0.0.0-20250504030450-1e678cb3d50b
github.com/metacubex/sing-shadowsocks v0.2.9
github.com/metacubex/sing-shadowsocks2 v0.2.3
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f
github.com/metacubex/sing-shadowsocks v0.2.10
github.com/metacubex/sing-shadowsocks2 v0.2.4
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6
github.com/metacubex/sing-vmess v0.2.1
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c
github.com/metacubex/sing-vmess v0.2.2
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/metacubex/utls v1.7.0-alpha.2
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4
github.com/metacubex/utls v1.7.3
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0
@@ -72,7 +72,7 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect

44
go.sum
View File

@@ -26,8 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 h1:W7mpP4uiOAbBOdDnRXT9EUdauFv7bz+ERT5rPIord00=
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
@@ -97,8 +97,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
@@ -111,35 +111,35 @@ github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.51.1-0.20250423035655-e3948b36ce14 h1:vhB4KEgiN89xXtLlyYWczu3AxgN2T1lp0kIDYT4Faag=
github.com/metacubex/quic-go v0.51.1-0.20250423035655-e3948b36ce14/go.mod h1:9R1NOzCgTcWsdWvOMlmtMuF0uKzuOpsfvEf7U3I8zM0=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxMzzmnXams+ebwabcry4Ydep/zNiesQ=
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM=
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250504030450-1e678cb3d50b h1:JKx0yY/eXU7U5tHiAxANytFtkfEjzOte19qLlc+pFeY=
github.com/metacubex/sing-quic v0.0.0-20250504030450-1e678cb3d50b/go.mod h1:mqtr9bgM9eLvLKQqiLOi5I6AJHkvqAw2a61ABZcLuoE=
github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00=
github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA=
github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0=
github.com/metacubex/sing-shadowsocks2 v0.2.3/go.mod h1:/WNy/Q8ahLCoPRriWuFZFD0Jy+JNp1MEQl28Zw6SaF8=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.10 h1:Pr7LDbjMANIQHl07zWgl1vDuhpsfDQUpZ8cX6DPabfg=
github.com/metacubex/sing-shadowsocks v0.2.10/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk=
github.com/metacubex/sing-shadowsocks2 v0.2.4 h1:Ec0x3hHR7xkld5Z09IGh16wtUUpBb2HgqZ9DExd8Q7s=
github.com/metacubex/sing-shadowsocks2 v0.2.4/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 h1:TAwL91XPa6x1QK55CRm+VTzPvLPUfEr/uFDnOZArqEU=
github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/sing-vmess v0.2.1 h1:I6gM3VUjtvJ15D805EUbNH+SRBuqzJeFnuIbKYUsWZ0=
github.com/metacubex/sing-vmess v0.2.1/go.mod h1:DsODWItJtOMZNna8Qhheg8r3tUivrcO3vWgaTYKnfTo=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.0-alpha.2 h1:kLRg6zDV12R1uclL5qW9Tx4RD6ztGIIrTZWY5zrJXCg=
github.com/metacubex/utls v1.7.0-alpha.2/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A=
github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=

View File

@@ -113,12 +113,11 @@ func ApplyConfig(cfg *config.Config, force bool) {
tunnel.OnInnerLoading()
initInnerTcp()
loadProxyProvider(cfg.Providers)
loadProvider(cfg.Providers)
updateProfile(cfg)
loadRuleProvider(cfg.RuleProviders)
loadProvider(cfg.RuleProviders)
runtime.GC()
tunnel.OnRunning()
hcCompatibleProvider(cfg.Providers)
updateUpdater(cfg)
resolver.ResetConnection()
@@ -303,79 +302,40 @@ func updateRules(rules []C.Rule, subRules map[string][]C.Rule, ruleProviders map
tunnel.UpdateRules(rules, subRules, ruleProviders)
}
func loadProvider(pv provider.Provider) {
if pv.VehicleType() == provider.Compatible {
return
} else {
log.Infoln("Start initial provider %s", (pv).Name())
}
if err := pv.Initial(); err != nil {
switch pv.Type() {
case provider.Proxy:
{
log.Errorln("initial proxy provider %s error: %v", (pv).Name(), err)
}
case provider.Rule:
{
log.Errorln("initial rule provider %s error: %v", (pv).Name(), err)
}
func loadProvider[P provider.Provider](providers map[string]P) {
load := func(pv P) {
name := pv.Name()
if pv.VehicleType() == provider.Compatible {
log.Infoln("Start initial compatible provider %s", name)
} else {
log.Infoln("Start initial provider %s", name)
}
}
}
func loadRuleProvider(ruleProviders map[string]provider.RuleProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, ruleProvider := range ruleProviders {
ruleProvider := ruleProvider
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
loadProvider(ruleProvider)
}()
}
wg.Wait()
}
func loadProxyProvider(proxyProviders map[string]provider.ProxyProvider) {
// limit concurrent size
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
loadProvider(proxyProvider)
}()
}
wg.Wait()
}
func hcCompatibleProvider(proxyProviders map[string]provider.ProxyProvider) {
// limit concurrent size
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == provider.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
if err := pv.Initial(); err != nil {
switch pv.Type() {
case provider.Proxy:
{
log.Errorln("initial proxy provider %s error: %v", name, err)
}
}()
case provider.Rule:
{
log.Errorln("initial rule provider %s error: %v", name, err)
}
}
}
}
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, pv := range providers {
pv := pv
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
load(pv)
}()
}
wg.Wait()
}

View File

@@ -57,6 +57,7 @@ func applyRoute(cfg *config.Config) {
Secret: cfg.Controller.Secret,
Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey,
EchKey: cfg.TLS.EchKey,
DohServer: cfg.Controller.ExternalDohServer,
IsDebug: cfg.General.LogLevel == log.DEBUG,
Cors: route.Cors{

View File

@@ -371,12 +371,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
return
}
} else {
if req.Path == "" {
if req.Path == "" { // default path unneeded any safe check
req.Path = C.Path.Config()
} else if !filepath.IsLocal(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("path is not a valid absolute path"))
return
} else {
if !filepath.IsAbs(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("path is not a absolute path"))
return
}
if !C.Path.IsSafePath(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(C.Path.ErrNotSafePath(req.Path).Error()))
return
}
}
cfg, err = executor.ParseWithPath(req.Path)

View File

@@ -3,7 +3,6 @@ package route
import (
"bytes"
"crypto/subtle"
"crypto/tls"
"encoding/json"
"net"
"net/http"
@@ -17,6 +16,8 @@ import (
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel/statistic"
@@ -62,6 +63,7 @@ type Config struct {
Secret string
Certificate string
PrivateKey string
EchKey string
DohServer string
IsDebug bool
Cors Cors
@@ -186,7 +188,7 @@ func startTLS(cfg *Config) {
// handle tlsAddr
if len(cfg.TLSAddr) > 0 {
c, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path)
cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
@@ -199,14 +201,22 @@ func startTLS(cfg *Config) {
}
log.Infoln("RESTful API tls listening at: %s", l.Addr().String())
tlsConfig := &tlsC.Config{}
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if cfg.EchKey != "" {
err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path)
if err != nil {
log.Errorln("External controller tls serve error: %s", err)
return
}
}
server := &http.Server{
Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{c},
},
}
tlsServer = server
if err = server.ServeTLS(l, "", ""); err != nil {
if err = server.Serve(tlsC.NewListenerForHttps(l, server, tlsConfig)); err != nil {
log.Errorln("External controller tls serve error: %s", err)
}
}

View File

@@ -3,7 +3,6 @@ package anytls
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/binary"
"errors"
"net"
@@ -13,6 +12,8 @@ import (
"github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/buf"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing"
@@ -28,7 +29,7 @@ type Listener struct {
closed bool
config LC.AnyTLSServer
listeners []net.Listener
tlsConfig *tls.Config
tlsConfig *tlsC.Config
userMap map[[32]byte]string
padding atomic.TypedValue[*padding.PaddingFactory]
}
@@ -41,13 +42,20 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
}
tlsConfig := &tls.Config{}
tlsConfig := &tlsC.Config{}
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
}
sl = &Listener{
@@ -87,7 +95,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
return nil, err
}
if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
l = tlsC.NewListener(l, tlsConfig)
} else {
return nil, errors.New("disallow using AnyTLS without certificates config")
}

View File

@@ -10,6 +10,7 @@ type AnyTLSServer struct {
Users map[string]string `yaml:"users" json:"users,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"`
}

View File

@@ -12,5 +12,6 @@ type AuthServer struct {
AuthStore auth.AuthStore
Certificate string
PrivateKey string
EchKey string
RealityConfig reality.Config
}

View File

@@ -14,6 +14,7 @@ type Hysteria2Server struct {
ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
Up string `yaml:"up" json:"up,omitempty"`

View File

@@ -20,6 +20,7 @@ type TrojanServer struct {
GrpcServiceName string
Certificate string
PrivateKey string
EchKey string
RealityConfig reality.Config
MuxOption sing.MuxOption
TrojanSSOption TrojanSSOption

View File

@@ -13,6 +13,7 @@ type TuicServer struct {
Users map[string]string `yaml:"users" json:"users,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"`

View File

@@ -21,6 +21,7 @@ type VlessServer struct {
GrpcServiceName string
Certificate string
PrivateKey string
EchKey string
RealityConfig reality.Config
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}

View File

@@ -21,6 +21,7 @@ type VmessServer struct {
GrpcServiceName string
Certificate string
PrivateKey string
EchKey string
RealityConfig reality.Config
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}

View File

@@ -1,12 +1,13 @@
package http
import (
"crypto/tls"
"errors"
"net"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth"
LC "github.com/metacubex/mihomo/listener/config"
@@ -64,7 +65,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
return nil, err
}
tlsConfig := &tls.Config{}
tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" {
@@ -72,7 +73,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
@@ -87,7 +95,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
l = tlsC.NewListener(l, tlsConfig)
}
hl := &Listener{

View File

@@ -14,6 +14,7 @@ type AnyTLSOption struct {
Users map[string]string `inbound:"users,omitempty"`
Certificate string `inbound:"certificate"`
PrivateKey string `inbound:"private-key"`
EchKey string `inbound:"ech-key,omitempty"`
PaddingScheme string `inbound:"padding-scheme,omitempty"`
}
@@ -42,6 +43,7 @@ func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) {
Users: options.Users,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
PaddingScheme: options.PaddingScheme,
},
}, nil

View File

@@ -60,4 +60,14 @@ func TestInboundAnyTLS_TLS(t *testing.T) {
Fingerprint: tlsFingerprint,
}
testInboundAnyTLS(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,
}
testInboundAnyTLS(t, inboundOptions, outboundOptions)
})
}

View File

@@ -18,12 +18,15 @@ import (
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/generater"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/stretchr/testify/assert"
"golang.org/x/net/http2"
)
var httpPath = "/inbound_test"
@@ -38,6 +41,8 @@ var realityPrivateKey, realityPublickey string
var realityDest = "itunes.apple.com"
var realityShortid = "10f897e26c4b9478"
var realityRealDial = false
var echPublicSni = "public.sni"
var echConfigBase64, echKeyPem, _ = ech.GenECHConfig(echPublicSni)
func init() {
rand.Read(httpData)
@@ -131,7 +136,10 @@ func NewHttpTestTunnel() *TestTunnel {
r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) {
render.Data(w, r, httpData)
})
go http.Serve(ln, r)
h2Server := &http2.Server{}
server := http.Server{Handler: r}
_ = http2.ConfigureServer(&server, h2Server)
go server.Serve(ln)
testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil)
if !assert.NoError(t, err) {
@@ -205,23 +213,27 @@ func NewHttpTestTunnel() *TestTunnel {
ch: make(chan struct{}),
}
if metadata.DstPort == 443 {
tlsConn := tls.Server(c, tlsConfig.Clone())
tlsConn := tlsC.Server(c, tlsC.UConfig(tlsConfig))
if metadata.Host == realityDest { // ignore the tls handshake error for realityDest
if realityRealDial {
rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
if err != nil {
panic(err)
}
N.Relay(rconn, tlsConn)
return
}
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(ctx); err != nil {
N.Relay(rconn, conn)
return
}
}
ln.ch <- tlsConn
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(ctx); err != nil {
return
}
if tlsConn.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
h2Server.ServeConn(tlsConn, &http2.ServeConnOpts{BaseConfig: &server})
} else {
ln.ch <- tlsConn
}
} else {
ln.ch <- c
}

View File

@@ -16,6 +16,7 @@ type HTTPOption struct {
Users AuthUsers `inbound:"users,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
}
@@ -64,6 +65,7 @@ func (h *HTTP) Listen(tunnel C.Tunnel) error {
AuthStore: h.config.Users.GetAuthStore(),
Certificate: h.config.Certificate,
PrivateKey: h.config.PrivateKey,
EchKey: h.config.EchKey,
RealityConfig: h.config.RealityConfig.Build(),
},
tunnel,

View File

@@ -16,6 +16,7 @@ type Hysteria2Option struct {
ObfsPassword string `inbound:"obfs-password,omitempty"`
Certificate string `inbound:"certificate"`
PrivateKey string `inbound:"private-key"`
EchKey string `inbound:"ech-key,omitempty"`
MaxIdleTime int `inbound:"max-idle-time,omitempty"`
ALPN []string `inbound:"alpn,omitempty"`
Up string `inbound:"up,omitempty"`
@@ -60,6 +61,7 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) {
ObfsPassword: options.ObfsPassword,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
MaxIdleTime: options.MaxIdleTime,
ALPN: options.ALPN,
Up: options.Up,

View File

@@ -60,6 +60,16 @@ func TestInboundHysteria2_TLS(t *testing.T) {
Fingerprint: tlsFingerprint,
}
testInboundHysteria2(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,
}
testInboundHysteria2(t, inboundOptions, outboundOptions)
})
}
func TestInboundHysteria2_Salamander(t *testing.T) {
@@ -75,6 +85,16 @@ func TestInboundHysteria2_Salamander(t *testing.T) {
ObfsPassword: userUUID,
}
testInboundHysteria2(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,
}
testInboundHysteria2(t, inboundOptions, outboundOptions)
})
}
func TestInboundHysteria2_Brutal(t *testing.T) {
@@ -90,4 +110,14 @@ func TestInboundHysteria2_Brutal(t *testing.T) {
Down: "200 Mbps",
}
testInboundHysteria2(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,
}
testInboundHysteria2(t, inboundOptions, outboundOptions)
})
}

View File

@@ -18,6 +18,7 @@ type MixedOption struct {
UDP bool `inbound:"udp,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
}
@@ -69,6 +70,7 @@ func (m *Mixed) Listen(tunnel C.Tunnel) error {
AuthStore: m.config.Users.GetAuthStore(),
Certificate: m.config.Certificate,
PrivateKey: m.config.PrivateKey,
EchKey: m.config.EchKey,
RealityConfig: m.config.RealityConfig.Build(),
},
tunnel,

View File

@@ -17,6 +17,7 @@ type SocksOption struct {
UDP bool `inbound:"udp,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
}
@@ -89,6 +90,7 @@ func (s *Socks) Listen(tunnel C.Tunnel) error {
AuthStore: s.config.Users.GetAuthStore(),
Certificate: s.config.Certificate,
PrivateKey: s.config.PrivateKey,
EchKey: s.config.EchKey,
RealityConfig: s.config.RealityConfig.Build(),
},
tunnel,

View File

@@ -16,6 +16,7 @@ type TrojanOption struct {
GrpcServiceName string `inbound:"grpc-service-name,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
SSOption TrojanSSOption `inbound:"ss-option,omitempty"`
@@ -67,6 +68,7 @@ func NewTrojan(options *TrojanOption) (*Trojan, error) {
GrpcServiceName: options.GrpcServiceName,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
RealityConfig: options.RealityConfig.Build(),
MuxOption: options.MuxOption.Build(),
TrojanSSOption: LC.TrojanSSOption{

View File

@@ -64,6 +64,16 @@ func TestInboundTrojan_TLS(t *testing.T) {
Fingerprint: tlsFingerprint,
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrojan_Wss1(t *testing.T) {
@@ -80,6 +90,16 @@ func TestInboundTrojan_Wss1(t *testing.T) {
},
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrojan_Wss2(t *testing.T) {
@@ -97,6 +117,16 @@ func TestInboundTrojan_Wss2(t *testing.T) {
},
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrojan_Grpc1(t *testing.T) {
@@ -111,6 +141,16 @@ func TestInboundTrojan_Grpc1(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrojan_Grpc2(t *testing.T) {
@@ -126,6 +166,16 @@ func TestInboundTrojan_Grpc2(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrojan_Reality(t *testing.T) {
@@ -190,6 +240,16 @@ func TestInboundTrojan_TLS_TrojanSS(t *testing.T) {
},
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}
func TestInboundTrojan_Wss_TrojanSS(t *testing.T) {
@@ -216,4 +276,14 @@ func TestInboundTrojan_Wss_TrojanSS(t *testing.T) {
},
}
testInboundTrojan(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,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
})
}

View File

@@ -15,6 +15,7 @@ type TuicOption struct {
Users map[string]string `inbound:"users,omitempty"`
Certificate string `inbound:"certificate"`
PrivateKey string `inbound:"private-key"`
EchKey string `inbound:"ech-key,omitempty"`
CongestionController string `inbound:"congestion-controller,omitempty"`
MaxIdleTime int `inbound:"max-idle-time,omitempty"`
AuthenticationTimeout int `inbound:"authentication-timeout,omitempty"`
@@ -50,6 +51,7 @@ func NewTuic(options *TuicOption) (*Tuic, error) {
Users: options.Users,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
CongestionController: options.CongestionController,
MaxIdleTime: options.MaxIdleTime,
AuthenticationTimeout: options.AuthenticationTimeout,

View File

@@ -89,4 +89,14 @@ func TestInboundTuic_TLS(t *testing.T) {
Fingerprint: tlsFingerprint,
}
testInboundTuic(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,
}
testInboundTuic(t, inboundOptions, outboundOptions)
})
}

View File

@@ -16,6 +16,7 @@ type VlessOption struct {
GrpcServiceName string `inbound:"grpc-service-name,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
@@ -61,6 +62,7 @@ func NewVless(options *VlessOption) (*Vless, error) {
GrpcServiceName: options.GrpcServiceName,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
RealityConfig: options.RealityConfig.Build(),
MuxOption: options.MuxOption.Build(),
},

View File

@@ -66,9 +66,25 @@ func TestInboundVless_TLS(t *testing.T) {
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(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,
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
})
}
func TestInboundVless_Wss1(t *testing.T) {
@@ -87,9 +103,25 @@ func TestInboundVless_Wss1(t *testing.T) {
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(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,
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
})
}
func TestInboundVless_Wss2(t *testing.T) {
@@ -109,9 +141,25 @@ func TestInboundVless_Wss2(t *testing.T) {
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(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,
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
})
}
func TestInboundVless_Grpc1(t *testing.T) {
@@ -127,6 +175,16 @@ func TestInboundVless_Grpc1(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVless(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,
}
testInboundVless(t, inboundOptions, outboundOptions)
})
}
func TestInboundVless_Grpc2(t *testing.T) {
@@ -143,6 +201,16 @@ func TestInboundVless_Grpc2(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVless(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,
}
testInboundVless(t, inboundOptions, outboundOptions)
})
}
func TestInboundVless_Reality(t *testing.T) {
@@ -165,9 +233,20 @@ func TestInboundVless_Reality(t *testing.T) {
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
t.Run("X25519MLKEM768", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.RealityOpts.SupportX25519MLKEM768 = true
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
})
}
func TestInboundVless_Reality_Grpc(t *testing.T) {
@@ -192,4 +271,9 @@ func TestInboundVless_Reality_Grpc(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("X25519MLKEM768", func(t *testing.T) {
outboundOptions := outboundOptions
outboundOptions.RealityOpts.SupportX25519MLKEM768 = true
testInboundVless(t, inboundOptions, outboundOptions)
})
}

View File

@@ -16,6 +16,7 @@ type VmessOption struct {
GrpcServiceName string `inbound:"grpc-service-name,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
EchKey string `inbound:"ech-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
@@ -61,6 +62,7 @@ func NewVmess(options *VmessOption) (*Vmess, error) {
GrpcServiceName: options.GrpcServiceName,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
EchKey: options.EchKey,
RealityConfig: options.RealityConfig.Build(),
MuxOption: options.MuxOption.Build(),
},

View File

@@ -73,6 +73,16 @@ func TestInboundVMess_TLS(t *testing.T) {
Fingerprint: tlsFingerprint,
}
testInboundVMess(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,
}
testInboundVMess(t, inboundOptions, outboundOptions)
})
}
func TestInboundVMess_Ws(t *testing.T) {
@@ -160,6 +170,16 @@ func TestInboundVMess_Wss1(t *testing.T) {
},
}
testInboundVMess(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,
}
testInboundVMess(t, inboundOptions, outboundOptions)
})
}
func TestInboundVMess_Wss2(t *testing.T) {
@@ -178,6 +198,16 @@ func TestInboundVMess_Wss2(t *testing.T) {
},
}
testInboundVMess(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,
}
testInboundVMess(t, inboundOptions, outboundOptions)
})
}
func TestInboundVMess_Grpc1(t *testing.T) {
@@ -193,6 +223,16 @@ func TestInboundVMess_Grpc1(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVMess(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,
}
testInboundVMess(t, inboundOptions, outboundOptions)
})
}
func TestInboundVMess_Grpc2(t *testing.T) {
@@ -209,6 +249,16 @@ func TestInboundVMess_Grpc2(t *testing.T) {
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVMess(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,
}
testInboundVMess(t, inboundOptions, outboundOptions)
})
}
func TestInboundVMess_Reality(t *testing.T) {

View File

@@ -1,7 +1,6 @@
package mixed
import (
"crypto/tls"
"errors"
"net"
@@ -9,6 +8,8 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth"
LC "github.com/metacubex/mihomo/listener/config"
@@ -60,7 +61,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
return nil, err
}
tlsConfig := &tls.Config{}
tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" {
@@ -68,7 +69,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
@@ -83,7 +91,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
l = tlsC.NewListener(l, tlsConfig)
}
ml := &Listener{

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net"
"runtime/debug"
"time"
N "github.com/metacubex/mihomo/common/net"
@@ -89,7 +90,8 @@ func (b Builder) NewListener(l net.Listener) net.Listener {
// We fixed it by calling Close() directly.
return realityConnWrapper{c}, nil
}, func(a any) {
log.Errorln("reality server panic: %s", a)
stack := debug.Stack()
log.Errorln("reality server panic: %s\n%s", a, stack)
})
}

View File

@@ -3,6 +3,7 @@ package sing
import (
"context"
"golang.org/x/exp/slices"
"net"
"github.com/metacubex/mihomo/adapter/inbound"
@@ -29,3 +30,18 @@ func getAdditions(ctx context.Context) (additions []inbound.Addition) {
}
return
}
var ctxKeyInAddr = contextKey("InAddr")
func WithInAddr(ctx context.Context, inAddr net.Addr) context.Context {
return context.WithValue(ctx, ctxKeyInAddr, inAddr)
}
func getInAddr(ctx context.Context) net.Addr {
if v := ctx.Value(ctxKeyInAddr); v != nil {
if a, ok := v.(net.Addr); ok {
return a
}
}
return nil
}

View File

@@ -16,6 +16,7 @@ import (
mux "github.com/metacubex/sing-mux"
vmess "github.com/metacubex/sing-vmess"
"github.com/metacubex/sing/common"
"github.com/metacubex/sing/common/buf"
"github.com/metacubex/sing/common/bufio"
"github.com/metacubex/sing/common/bufio/deadline"
@@ -146,11 +147,11 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta
func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error {
defer func() { _ = conn.Close() }()
mutex := sync.Mutex{}
conn2 := bufio.NewNetPacketConn(conn) // a new interface to set nil in defer
writer := bufio.NewNetPacketWriter(conn) // a new interface to set nil in defer
defer func() {
mutex.Lock() // this goroutine must exit after all conn.WritePacket() is not running
defer mutex.Unlock()
conn2 = nil
writer = nil
}()
rwOptions := network.ReadWaitOptions{}
readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn)
@@ -180,32 +181,59 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.
return err
}
cPacket := &packet{
conn: &conn2,
mutex: &mutex,
rAddr: metadata.Source.UDPAddr(),
lAddr: conn.LocalAddr(),
buff: buff,
writer: &writer,
mutex: &mutex,
rAddr: metadata.Source.UDPAddr(),
lAddr: conn.LocalAddr(),
buff: buff,
}
cMetadata := &C.Metadata{
NetWork: C.UDP,
Type: h.Type,
if lAddr := getInAddr(ctx); lAddr != nil {
cPacket.lAddr = lAddr
}
if metadata.Source.IsIP() && metadata.Source.Fqdn == "" {
cMetadata.RawSrcAddr = metadata.Source.Unwrap().UDPAddr()
}
if dest.IsIP() && dest.Fqdn == "" {
cMetadata.RawDstAddr = dest.Unwrap().UDPAddr()
}
inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(dest), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr()))
inbound.ApplyAdditions(cMetadata, h.Additions...)
inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...)
h.Tunnel.HandleUDPPacket(cPacket, cMetadata)
h.handlePacket(ctx, cPacket, metadata.Source, dest)
}
return nil
}
type localAddr interface {
LocalAddr() net.Addr
}
func (h *ListenerHandler) NewPacket(ctx context.Context, key netip.AddrPort, buffer *buf.Buffer, metadata M.Metadata, init func(natConn network.PacketConn) network.PacketWriter) {
writer := bufio.NewNetPacketWriter(init(nil))
mutex := sync.Mutex{}
cPacket := &packet{
writer: &writer,
mutex: &mutex,
rAddr: metadata.Source.UDPAddr(),
buff: buffer,
}
if conn, ok := common.Cast[localAddr](writer); ok {
cPacket.rAddr = conn.LocalAddr()
} else {
cPacket.rAddr = metadata.Source.UDPAddr() // tun does not have real inAddr
}
h.handlePacket(ctx, cPacket, metadata.Source, metadata.Destination)
}
func (h *ListenerHandler) handlePacket(ctx context.Context, cPacket *packet, source M.Socksaddr, destination M.Socksaddr) {
cMetadata := &C.Metadata{
NetWork: C.UDP,
Type: h.Type,
}
if source.IsIP() && source.Fqdn == "" {
cMetadata.RawSrcAddr = source.Unwrap().UDPAddr()
}
if destination.IsIP() && destination.Fqdn == "" {
cMetadata.RawDstAddr = destination.Unwrap().UDPAddr()
}
inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(destination), inbound.WithSrcAddr(source), inbound.WithInAddr(cPacket.InAddr()))
inbound.ApplyAdditions(cMetadata, h.Additions...)
inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...)
h.Tunnel.HandleUDPPacket(cPacket, cMetadata)
}
func (h *ListenerHandler) NewError(ctx context.Context, err error) {
log.Warnln("%s listener get error: %+v", h.Type.String(), err)
}
@@ -225,11 +253,11 @@ func ShouldIgnorePacketError(err error) bool {
}
type packet struct {
conn *network.NetPacketConn
mutex *sync.Mutex
rAddr net.Addr
lAddr net.Addr
buff *buf.Buffer
writer *network.NetPacketWriter
mutex *sync.Mutex
rAddr net.Addr
lAddr net.Addr
buff *buf.Buffer
}
func (c *packet) Data() []byte {
@@ -245,7 +273,7 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) {
c.mutex.Lock()
defer c.mutex.Unlock()
conn := *c.conn
conn := *c.writer
if conn == nil {
err = errors.New("writeBack to closed connection")
return

View File

@@ -2,7 +2,6 @@ package sing_hysteria2
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
@@ -15,6 +14,7 @@ import (
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/common/sockopt"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
@@ -60,9 +60,16 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{cert},
tlsConfig := &tlsC.Config{
MinVersion: tlsC.VersionTLS13,
}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
if len(config.ALPN) > 0 {
tlsConfig.NextProtos = config.ALPN
@@ -125,7 +132,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
SendBPS: outbound.StringToBps(config.Up),
ReceiveBPS: outbound.StringToBps(config.Down),
SalamanderPassword: salamanderPassword,
TLSConfig: tlsC.UConfig(tlsConfig),
TLSConfig: tlsConfig,
QUICConfig: quicConfig,
IgnoreClientBandwidth: config.IgnoreClientBandwidth,
UDPTimeout: sing.UDPTimeout,

View File

@@ -156,11 +156,7 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
go func() {
conn := bufio.NewPacketConn(ul)
rwOptions := network.ReadWaitOptions{
FrontHeadroom: network.CalculateFrontHeadroom(sl.service),
RearHeadroom: network.CalculateRearHeadroom(sl.service),
MTU: network.CalculateMTU(conn, sl.service),
}
rwOptions := network.NewReadWaitOptions(conn, sl.service)
readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn)
if isReadWaiter {
readWaiter.InitializeReadWaiter(rwOptions)
@@ -188,7 +184,9 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
}
continue
}
_ = sl.service.NewPacket(context.TODO(), conn, buff, M.Metadata{
ctx := context.TODO()
ctx = sing.WithInAddr(ctx, ul.LocalAddr())
_ = sl.service.NewPacket(ctx, conn, buff, M.Metadata{
Protocol: "shadowsocks",
Source: dest,
})

View File

@@ -43,16 +43,31 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta
return h.ListenerHandler.NewConnection(ctx, conn, metadata)
}
func (h *ListenerHandler) NewPacket(ctx context.Context, key netip.AddrPort, buffer *buf.Buffer, metadata M.Metadata, init func(natConn network.PacketConn) network.PacketWriter) {
if h.ShouldHijackDns(metadata.Destination.AddrPort()) {
log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String())
writer := init(nil)
rwOptions := network.ReadWaitOptions{
FrontHeadroom: network.CalculateFrontHeadroom(writer),
RearHeadroom: network.CalculateRearHeadroom(writer),
MTU: resolver.SafeDnsPacketSize,
}
go relayDnsPacket(ctx, buffer, rwOptions, metadata.Destination, nil, &writer)
return
}
h.ListenerHandler.NewPacket(ctx, key, buffer, metadata, init)
}
func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error {
if h.ShouldHijackDns(metadata.Destination.AddrPort()) {
log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String())
defer func() { _ = conn.Close() }()
mutex := sync.Mutex{}
conn2 := conn // a new interface to set nil in defer
var writer network.PacketWriter = conn // a new interface to set nil in defer
defer func() {
mutex.Lock() // this goroutine must exit after all conn.WritePacket() is not running
defer mutex.Unlock()
conn2 = nil
writer = nil
}()
rwOptions := network.ReadWaitOptions{
FrontHeadroom: network.CalculateFrontHeadroom(conn),
@@ -89,43 +104,47 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.
}
return err
}
go func() {
ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDnsRelayTimeout)
defer cancel()
inData := readBuff.Bytes()
writeBuff := readBuff
writeBuff.Resize(writeBuff.Start(), 0)
if len(writeBuff.FreeBytes()) < resolver.SafeDnsPacketSize { // only create a new buffer when space don't enough
writeBuff = rwOptions.NewPacketBuffer()
}
msg, err := resolver.RelayDnsPacket(ctx, inData, writeBuff.FreeBytes())
if writeBuff != readBuff {
readBuff.Release()
}
if err != nil {
writeBuff.Release()
return
}
writeBuff.Truncate(len(msg))
mutex.Lock()
defer mutex.Unlock()
conn := conn2
if conn == nil {
writeBuff.Release()
return
}
err = conn.WritePacket(writeBuff, dest) // WritePacket will release writeBuff
if err != nil {
writeBuff.Release()
return
}
}()
go relayDnsPacket(ctx, readBuff, rwOptions, dest, &mutex, &writer)
}
return nil
}
return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata)
}
func relayDnsPacket(ctx context.Context, readBuff *buf.Buffer, rwOptions network.ReadWaitOptions, dest M.Socksaddr, mutex *sync.Mutex, writer *network.PacketWriter) {
ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDnsRelayTimeout)
defer cancel()
inData := readBuff.Bytes()
writeBuff := readBuff
writeBuff.Resize(writeBuff.Start(), 0)
if len(writeBuff.FreeBytes()) < resolver.SafeDnsPacketSize { // only create a new buffer when space don't enough
writeBuff = rwOptions.NewPacketBuffer()
}
msg, err := resolver.RelayDnsPacket(ctx, inData, writeBuff.FreeBytes())
if writeBuff != readBuff {
readBuff.Release()
}
if err != nil {
writeBuff.Release()
return
}
writeBuff.Truncate(len(msg))
if mutex != nil {
mutex.Lock()
defer mutex.Unlock()
}
conn := *writer
if conn == nil {
writeBuff.Release()
return
}
err = conn.WritePacket(writeBuff, dest) // WritePacket will release writeBuff
if err != nil {
writeBuff.Release()
return
}
}
func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler {
handle := *h
handle.ListenerHandler = h.ListenerHandler.TypeMutation(typ)

View File

@@ -2,7 +2,6 @@ package sing_vless
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
@@ -12,6 +11,7 @@ import (
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
@@ -82,16 +82,23 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
sl = &Listener{false, config, nil, service}
tlsConfig := &tls.Config{}
tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder
var httpHandler http.Handler
var httpServer http.Server
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
@@ -112,16 +119,16 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
sl.HandleConn(conn, tunnel, additions...)
})
httpHandler = httpMux
httpServer.Handler = httpMux
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1")
}
if config.GrpcServiceName != "" {
httpHandler = gun.NewServerHandler(gun.ServerOption{
httpServer.Handler = gun.NewServerHandler(gun.ServerOption{
ServiceName: config.GrpcServiceName,
ConnHandler: func(conn net.Conn) {
sl.HandleConn(conn, tunnel, additions...)
},
HttpHandler: httpHandler,
HttpHandler: httpServer.Handler,
})
tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1
}
@@ -137,15 +144,19 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
if httpServer.Handler != nil {
l = tlsC.NewListenerForHttps(l, &httpServer, tlsConfig)
} else {
l = tlsC.NewListener(l, tlsConfig)
}
} else {
return nil, errors.New("disallow using Vless without both certificates/reality config")
}
sl.listeners = append(sl.listeners, l)
go func() {
if httpHandler != nil {
_ = http.Serve(l, httpHandler)
if httpServer.Handler != nil {
_ = httpServer.Serve(l)
return
}
for {

View File

@@ -2,7 +2,6 @@ package sing_vmess
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
@@ -11,6 +10,8 @@ import (
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/reality"
@@ -75,16 +76,23 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
sl = &Listener{false, config, nil, service}
tlsConfig := &tls.Config{}
tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder
var httpHandler http.Handler
var httpServer http.Server
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
@@ -105,16 +113,16 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
sl.HandleConn(conn, tunnel, additions...)
})
httpHandler = httpMux
httpServer.Handler = httpMux
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1")
}
if config.GrpcServiceName != "" {
httpHandler = gun.NewServerHandler(gun.ServerOption{
httpServer.Handler = gun.NewServerHandler(gun.ServerOption{
ServiceName: config.GrpcServiceName,
ConnHandler: func(conn net.Conn) {
sl.HandleConn(conn, tunnel, additions...)
},
HttpHandler: httpHandler,
HttpHandler: httpServer.Handler,
})
tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1
}
@@ -130,13 +138,17 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
if httpServer.Handler != nil {
l = tlsC.NewListenerForHttps(l, &httpServer, tlsConfig)
} else {
l = tlsC.NewListener(l, tlsConfig)
}
}
sl.listeners = append(sl.listeners, l)
go func() {
if httpHandler != nil {
_ = http.Serve(l, httpHandler)
if httpServer.Handler != nil {
_ = httpServer.Serve(l)
return
}
for {

View File

@@ -1,7 +1,6 @@
package socks
import (
"crypto/tls"
"errors"
"io"
"net"
@@ -10,6 +9,8 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth"
LC "github.com/metacubex/mihomo/listener/config"
@@ -59,7 +60,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
return nil, err
}
tlsConfig := &tls.Config{}
tlsConfig := &tlsC.Config{}
var realityBuilder *reality.Builder
if config.Certificate != "" && config.PrivateKey != "" {
@@ -67,7 +68,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
@@ -82,7 +90,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if realityBuilder != nil {
l = realityBuilder.NewListener(l)
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
l = tlsC.NewListener(l, tlsConfig)
}
sl := &Listener{

Some files were not shown because too many files have changed in this diff Show More