diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 87202e6992..3e9d3d936e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,12 +5,14 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v1 + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v3 with: - go-version: 1.18 - - name: Check out code - uses: actions/checkout@v1 + go-version: '1.19' + check-latest: true + cache: true - name: Build run: make all - name: Release diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 927d99aec0..c20efb0b74 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -13,26 +13,15 @@ jobs: Build: runs-on: ubuntu-latest steps: - - name: Get latest go version - id: version - run: | - echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: ${{ steps.version.outputs.go_version }} - - name: Check out code into the Go module directory uses: actions/checkout@v3 - - name: Cache go module - uses: actions/cache@v2 + - name: Setup Go + uses: actions/setup-go@v3 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - + go-version: '1.19' + check-latest: true + cache: true - name: Test if: ${{github.ref_name=='Beta'}} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4b2b60da8c..c74637a6ff 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,24 +7,16 @@ jobs: Build: runs-on: ubuntu-latest steps: - - name: Get latest go version - id: version - run: | - echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: ${{ steps.version.outputs.go_version }} - - name: Check out code into the Go module directory uses: actions/checkout@v3 - - name: Cache go module - uses: actions/cache@v2 + + - name: Setup Go + uses: actions/setup-go@v3 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: '1.19' + check-latest: true + cache: true + - name: Test run: | go test ./... diff --git a/.golangci.yaml b/.golangci.yaml index a0764585c3..f5b6739734 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -8,9 +8,10 @@ linters: linters-settings: gci: + custom-order: true sections: - standard - prefix(github.com/Dreamacro/clash) - default staticcheck: - go: '1.18' + go: '1.19' diff --git a/README.md b/README.md index f95d4572bc..321ce97ed4 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,43 @@ proxies: grpc-service-name: grpcname ``` + +Support outbound transport protocol `Wireguard` +```yaml +proxies: + - name: "wg" + type: wireguard + server: 162.159.192.1 + port: 2480 + ip: 172.16.0.2 + ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5 + private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= + public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= + udp: true +``` + +Support outbound transport protocol `Tuic` +```yaml +proxies: + - name: "tuic" + server: www.example.com + port: 10443 + type: tuic + token: TOKEN + # ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server' + # heartbeat-interval: 10000 + # alpn: [h3] + # disable-sni: true + reduce-rtt: true + # request-timeout: 8000 + udp-relay-mode: native # Available: "native", "quic". Default: "native" + # congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic" + # max-udp-relay-packet-size: 1500 + # fast-open: true + # skip-cert-verify: true + +``` + ### IPTABLES configuration Work on Linux OS who's supported `iptables` @@ -306,6 +343,7 @@ the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library ## Credits * [Dreamacro/clash](https://github.com/Dreamacro/clash) +* [SagerNet/sing-box](https://github.com/SagerNet/sing-box) * [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) * [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) * [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go) diff --git a/adapter/adapter.go b/adapter/adapter.go index 2b3c7e6c31..feef72be47 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -92,6 +92,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) { mapping["history"] = p.DelayHistory() mapping["name"] = p.Name() mapping["udp"] = p.SupportUDP() + mapping["tfo"] = p.SupportTFO() return json.Marshal(mapping) } @@ -198,10 +199,9 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) { } addr = C.Metadata{ - AddrType: C.AtypDomainName, - Host: u.Hostname(), - DstIP: netip.Addr{}, - DstPort: port, + Host: u.Hostname(), + DstIP: netip.Addr{}, + DstPort: port, } return } diff --git a/adapter/inbound/addition.go b/adapter/inbound/addition.go new file mode 100644 index 0000000000..5966e78485 --- /dev/null +++ b/adapter/inbound/addition.go @@ -0,0 +1,29 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" +) + +type Addition func(metadata *C.Metadata) + +func (a Addition) Apply(metadata *C.Metadata) { + a(metadata) +} + +func WithInName(name string) Addition { + return func(metadata *C.Metadata) { + metadata.InName = name + } +} + +func WithSpecialRules(specialRules string) Addition { + return func(metadata *C.Metadata) { + metadata.SpecialRules = specialRules + } +} + +func WithSpecialProxy(specialProxy string) Addition { + return func(metadata *C.Metadata) { + metadata.SpecialProxy = specialProxy + } +} diff --git a/adapter/inbound/http.go b/adapter/inbound/http.go index 89960cf340..94040078e6 100644 --- a/adapter/inbound/http.go +++ b/adapter/inbound/http.go @@ -9,13 +9,20 @@ import ( ) // NewHTTP receive normal http request and return HTTPContext -func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnContext { +func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn, additions ...Addition) *context.ConnContext { metadata := parseSocksAddr(target) metadata.NetWork = C.TCP metadata.Type = C.HTTP + for _, addition := range additions { + addition.Apply(metadata) + } if ip, port, err := parseAddr(source.String()); err == nil { metadata.SrcIP = ip metadata.SrcPort = port } + if ip, port, err := parseAddr(conn.LocalAddr().String()); err == nil { + metadata.InIP = ip + metadata.InPort = port + } return context.NewConnContext(conn, metadata) } diff --git a/adapter/inbound/https.go b/adapter/inbound/https.go index 99bc433f62..3fff23710a 100644 --- a/adapter/inbound/https.go +++ b/adapter/inbound/https.go @@ -9,12 +9,19 @@ import ( ) // NewHTTPS receive CONNECT request and return ConnContext -func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext { +func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) *context.ConnContext { metadata := parseHTTPAddr(request) metadata.Type = C.HTTPS + for _, addition := range additions { + addition.Apply(metadata) + } if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil { metadata.SrcIP = ip metadata.SrcPort = port } + if ip, port, err := parseAddr(conn.LocalAddr().String()); err == nil { + metadata.InIP = ip + metadata.InPort = port + } return context.NewConnContext(conn, metadata) } diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go new file mode 100644 index 0000000000..d481a56e22 --- /dev/null +++ b/adapter/inbound/listen.go @@ -0,0 +1,26 @@ +package inbound + +import ( + "context" + "net" + + "github.com/database64128/tfo-go/v2" +) + +var ( + lc = tfo.ListenConfig{ + DisableTFO: true, + } +) + +func SetTfo(open bool) { + lc.DisableTFO = !open +} + +func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { + return lc.Listen(ctx, network, address) +} + +func Listen(network, address string) (net.Listener, error) { + return ListenContext(context.Background(), network, address) +} diff --git a/adapter/inbound/packet.go b/adapter/inbound/packet.go index 80b136cd60..d1fcb05dd9 100644 --- a/adapter/inbound/packet.go +++ b/adapter/inbound/packet.go @@ -17,17 +17,26 @@ func (s *PacketAdapter) Metadata() *C.Metadata { } // NewPacket is PacketAdapter generator -func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter { +func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter { metadata := parseSocksAddr(target) metadata.NetWork = C.UDP metadata.Type = source + for _, addition := range additions { + addition.Apply(metadata) + } if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil { metadata.SrcIP = ip metadata.SrcPort = port } + if p, ok := packet.(C.UDPPacketInAddr); ok { + if ip, port, err := parseAddr(p.InAddr().String()); err == nil { + metadata.InIP = ip + metadata.InPort = port + } + } return &PacketAdapter{ - UDPPacket: packet, - metadata: metadata, + packet, + metadata, } } diff --git a/adapter/inbound/socket.go b/adapter/inbound/socket.go index 28608be9b2..557b22abc4 100644 --- a/adapter/inbound/socket.go +++ b/adapter/inbound/socket.go @@ -10,11 +10,16 @@ import ( ) // NewSocket receive TCP inbound and return ConnContext -func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext { +func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) *context.ConnContext { metadata := parseSocksAddr(target) metadata.NetWork = C.TCP metadata.Type = source + for _, addition := range additions { + addition.Apply(metadata) + } + remoteAddr := conn.RemoteAddr() + // Filter when net.Addr interface is nil if remoteAddr != nil { if ip, port, err := parseAddr(remoteAddr.String()); err == nil { @@ -22,6 +27,14 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo metadata.SrcPort = port } } + localAddr := conn.LocalAddr() + // Filter when net.Addr interface is nil + if localAddr != nil { + if ip, port, err := parseAddr(localAddr.String()); err == nil { + metadata.InIP = ip + metadata.InPort = port + } + } return context.NewConnContext(conn, metadata) } @@ -32,17 +45,12 @@ func NewInner(conn net.Conn, dst string, host string) *context.ConnContext { metadata.Type = C.INNER metadata.DNSMode = C.DNSMapping metadata.Host = host - metadata.AddrType = C.AtypDomainName metadata.Process = C.ClashName if h, port, err := net.SplitHostPort(dst); err == nil { metadata.DstPort = port if host == "" { if ip, err := netip.ParseAddr(h); err == nil { metadata.DstIP = ip - metadata.AddrType = C.AtypIPv4 - if ip.Is6() { - metadata.AddrType = C.AtypIPv6 - } } } } diff --git a/adapter/inbound/util.go b/adapter/inbound/util.go index 9a024529dd..cddbcf1f65 100644 --- a/adapter/inbound/util.go +++ b/adapter/inbound/util.go @@ -13,9 +13,7 @@ import ( ) func parseSocksAddr(target socks5.Addr) *C.Metadata { - metadata := &C.Metadata{ - AddrType: int(target[0]), - } + metadata := &C.Metadata{} switch target[0] { case socks5.AtypDomainName: @@ -45,21 +43,14 @@ func parseHTTPAddr(request *http.Request) *C.Metadata { host = strings.TrimRight(host, ".") metadata := &C.Metadata{ - NetWork: C.TCP, - AddrType: C.AtypDomainName, - Host: host, - DstIP: netip.Addr{}, - DstPort: port, + NetWork: C.TCP, + Host: host, + DstIP: netip.Addr{}, + DstPort: port, } ip, err := netip.ParseAddr(host) if err == nil { - switch { - case ip.Is6(): - metadata.AddrType = C.AtypIPv6 - default: - metadata.AddrType = C.AtypIPv4 - } metadata.DstIP = ip } diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index 145a3f97ff..d7ffec5aee 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -18,6 +18,7 @@ type Base struct { iface string tp C.AdapterType udp bool + tfo bool rmark int id string prefer C.DNSPrefer @@ -56,16 +57,26 @@ func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di return nil, errors.New("no support") } +// DialContextWithDialer implements C.ProxyAdapter +func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + return nil, errors.New("no support") +} + // ListenPacketContext implements C.ProxyAdapter func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { return nil, errors.New("no support") } -// ListenPacketOnStreamConn implements C.ProxyAdapter -func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { +// ListenPacketWithDialer implements C.ProxyAdapter +func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { return nil, errors.New("no support") } +// SupportWithDialer implements C.ProxyAdapter +func (b *Base) SupportWithDialer() bool { + return false +} + // SupportUOT implements C.ProxyAdapter func (b *Base) SupportUOT() bool { return false @@ -76,6 +87,11 @@ func (b *Base) SupportUDP() bool { return b.udp } +// SupportTFO implements C.ProxyAdapter +func (b *Base) SupportTFO() bool { + return b.tfo +} + // MarshalJSON implements C.ProxyAdapter func (b *Base) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]string{ @@ -130,6 +146,7 @@ type BaseOption struct { Addr string Type C.AdapterType UDP bool + TFO bool Interface string RoutingMark int Prefer C.DNSPrefer @@ -141,6 +158,7 @@ func NewBase(opt BaseOption) *Base { addr: opt.Addr, tp: opt.Type, udp: opt.UDP, + tfo: opt.TFO, iface: opt.Interface, rmark: opt.RoutingMark, prefer: opt.Prefer, diff --git a/adapter/outbound/direct.go b/adapter/outbound/direct.go index fdbbcb62e6..cf1b2648e3 100644 --- a/adapter/outbound/direct.go +++ b/adapter/outbound/direct.go @@ -5,6 +5,7 @@ import ( "net" "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" ) @@ -14,7 +15,7 @@ type Direct struct { // DialContext implements C.ProxyAdapter func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { - opts = append(opts, dialer.WithDirect()) + opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) if err != nil { return nil, err @@ -25,8 +26,8 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ... // ListenPacketContext implements C.ProxyAdapter func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - opts = append(opts, dialer.WithDirect()) - pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...) + opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) + pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...) if err != nil { return nil, err } diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index ae4fd75b88..b734290af1 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -44,7 +44,9 @@ type HttpOption struct { func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { if h.tlsConfig != nil { cc := tls.Client(c, h.tlsConfig) - err := cc.Handshake() + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + err := cc.HandshakeContext(ctx) c = cc if err != nil { return nil, fmt.Errorf("%s connect error: %w", h.addr, err) @@ -59,13 +61,20 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { // DialContext implements C.ProxyAdapter func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { - c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...) + return h.DialContextWithDialer(ctx, dialer.NewDialer(h.Base.DialOptions(opts...)...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", h.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", h.addr, err) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = h.StreamConn(c, metadata) if err != nil { @@ -75,6 +84,11 @@ func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di return NewConn(c, h), nil } +// SupportWithDialer implements C.ProxyAdapter +func (h *Http) SupportWithDialer() bool { + return true +} + func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { addr := metadata.RemoteAddress() req := &http.Request{ diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index 490b14e1d3..a127641507 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -9,13 +9,14 @@ import ( "encoding/pem" "fmt" "net" + "net/netip" "os" "regexp" "strconv" "time" - "github.com/lucas-clemente/quic-go" - "github.com/lucas-clemente/quic-go/congestion" + "github.com/metacubex/quic-go" + "github.com/metacubex/quic-go/congestion" M "github.com/sagernet/sing/common/metadata" "github.com/Dreamacro/clash/component/dialer" @@ -30,15 +31,14 @@ import ( ) const ( - mbpsToBps = 125000 - minSpeedBPS = 16384 + mbpsToBps = 125000 DefaultStreamReceiveWindow = 15728640 // 15 MB/s DefaultConnectionReceiveWindow = 67108864 // 64 MB/s - DefaultMaxIncomingStreams = 1024 - DefaultALPN = "hysteria" - DefaultProtocol = "udp" + DefaultALPN = "hysteria" + DefaultProtocol = "udp" + DefaultHopInterval = 10 ) var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) @@ -52,11 +52,11 @@ type Hysteria struct { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { hdc := hyDialerWithContext{ ctx: context.Background(), - hyDialer: func() (net.PacketConn, error) { - return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...) + hyDialer: func(network string) (net.PacketConn, error) { + return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...) }, remoteAddr: func(addr string) (net.Addr, error) { - return resolveUDPAddrWithPrefer("udp", addr, h.prefer) + return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer) }, } @@ -71,11 +71,11 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts . func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { hdc := hyDialerWithContext{ ctx: context.Background(), - hyDialer: func() (net.PacketConn, error) { - return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...) + hyDialer: func(network string) (net.PacketConn, error) { + return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...) }, remoteAddr: func(addr string) (net.Addr, error) { - return resolveUDPAddrWithPrefer("udp", addr, h.prefer) + return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer) }, } udpConn, err := h.client.DialUDP(&hdc) @@ -89,7 +89,8 @@ type HysteriaOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` - Port int `proxy:"port"` + 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"` @@ -97,17 +98,19 @@ type HysteriaOption struct { Down string `proxy:"down"` DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash Auth string `proxy:"auth,omitempty"` - AuthString string `proxy:"auth_str,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"` + 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) { @@ -131,8 +134,9 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { Timeout: 8 * time.Second, }, } - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + ports := option.Ports + serverName := option.Server if option.SNI != "" { serverName = option.SNI @@ -182,7 +186,6 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { } else { tlsConfig.NextProtos = []string{DefaultALPN} } - quicConfig := &quic.Config{ InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), @@ -198,7 +201,11 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { if option.Protocol == "" { option.Protocol = DefaultProtocol } - if option.ReceiveWindowConn == 0 { + if option.HopInterval == 0 { + option.HopInterval = DefaultHopInterval + } + hopInterval := time.Duration(int64(option.HopInterval)) * time.Second + if option.ReceiveWindow == 0 { quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10 quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow } @@ -233,9 +240,9 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { down = uint64(option.DownSpeed * mbpsToBps) } client, err := core.NewClient( - addr, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { + addr, ports, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) - }, obfuscator, + }, obfuscator, hopInterval, option.FastOpen, ) if err != nil { return nil, fmt.Errorf("hysteria %s create error: %w", addr, err) @@ -246,6 +253,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { addr: addr, tp: C.Hysteria, udp: true, + tfo: option.FastOpen, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), @@ -314,13 +322,17 @@ func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { } type hyDialerWithContext struct { - hyDialer func() (net.PacketConn, error) + hyDialer func(network string) (net.PacketConn, error) ctx context.Context remoteAddr func(host string) (net.Addr, error) } -func (h *hyDialerWithContext) ListenPacket() (net.PacketConn, error) { - return h.hyDialer() +func (h *hyDialerWithContext) ListenPacket(rAddr net.Addr) (net.PacketConn, error) { + network := "udp" + if addrPort, err := netip.ParseAddrPort(rAddr.String()); err == nil { + network = dialer.ParseNetwork(network, addrPort.Addr()) + } + return h.hyDialer(network) } func (h *hyDialerWithContext) Context() context.Context { diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 6eeacf45fe..c318d26320 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -13,8 +13,9 @@ import ( obfs "github.com/Dreamacro/clash/transport/simple-obfs" "github.com/Dreamacro/clash/transport/socks5" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" - "github.com/sagernet/sing-shadowsocks" - "github.com/sagernet/sing-shadowsocks/shadowimpl" + + "github.com/metacubex/sing-shadowsocks" + "github.com/metacubex/sing-shadowsocks/shadowimpl" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/uot" @@ -83,13 +84,20 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e // DialContext implements C.ProxyAdapter func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { - c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) + return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", ss.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = ss.StreamConn(c, metadata) return NewConn(c, ss), err @@ -97,27 +105,36 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op // ListenPacketContext implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { + return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { if ss.option.UDPOverTCP { - tcpConn, err := ss.DialContext(ctx, metadata, opts...) + tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata) if err != nil { return nil, err } return newPacketConn(uot.NewClientConn(tcpConn), ss), nil } - pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) + addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer) if err != nil { return nil, err } - addr, err := resolveUDPAddrWithPrefer("udp", ss.addr, ss.prefer) + pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) if err != nil { - pc.Close() return nil, err } pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr}) return newPacketConn(pc, ss), nil } +// SupportWithDialer implements C.ProxyAdapter +func (ss *ShadowSocks) SupportWithDialer() bool { + return true +} + // ListenPacketOnStreamConn implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { if ss.option.UDPOverTCP { diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index 6b6b9a983b..e84de879a6 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -60,13 +60,20 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, // DialContext implements C.ProxyAdapter func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { - c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...) + return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", ssr.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = ssr.StreamConn(c, metadata) return NewConn(c, ssr), err @@ -74,14 +81,18 @@ func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, // ListenPacketContext implements C.ProxyAdapter func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...) + return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer) if err != nil { return nil, err } - addr, err := resolveUDPAddrWithPrefer("udp", ssr.addr, ssr.prefer) + pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) if err != nil { - pc.Close() return nil, err } @@ -90,6 +101,11 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil } +// SupportWithDialer implements C.ProxyAdapter +func (ssr *ShadowSocksR) SupportWithDialer() bool { + return true +} + func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { // SSR protocol compatibility // https://github.com/Dreamacro/clash/pull/2056 diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 0aadb1c8c2..1331b52632 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -78,13 +78,20 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d return NewConn(c, s), err } - c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...) + return s.DialContextWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", s.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", s.addr, err) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = s.StreamConn(c, metadata) return NewConn(c, s), err @@ -92,7 +99,12 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d // ListenPacketContext implements C.ProxyAdapter func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...) + return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) { + c, err := dialer.DialContext(ctx, "tcp", s.addr) if err != nil { return nil, err } @@ -108,10 +120,9 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o return newPacketConn(pc, s), nil } -// ListenPacketOnStreamConn implements C.ProxyAdapter -func (s *Snell) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { - pc := snell.PacketConn(c) - return newPacketConn(pc, s), nil +// SupportWithDialer implements C.ProxyAdapter +func (s *Snell) SupportWithDialer() bool { + return true } // SupportUOT implements C.ProxyAdapter diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index 43900b1edd..28d411802b 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -41,7 +41,9 @@ type Socks5Option struct { func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { if ss.tls { cc := tls.Client(c, ss.tlsConfig) - err := cc.Handshake() + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + err := cc.HandshakeContext(ctx) c = cc if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) @@ -63,13 +65,20 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) // DialContext implements C.ProxyAdapter func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { - c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) + return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", ss.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = ss.StreamConn(c, metadata) if err != nil { @@ -79,6 +88,11 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts .. return NewConn(c, ss), nil } +// SupportWithDialer implements C.ProxyAdapter +func (ss *Socks5) SupportWithDialer() bool { + return true +} + // ListenPacketContext implements C.ProxyAdapter func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) @@ -89,11 +103,15 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, if ss.tls { cc := tls.Client(c, ss.tlsConfig) - err = cc.Handshake() + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + err = cc.HandshakeContext(ctx) c = cc } - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) tcpKeepAlive(c) var user *socks5.User @@ -110,26 +128,13 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, return } - pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) - if err != nil { - return - } - - go func() { - io.Copy(io.Discard, c) - c.Close() - // A UDP association terminates when the TCP connection that the UDP - // ASSOCIATE request arrived on terminates. RFC1928 - pc.Close() - }() - // Support unspecified UDP bind address. bindUDPAddr := bindAddr.UDPAddr() if bindUDPAddr == nil { err = errors.New("invalid UDP bind address") return } else if bindUDPAddr.IP.IsUnspecified() { - serverAddr, err := resolveUDPAddr("udp", ss.Addr()) + serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr()) if err != nil { return nil, err } @@ -137,6 +142,19 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, bindUDPAddr.IP = serverAddr.IP } + pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...) + if err != nil { + return + } + + go func() { + io.Copy(io.Discard, c) + c.Close() + // A UDP association terminates when the TCP connection that the UDP + // ASSOCIATE request arrived on terminates. RFC1928 + pc.Close() + }() + return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil } diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 946a0101e2..698db59803 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -120,14 +120,20 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ... return NewConn(c, t), nil } + return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata) +} - c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) +// DialContextWithDialer implements C.ProxyAdapter +func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", t.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = t.StreamConn(c, metadata) if err != nil { @@ -147,18 +153,33 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } - defer safeConnClose(c, err) - } else { - c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.addr, err) - } - defer safeConnClose(c, err) - tcpKeepAlive(c) - c, err = t.plainStream(c) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + return nil, err } + + pc := t.instance.PacketConn(c) + return newPacketConn(pc, t), err + } + return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + c, err := dialer.DialContext(ctx, "tcp", t.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + } + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + tcpKeepAlive(c) + c, err = t.plainStream(c) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) @@ -170,6 +191,11 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, return newPacketConn(pc, t), err } +// SupportWithDialer implements C.ProxyAdapter +func (t *Trojan) SupportWithDialer() bool { + return true +} + // ListenPacketOnStreamConn implements C.ProxyAdapter func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { pc := t.instance.PacketConn(c) diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go new file mode 100644 index 0000000000..fa24ae39cb --- /dev/null +++ b/adapter/outbound/tuic.go @@ -0,0 +1,242 @@ +package outbound + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "encoding/pem" + "fmt" + "math" + "net" + "os" + "strconv" + "time" + + "github.com/metacubex/quic-go" + + "github.com/Dreamacro/clash/component/dialer" + tlsC "github.com/Dreamacro/clash/component/tls" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/tuic" +) + +type Tuic struct { + *Base + client *tuic.PoolClient +} + +type TuicOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Token string `proxy:"token"` + Ip string `proxy:"ip,omitempty"` + HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"` + ALPN []string `proxy:"alpn,omitempty"` + ReduceRtt bool `proxy:"reduce-rtt,omitempty"` + RequestTimeout int `proxy:"request-timeout,omitempty"` + UdpRelayMode string `proxy:"udp-relay-mode,omitempty"` + CongestionController string `proxy:"congestion-controller,omitempty"` + 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"` + 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"` +} + +// DialContext implements C.ProxyAdapter +func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) { + conn, err := t.client.DialContextWithDialer(ctx, metadata, dialer, t.dialWithDialer) + if err != nil { + return nil, err + } + return NewConn(conn, t), err +} + +// ListenPacketContext implements C.ProxyAdapter +func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { + return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer) + if err != nil { + return nil, err + } + return newPacketConn(pc, t), nil +} + +// SupportWithDialer implements C.ProxyAdapter +func (t *Tuic) SupportWithDialer() bool { + return true +} + +func (t *Tuic) dial(ctx context.Context, opts ...dialer.Option) (pc net.PacketConn, addr net.Addr, err error) { + return t.dialWithDialer(ctx, dialer.NewDialer(opts...)) +} + +func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) { + udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer) + if err != nil { + return nil, nil, err + } + addr = udpAddr + pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort()) + if err != nil { + return nil, nil, err + } + return +} + +func NewTuic(option TuicOption) (*Tuic, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + serverName := option.Server + + tlsConfig := &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: option.SkipCertVerify, + MinVersion: tls.VersionTLS13, + } + + var bs []byte + var err error + if len(option.CustomCA) > 0 { + bs, err = os.ReadFile(option.CustomCA) + if err != nil { + return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err) + } + } else if option.CustomCAString != "" { + bs = []byte(option.CustomCAString) + } + + if len(bs) > 0 { + block, _ := pem.Decode(bs) + if block == nil { + return nil, fmt.Errorf("CA cert is not PEM") + } + + fpBytes := sha256.Sum256(block.Bytes) + if len(option.Fingerprint) == 0 { + option.Fingerprint = hex.EncodeToString(fpBytes[:]) + } + } + + if len(option.Fingerprint) != 0 { + var err error + tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + if err != nil { + return nil, err + } + } else { + tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig) + } + + if len(option.ALPN) > 0 { + tlsConfig.NextProtos = option.ALPN + } else { + tlsConfig.NextProtos = []string{"h3"} + } + + if option.RequestTimeout == 0 { + option.RequestTimeout = 8000 + } + + if option.HeartbeatInterval <= 0 { + option.HeartbeatInterval = 10000 + } + + if option.UdpRelayMode != "quic" { + option.UdpRelayMode = "native" + } + + if option.MaxUdpRelayPacketSize == 0 { + option.MaxUdpRelayPacketSize = 1500 + } + + if option.MaxOpenStreams == 0 { + option.MaxOpenStreams = 100 + } + + // ensure server's incoming stream can handle correctly, increase to 1.1x + quicMaxOpenStreams := int64(option.MaxOpenStreams) + quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0)) + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), + MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), + InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), + MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), + MaxIncomingStreams: quicMaxOpenStreams, + MaxIncomingUniStreams: quicMaxOpenStreams, + KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond, + DisablePathMTUDiscovery: option.DisableMTUDiscovery, + EnableDatagrams: true, + } + if option.ReceiveWindowConn == 0 { + quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 + quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow + } + if option.ReceiveWindow == 0 { + quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 + quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow + } + + if len(option.Ip) > 0 { + addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port)) + } + host := option.Server + if option.DisableSni { + host = "" + tlsConfig.ServerName = "" + } + tkn := tuic.GenTKN(option.Token) + + t := &Tuic{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.Tuic, + udp: true, + tfo: option.FastOpen, + iface: option.Interface, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + } + // to avoid tuic's "too many open streams", decrease to 0.9x + clientMaxOpenStreams := int64(option.MaxOpenStreams) + clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0)) + if clientMaxOpenStreams < 1 { + clientMaxOpenStreams = 1 + } + clientOption := &tuic.ClientOption{ + TlsConfig: tlsConfig, + QuicConfig: quicConfig, + Host: host, + Token: tkn, + UdpRelayMode: option.UdpRelayMode, + CongestionController: option.CongestionController, + ReduceRtt: option.ReduceRtt, + RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond, + MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, + FastOpen: option.FastOpen, + MaxOpenStreams: clientMaxOpenStreams, + } + + t.client = tuic.NewPoolClient(clientOption) + + return t, nil +} diff --git a/adapter/outbound/util.go b/adapter/outbound/util.go index d521913128..68d6b3556c 100644 --- a/adapter/outbound/util.go +++ b/adapter/outbound/util.go @@ -2,6 +2,7 @@ package outbound import ( "bytes" + "context" "crypto/tls" xtls "github.com/xtls/go" "net" @@ -44,10 +45,11 @@ func getClientXSessionCache() xtls.ClientSessionCache { func serializesSocksAddr(metadata *C.Metadata) []byte { var buf [][]byte - aType := uint8(metadata.AddrType) + addrType := metadata.AddrType() + aType := uint8(addrType) p, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port := []byte{uint8(p >> 8), uint8(p & 0xff)} - switch metadata.AddrType { + switch addrType { case socks5.AtypDomainName: lenM := uint8(len(metadata.Host)) host := []byte(metadata.Host) @@ -62,34 +64,34 @@ func serializesSocksAddr(metadata *C.Metadata) []byte { return bytes.Join(buf, nil) } -func resolveUDPAddr(network, address string) (*net.UDPAddr, error) { +func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } - ip, err := resolver.ResolveProxyServerHost(host) + ip, err := resolver.ResolveProxyServerHost(ctx, host) if err != nil { return nil, err } return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) } -func resolveUDPAddrWithPrefer(network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) { +func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } var ip netip.Addr + var fallback netip.Addr switch prefer { case C.IPv4Only: - ip, err = resolver.ResolveIPv4ProxyServerHost(host) + ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host) case C.IPv6Only: - ip, err = resolver.ResolveIPv6ProxyServerHost(host) + ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host) case C.IPv6Prefer: var ips []netip.Addr - ips, err = resolver.ResolveAllIPProxyServerHost(host) - var fallback netip.Addr + ips, err = resolver.LookupIPProxyServerHost(ctx, host) if err == nil { for _, addr := range ips { if addr.Is6() { @@ -101,13 +103,11 @@ func resolveUDPAddrWithPrefer(network, address string, prefer C.DNSPrefer) (*net } } } - ip = fallback } default: // C.IPv4Prefer, C.DualStack and other var ips []netip.Addr - ips, err = resolver.ResolveAllIPProxyServerHost(host) - var fallback netip.Addr + ips, err = resolver.LookupIPProxyServerHost(ctx, host) if err == nil { for _, addr := range ips { if addr.Is4() { @@ -120,12 +120,13 @@ func resolveUDPAddrWithPrefer(network, address string, prefer C.DNSPrefer) (*net } } - if !ip.IsValid() && fallback.IsValid() { - ip = fallback - } } } + if !ip.IsValid() && fallback.IsValid() { + ip = fallback + } + if err != nil { return nil, err } @@ -133,7 +134,7 @@ func resolveUDPAddrWithPrefer(network, address string, prefer C.DNSPrefer) (*net } func safeConnClose(c net.Conn, err error) { - if err != nil { + if err != nil && c != nil { _ = c.Close() } } diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 4811de0fe6..0662791296 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -6,18 +6,19 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/Dreamacro/clash/common/convert" - tlsC "github.com/Dreamacro/clash/component/tls" "io" "net" "net/http" "strconv" "sync" + "github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" + tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/gun" + "github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vmess" ) @@ -207,7 +208,9 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d if err != nil { return nil, err } - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) if err != nil { @@ -216,13 +219,19 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d return NewConn(c, v), nil } + return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata) +} - c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) +// DialContextWithDialer implements C.ProxyAdapter +func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", v.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = v.StreamConn(c, metadata) return NewConn(c, v), err @@ -232,7 +241,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { // vless use stream-oriented udp with a special address, so we needs a net.UDPAddr if !metadata.Resolved() { - ip, err := resolver.ResolveIP(metadata.Host) + ip, err := resolver.ResolveIP(ctx, metadata.Host) if err != nil { return nil, errors.New("can't resolve ip") } @@ -246,19 +255,41 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o if err != nil { return nil, err } - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) - } else { - c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + return nil, fmt.Errorf("new vless client error: %v", err) } - tcpKeepAlive(c) - defer safeConnClose(c, err) - c, err = v.StreamConn(c, metadata) + return v.ListenPacketOnStreamConn(c, metadata) + } + return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + // vless use stream-oriented udp with a special address, so we needs 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 + } + c, err := dialer.DialContext(ctx, "tcp", v.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } + tcpKeepAlive(c) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + c, err = v.StreamConn(c, metadata) if err != nil { return nil, fmt.Errorf("new vless client error: %v", err) @@ -267,6 +298,11 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o return v.ListenPacketOnStreamConn(c, metadata) } +// SupportWithDialer implements C.ProxyAdapter +func (v *Vless) SupportWithDialer() bool { + return true +} + // ListenPacketOnStreamConn implements C.ProxyAdapter func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil @@ -280,16 +316,16 @@ func (v *Vless) SupportUOT() bool { func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { var addrType byte var addr []byte - switch metadata.AddrType { - case C.AtypIPv4: + switch metadata.AddrType() { + case socks5.AtypIPv4: addrType = vless.AtypIPv4 addr = make([]byte, net.IPv4len) copy(addr[:], metadata.DstIP.AsSlice()) - case C.AtypIPv6: + case socks5.AtypIPv6: addrType = vless.AtypIPv6 addr = make([]byte, net.IPv6len) copy(addr[:], metadata.DstIP.AsSlice()) - case C.AtypDomainName: + case socks5.AtypDomainName: addrType = vless.AtypDomainName addr = make([]byte, len(metadata.Host)+1) addr[0] = byte(len(metadata.Host)) diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index a66ee40f31..8d018caa5b 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -18,10 +18,13 @@ import ( C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/gun" clashVMess "github.com/Dreamacro/clash/transport/vmess" + "github.com/sagernet/sing-vmess/packetaddr" M "github.com/sagernet/sing/common/metadata" ) +var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address") + type Vmess struct { *Base client *vmess.Client @@ -218,7 +221,9 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d if err != nil { return nil, err } - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) if err != nil { @@ -227,13 +232,19 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d return NewConn(c, v), nil } + return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata) +} - c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) +// DialContextWithDialer implements C.ProxyAdapter +func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + c, err := dialer.DialContext(ctx, "tcp", v.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } tcpKeepAlive(c) - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) c, err = v.StreamConn(c, metadata) return NewConn(c, v), err @@ -243,7 +254,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr if !metadata.Resolved() { - ip, err := resolver.ResolveIP(metadata.Host) + ip, err := resolver.ResolveIP(ctx, metadata.Host) if err != nil { return nil, errors.New("can't resolve ip") } @@ -264,34 +275,56 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o if err != nil { return nil, err } - defer safeConnClose(c, err) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) if v.option.XUDP { c, err = v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) } else { c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) } - } else { - c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + return nil, fmt.Errorf("new vmess client error: %v", err) } - tcpKeepAlive(c) - defer safeConnClose(c, err) + return v.ListenPacketOnStreamConn(c, metadata) + } + c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) - c, err = v.StreamConn(c, metadata) + c, err = v.StreamConn(c, metadata) + return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata) +} + +// ListenPacketWithDialer implements C.ProxyAdapter +func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + // vmess use stream-oriented udp with a special address, so we needs 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 } + c, err := dialer.DialContext(ctx, "tcp", v.addr) if err != nil { return nil, fmt.Errorf("new vmess client error: %v", err) } - if v.option.PacketAddr { - return newPacketConn(&threadSafePacketConn{PacketConn: packetaddr.NewBindConn(c)}, v), nil - } else if pc, ok := c.(net.PacketConn); ok { - return newPacketConn(&threadSafePacketConn{PacketConn: pc}, v), nil - } - return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil + return v.ListenPacketOnStreamConn(c, metadata) +} + +// SupportWithDialer implements C.ProxyAdapter +func (v *Vmess) SupportWithDialer() bool { + return true } // ListenPacketOnStreamConn implements C.ProxyAdapter @@ -408,7 +441,14 @@ type vmessPacketConn struct { access sync.Mutex } +// WriteTo implments C.PacketConn.WriteTo +// Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not. func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + allowedAddr := uc.rAddr.(*net.UDPAddr) + destAddr := addr.(*net.UDPAddr) + if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) { + return 0, ErrUDPRemoteAddrMismatch + } uc.access.Lock() defer uc.access.Unlock() return uc.Conn.Write(b) diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go new file mode 100644 index 0000000000..d1a5ea6edf --- /dev/null +++ b/adapter/outbound/wireguard.go @@ -0,0 +1,258 @@ +package outbound + +import ( + "context" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net" + "net/netip" + "runtime" + "strconv" + "strings" + "sync" + + CN "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/sing" + + wireguard "github.com/metacubex/sing-wireguard" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/debug" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/wireguard-go/device" +) + +type WireGuard struct { + *Base + bind *wireguard.ClientBind + device *device.Device + tunDevice wireguard.Device + dialer *wgDialer + startOnce sync.Once + startErr error +} + +type WireGuardOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Ip string `proxy:"ip,omitempty"` + Ipv6 string `proxy:"ipv6,omitempty"` + PrivateKey string `proxy:"private-key"` + PublicKey string `proxy:"public-key"` + PreSharedKey string `proxy:"pre-shared-key,omitempty"` + Reserved []uint8 `proxy:"reserved,omitempty"` + Workers int `proxy:"workers,omitempty"` + MTU int `proxy:"mtu,omitempty"` + UDP bool `proxy:"udp,omitempty"` + PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` +} + +type wgDialer struct { + options []dialer.Option +} + +func (d *wgDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return dialer.DialContext(ctx, network, destination.String(), d.options...) +} + +func (d *wgDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", destination.Addr), "", d.options...) +} + +func NewWireGuard(option WireGuardOption) (*WireGuard, error) { + outbound := &WireGuard{ + Base: &Base{ + name: option.Name, + addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), + tp: C.WireGuard, + udp: option.UDP, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + dialer: &wgDialer{}, + } + runtime.SetFinalizer(outbound, closeWireGuard) + + var reserved [3]uint8 + if len(option.Reserved) > 0 { + if len(option.Reserved) != 3 { + return nil, E.New("invalid reserved value, required 3 bytes, got ", len(option.Reserved)) + } + reserved[0] = uint8(option.Reserved[0]) + reserved[1] = uint8(option.Reserved[1]) + reserved[2] = uint8(option.Reserved[2]) + } + peerAddr := M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)) + outbound.bind = wireguard.NewClientBind(context.Background(), outbound.dialer, peerAddr, reserved) + localPrefixes := make([]netip.Prefix, 0, 2) + if len(option.Ip) > 0 { + if !strings.Contains(option.Ip, "/") { + option.Ip = option.Ip + "/32" + } + if prefix, err := netip.ParsePrefix(option.Ip); err == nil { + localPrefixes = append(localPrefixes, prefix) + } else { + return nil, E.Cause(err, "ip address parse error") + } + } + if len(option.Ipv6) > 0 { + if !strings.Contains(option.Ipv6, "/") { + option.Ipv6 = option.Ipv6 + "/128" + } + if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil { + localPrefixes = append(localPrefixes, prefix) + } else { + return nil, E.Cause(err, "ipv6 address parse error") + } + } + if len(localPrefixes) == 0 { + return nil, E.New("missing local address") + } + var privateKey, peerPublicKey, preSharedKey string + { + bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey) + if err != nil { + return nil, E.Cause(err, "decode private key") + } + privateKey = hex.EncodeToString(bytes) + } + { + bytes, err := base64.StdEncoding.DecodeString(option.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode peer public key") + } + peerPublicKey = hex.EncodeToString(bytes) + } + if option.PreSharedKey != "" { + bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey) + if err != nil { + return nil, E.Cause(err, "decode pre shared key") + } + preSharedKey = hex.EncodeToString(bytes) + } + ipcConf := "private_key=" + privateKey + ipcConf += "\npublic_key=" + peerPublicKey + ipcConf += "\nendpoint=" + peerAddr.String() + if preSharedKey != "" { + ipcConf += "\npreshared_key=" + preSharedKey + } + var has4, has6 bool + for _, address := range localPrefixes { + if address.Addr().Is4() { + has4 = true + } else { + has6 = true + } + } + if has4 { + ipcConf += "\nallowed_ip=0.0.0.0/0" + } + if has6 { + ipcConf += "\nallowed_ip=::/0" + } + if option.PersistentKeepalive != 0 { + ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive) + } + mtu := option.MTU + if mtu == 0 { + mtu = 1408 + } + var err error + outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu)) + if err != nil { + return nil, E.Cause(err, "create WireGuard device") + } + outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{ + Verbosef: func(format string, args ...interface{}) { + sing.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + }, + Errorf: func(format string, args ...interface{}) { + sing.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + }, + }, option.Workers) + if debug.Enabled { + sing.Logger.Trace("created wireguard ipc conf: \n", ipcConf) + } + err = outbound.device.IpcSet(ipcConf) + if err != nil { + return nil, E.Cause(err, "setup wireguard") + } + //err = outbound.tunDevice.Start() + return outbound, nil +} + +func closeWireGuard(w *WireGuard) { + if w.device != nil { + w.device.Close() + } + _ = common.Close(w.tunDevice) +} + +func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + w.dialer.options = opts + var conn net.Conn + w.startOnce.Do(func() { + w.startErr = w.tunDevice.Start() + }) + if w.startErr != nil { + return nil, w.startErr + } + if !metadata.Resolved() { + var addrs []netip.Addr + addrs, err = resolver.LookupIP(ctx, metadata.Host) + if err != nil { + return nil, err + } + conn, err = N.DialSerial(ctx, w.tunDevice, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()), addrs) + } else { + port, _ := strconv.Atoi(metadata.DstPort) + conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port))) + } + if err != nil { + return nil, err + } + if conn == nil { + return nil, E.New("conn is nil") + } + return NewConn(CN.NewRefConn(conn, w), w), nil +} + +func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { + w.dialer.options = opts + var pc net.PacketConn + w.startOnce.Do(func() { + w.startErr = w.tunDevice.Start() + }) + if w.startErr != nil { + return nil, w.startErr + } + if err != nil { + return nil, err + } + if !metadata.Resolved() { + ip, err := resolver.ResolveIP(ctx, metadata.Host) + if err != nil { + return nil, errors.New("can't resolve ip") + } + metadata.DstIP = ip + } + port, _ := strconv.Atoi(metadata.DstPort) + pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port))) + if err != nil { + return nil, err + } + if pc == nil { + return nil, E.New("packetConn is nil") + } + return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil +} diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go index a433d54da9..0a4dab4133 100644 --- a/adapter/outboundgroup/fallback.go +++ b/adapter/outboundgroup/fallback.go @@ -131,6 +131,7 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) RoutingMark: option.RoutingMark, }, option.Filter, + option.ExcludeFilter, providers, }), disableUDP: option.DisableUDP, diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index fbe887e3ab..426a12824b 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -18,23 +18,30 @@ import ( type GroupBase struct { *outbound.Base - filterRegs []*regexp2.Regexp - providers []provider.ProxyProvider - failedTestMux sync.Mutex - failedTimes int - failedTime time.Time - failedTesting *atomic.Bool - proxies [][]C.Proxy - versions []atomic.Uint32 + filterRegs []*regexp2.Regexp + excludeFilterReg *regexp2.Regexp + providers []provider.ProxyProvider + failedTestMux sync.Mutex + failedTimes int + failedTime time.Time + failedTesting *atomic.Bool + proxies [][]C.Proxy + versions []atomic.Uint32 } type GroupBaseOption struct { outbound.BaseOption - filter string - providers []provider.ProxyProvider + filter string + excludeFilter string + providers []provider.ProxyProvider } func NewGroupBase(opt GroupBaseOption) *GroupBase { + var excludeFilterReg *regexp2.Regexp + if opt.excludeFilter != "" { + excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, 0) + } + var filterRegs []*regexp2.Regexp if opt.filter != "" { for _, filter := range strings.Split(opt.filter, "`") { @@ -44,10 +51,11 @@ func NewGroupBase(opt GroupBaseOption) *GroupBase { } gb := &GroupBase{ - Base: outbound.NewBase(opt.BaseOption), - filterRegs: filterRegs, - providers: opt.providers, - failedTesting: atomic.NewBool(false), + Base: outbound.NewBase(opt.BaseOption), + filterRegs: filterRegs, + excludeFilterReg: excludeFilterReg, + providers: opt.providers, + failedTesting: atomic.NewBool(false), } gb.proxies = make([][]C.Proxy, len(opt.providers)) @@ -63,59 +71,54 @@ func (gb *GroupBase) Touch() { } func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { + var proxies []C.Proxy if len(gb.filterRegs) == 0 { - var proxies []C.Proxy for _, pd := range gb.providers { if touch { pd.Touch() } proxies = append(proxies, pd.Proxies()...) } - if len(proxies) == 0 { - return append(proxies, tunnel.Proxies()["COMPATIBLE"]) - } - return proxies - } - - for i, pd := range gb.providers { - if touch { - pd.Touch() - } + } else { + for i, pd := range gb.providers { + if touch { + pd.Touch() + } - if pd.VehicleType() == types.Compatible { - gb.versions[i].Store(pd.Version()) - gb.proxies[i] = pd.Proxies() - continue - } + if pd.VehicleType() == types.Compatible { + gb.versions[i].Store(pd.Version()) + gb.proxies[i] = pd.Proxies() + continue + } - version := gb.versions[i].Load() - if version != pd.Version() && gb.versions[i].CompareAndSwap(version, pd.Version()) { - var ( - proxies []C.Proxy - newProxies []C.Proxy - ) - - proxies = pd.Proxies() - proxiesSet := map[string]struct{}{} - for _, filterReg := range gb.filterRegs { - for _, p := range proxies { - name := p.Name() - if mat, _ := filterReg.FindStringMatch(name); mat != nil { - if _, ok := proxiesSet[name]; !ok { - proxiesSet[name] = struct{}{} - newProxies = append(newProxies, p) + version := gb.versions[i].Load() + if version != pd.Version() && gb.versions[i].CompareAndSwap(version, pd.Version()) { + var ( + proxies []C.Proxy + newProxies []C.Proxy + ) + + proxies = pd.Proxies() + proxiesSet := map[string]struct{}{} + for _, filterReg := range gb.filterRegs { + for _, p := range proxies { + name := p.Name() + if mat, _ := filterReg.FindStringMatch(name); mat != nil { + if _, ok := proxiesSet[name]; !ok { + proxiesSet[name] = struct{}{} + newProxies = append(newProxies, p) + } } } } - } - gb.proxies[i] = newProxies + gb.proxies[i] = newProxies + } } - } - var proxies []C.Proxy - for _, p := range gb.proxies { - proxies = append(proxies, p...) + for _, p := range gb.proxies { + proxies = append(proxies, p...) + } } if len(proxies) == 0 { @@ -146,6 +149,18 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { proxies = newProxies } + if gb.excludeFilterReg != nil { + var newProxies []C.Proxy + for _, p := range proxies { + name := p.Name() + if mat, _ := gb.excludeFilterReg.FindStringMatch(name); mat != nil { + continue + } + newProxies = append(newProxies, p) + } + proxies = newProxies + } + return proxies } diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 87d7de7b71..7f87545188 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -157,7 +157,7 @@ func strategyConsistentHashing() strategyFn { func strategyStickySessions() strategyFn { ttl := time.Minute * 10 maxRetry := 5 - lruCache := cache.NewLRUCache[uint64, int]( + lruCache := cache.New[uint64, int]( cache.WithAge[uint64, int](int64(ttl.Seconds())), cache.WithSize[uint64, int](1000)) return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { @@ -228,6 +228,7 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide RoutingMark: option.RoutingMark, }, option.Filter, + option.ExcludeFilter, providers, }), strategyFn: strategyFn, diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index b808079b9a..53a82a60d2 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -21,15 +21,16 @@ var ( type GroupCommonOption struct { outbound.BasicOption - Name string `group:"name"` - Type string `group:"type"` - Proxies []string `group:"proxies,omitempty"` - Use []string `group:"use,omitempty"` - URL string `group:"url,omitempty"` - Interval int `group:"interval,omitempty"` - Lazy bool `group:"lazy,omitempty"` - DisableUDP bool `group:"disable-udp,omitempty"` - Filter string `group:"filter,omitempty"` + Name string `group:"name"` + Type string `group:"type"` + Proxies []string `group:"proxies,omitempty"` + Use []string `group:"use,omitempty"` + URL string `group:"url,omitempty"` + Interval int `group:"interval,omitempty"` + Lazy bool `group:"lazy,omitempty"` + DisableUDP bool `group:"disable-udp,omitempty"` + Filter string `group:"filter,omitempty"` + ExcludeFilter string `group:"exclude-filter,omitempty"` } func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go index 729f413788..43ef81c967 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -3,9 +3,12 @@ package outboundgroup import ( "context" "encoding/json" - "fmt" + "net" + "net/netip" + "strings" "github.com/Dreamacro/clash/adapter/outbound" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" @@ -15,6 +18,36 @@ type Relay struct { *GroupBase } +type proxyDialer struct { + proxy C.Proxy + dialer C.Dialer +} + +func (p proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + currentMeta, err := addrToMetadata(address) + if err != nil { + return nil, err + } + if strings.Contains(network, "udp") { // should not support this operation + currentMeta.NetWork = C.UDP + pc, err := p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta) + if err != nil { + return nil, err + } + return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil + } + return p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta) +} + +func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { + currentMeta, err := addrToMetadata(rAddrPort.String()) + if err != nil { + return nil, err + } + currentMeta.NetWork = C.UDP + return p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta) +} + // DialContext implements C.ProxyAdapter func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { proxies, chainProxies := r.proxies(metadata, true) @@ -25,38 +58,20 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d case 1: return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) } - - first := proxies[0] - last := proxies[len(proxies)-1] - - c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) - } - tcpKeepAlive(c) - - var currentMeta *C.Metadata - for _, proxy := range proxies[1:] { - currentMeta, err = addrToMetadata(proxy.Addr()) - if err != nil { - return nil, err - } - - c, err = first.StreamConn(c, currentMeta) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + var d C.Dialer + d = dialer.NewDialer(r.Base.DialOptions(opts...)...) + for _, proxy := range proxies[:len(proxies)-1] { + d = proxyDialer{ + proxy: proxy, + dialer: d, } - - first = proxy } - - c, err = last.StreamConn(c, metadata) + last := proxies[len(proxies)-1] + conn, err := last.DialContextWithDialer(ctx, d, metadata) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err) + return nil, err } - conn := outbound.NewConn(c, last) - for i := len(chainProxies) - 2; i >= 0; i-- { conn.AppendToChains(chainProxies[i]) } @@ -77,39 +92,18 @@ func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...) } - first := proxies[0] - last := proxies[len(proxies)-1] - - c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) - } - tcpKeepAlive(c) - - var currentMeta *C.Metadata - for _, proxy := range proxies[1:] { - currentMeta, err = addrToMetadata(proxy.Addr()) - if err != nil { - return nil, err - } - - c, err = first.StreamConn(c, currentMeta) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + var d C.Dialer + d = dialer.NewDialer(r.Base.DialOptions(opts...)...) + for _, proxy := range proxies[:len(proxies)-1] { + d = proxyDialer{ + proxy: proxy, + dialer: d, } - - first = proxy - } - - c, err = last.StreamConn(c, metadata) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err) } - - var pc C.PacketConn - pc, err = last.ListenPacketOnStreamConn(c, metadata) + last := proxies[len(proxies)-1] + pc, err := last.ListenPacketWithDialer(ctx, d, metadata) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + return nil, err } for i := len(chainProxies) - 2; i >= 0; i-- { @@ -127,8 +121,19 @@ func (r *Relay) SupportUDP() bool { if len(proxies) == 0 { // C.Direct return true } - last := proxies[len(proxies)-1] - return last.SupportUDP() && last.SupportUOT() + for i := len(proxies) - 1; i >= 0; i-- { + proxy := proxies[i] + if !proxy.SupportUDP() { + return false + } + if proxy.SupportUOT() { + return true + } + if !proxy.SupportWithDialer() { + return false + } + } + return true } // MarshalJSON implements C.ProxyAdapter @@ -185,6 +190,7 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re RoutingMark: option.RoutingMark, }, "", + "", providers, }), } diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go index dcf0770731..71ebacce20 100644 --- a/adapter/outboundgroup/selector.go +++ b/adapter/outboundgroup/selector.go @@ -99,6 +99,7 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) RoutingMark: option.RoutingMark, }, option.Filter, + option.ExcludeFilter, providers, }), selected: "COMPATIBLE", diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go index 1e69652c9d..55c1cc7c4b 100644 --- a/adapter/outboundgroup/urltest.go +++ b/adapter/outboundgroup/urltest.go @@ -143,6 +143,7 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o }, option.Filter, + option.ExcludeFilter, providers, }), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), diff --git a/adapter/outboundgroup/util.go b/adapter/outboundgroup/util.go index e1700d9423..578011f8c8 100644 --- a/adapter/outboundgroup/util.go +++ b/adapter/outboundgroup/util.go @@ -16,32 +16,19 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) { return } - ip, err := netip.ParseAddr(host) - if err != nil { + if ip, err := netip.ParseAddr(host); err != nil { addr = &C.Metadata{ - AddrType: C.AtypDomainName, - Host: host, - DstIP: netip.Addr{}, - DstPort: port, + Host: host, + DstPort: port, } - err = nil - return - } else if ip.Is4() { + } else { addr = &C.Metadata{ - AddrType: C.AtypIPv4, - Host: "", - DstIP: ip, - DstPort: port, + Host: "", + DstIP: ip.Unmap(), + DstPort: port, } - return } - addr = &C.Metadata{ - AddrType: C.AtypIPv6, - Host: "", - DstIP: ip, - DstPort: port, - } return } diff --git a/adapter/parser.go b/adapter/parser.go index b68e81ab71..86fe96f96f 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -2,14 +2,13 @@ package adapter import ( "fmt" - "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/common/structure" C "github.com/Dreamacro/clash/constant" ) func ParseProxy(mapping map[string]any) (C.Proxy, error) { - decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true}) + decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) proxyType, existType := mapping["type"].(string) if !existType { return nil, fmt.Errorf("missing type") @@ -88,6 +87,20 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy, err = outbound.NewHysteria(*hyOption) + case "wireguard": + wgOption := &outbound.WireGuardOption{} + err = decoder.Decode(mapping, wgOption) + if err != nil { + break + } + proxy, err = outbound.NewWireGuard(*wgOption) + case "tuic": + tuicOption := &outbound.TuicOption{} + err = decoder.Decode(mapping, tuicOption) + if err != nil { + break + } + proxy, err = outbound.NewTuic(*tuicOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index ebacf29134..51f536fe39 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -1,21 +1,24 @@ package provider import ( + "context" "encoding/json" "errors" "fmt" - "github.com/Dreamacro/clash/common/convert" - "github.com/Dreamacro/clash/component/resource" "github.com/dlclark/regexp2" + "gopkg.in/yaml.v3" + "net/http" "runtime" "strings" "time" "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/common/convert" + clashHttp "github.com/Dreamacro/clash/component/http" + "github.com/Dreamacro/clash/component/resource" C "github.com/Dreamacro/clash/constant" types "github.com/Dreamacro/clash/constant/provider" - - "gopkg.in/yaml.v3" + "github.com/Dreamacro/clash/log" ) const ( @@ -33,18 +36,20 @@ type ProxySetProvider struct { type proxySetProvider struct { *resource.Fetcher[[]C.Proxy] - proxies []C.Proxy - healthCheck *HealthCheck - version uint32 + proxies []C.Proxy + healthCheck *HealthCheck + version uint32 + subscriptionInfo *SubscriptionInfo } func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]any{ - "name": pp.Name(), - "type": pp.Type().String(), - "vehicleType": pp.VehicleType().String(), - "proxies": pp.Proxies(), - "updatedAt": pp.UpdatedAt, + "name": pp.Name(), + "type": pp.Type().String(), + "vehicleType": pp.VehicleType().String(), + "proxies": pp.Proxies(), + "updatedAt": pp.UpdatedAt, + "subscriptionInfo": pp.subscriptionInfo, }) } @@ -97,6 +102,40 @@ func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { } } +func (pp *proxySetProvider) getSubscriptionInfo() { + if pp.VehicleType() != types.HTTP { + return + } + go func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), + http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil) + if err != nil { + return + } + defer resp.Body.Close() + + userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo")) + if userInfoStr == "" { + resp2, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), + http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil) + if err != nil { + return + } + defer resp2.Body.Close() + userInfoStr = strings.TrimSpace(resp2.Header.Get("subscription-userinfo")) + if userInfoStr == "" { + return + } + } + pp.subscriptionInfo, err = NewSubscriptionInfo(userInfoStr) + if err != nil { + log.Warnln("[Provider] get subscription-userinfo: %e", err) + } + }() +} + func stopProxyProvider(pd *ProxySetProvider) { pd.healthCheck.close() _ = pd.Fetcher.Destroy() @@ -128,6 +167,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, filterRegs, excludeFilterReg), proxiesOnUpdate(pd)) pd.Fetcher = fetcher + pd.getSubscriptionInfo() wrapper := &ProxySetProvider{pd} runtime.SetFinalizer(wrapper, stopProxyProvider) return wrapper, nil @@ -218,6 +258,7 @@ func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) { return func(elm []C.Proxy) { pd.setProxies(elm) pd.version += 1 + pd.getSubscriptionInfo() } } diff --git a/adapter/provider/subscription_info.go b/adapter/provider/subscription_info.go new file mode 100644 index 0000000000..fc6992e2d0 --- /dev/null +++ b/adapter/provider/subscription_info.go @@ -0,0 +1,57 @@ +package provider + +import ( + "github.com/dlclark/regexp2" + "strconv" + "strings" +) + +type SubscriptionInfo struct { + Upload int64 + Download int64 + Total int64 + Expire int64 +} + +func NewSubscriptionInfo(str string) (si *SubscriptionInfo, err error) { + si = &SubscriptionInfo{} + str = strings.ToLower(str) + reTraffic := regexp2.MustCompile("upload=(\\d+); download=(\\d+); total=(\\d+)", 0) + reExpire := regexp2.MustCompile("expire=(\\d+)", 0) + + match, err := reTraffic.FindStringMatch(str) + if err != nil || match == nil { + return nil, err + } + group := match.Groups() + si.Upload, err = str2uint64(group[1].String()) + if err != nil { + return nil, err + } + + si.Download, err = str2uint64(group[2].String()) + if err != nil { + return nil, err + } + + si.Total, err = str2uint64(group[3].String()) + if err != nil { + return nil, err + } + + match, _ = reExpire.FindStringMatch(str) + if match != nil { + group = match.Groups() + si.Expire, err = str2uint64(group[1].String()) + if err != nil { + return nil, err + } + } + + return +} + +func str2uint64(str string) (int64, error) { + i, err := strconv.ParseInt(str, 10, 64) + return i, err +} diff --git a/common/cache/cache.go b/common/cache/cache.go deleted file mode 100644 index b87392b430..0000000000 --- a/common/cache/cache.go +++ /dev/null @@ -1,106 +0,0 @@ -package cache - -import ( - "runtime" - "sync" - "time" -) - -// Cache store element with a expired time -type Cache[K comparable, V any] struct { - *cache[K, V] -} - -type cache[K comparable, V any] struct { - mapping sync.Map - janitor *janitor[K, V] -} - -type element[V any] struct { - Expired time.Time - Payload V -} - -// Put element in Cache with its ttl -func (c *cache[K, V]) Put(key K, payload V, ttl time.Duration) { - c.mapping.Store(key, &element[V]{ - Payload: payload, - Expired: time.Now().Add(ttl), - }) -} - -// Get element in Cache, and drop when it expired -func (c *cache[K, V]) Get(key K) V { - item, exist := c.mapping.Load(key) - if !exist { - return getZero[V]() - } - elm := item.(*element[V]) - // expired - if time.Since(elm.Expired) > 0 { - c.mapping.Delete(key) - return getZero[V]() - } - return elm.Payload -} - -// GetWithExpire element in Cache with Expire Time -func (c *cache[K, V]) GetWithExpire(key K) (payload V, expired time.Time) { - item, exist := c.mapping.Load(key) - if !exist { - return - } - elm := item.(*element[V]) - // expired - if time.Since(elm.Expired) > 0 { - c.mapping.Delete(key) - return - } - return elm.Payload, elm.Expired -} - -func (c *cache[K, V]) cleanup() { - c.mapping.Range(func(k, v any) bool { - key := k.(string) - elm := v.(*element[V]) - if time.Since(elm.Expired) > 0 { - c.mapping.Delete(key) - } - return true - }) -} - -type janitor[K comparable, V any] struct { - interval time.Duration - stop chan struct{} -} - -func (j *janitor[K, V]) process(c *cache[K, V]) { - ticker := time.NewTicker(j.interval) - for { - select { - case <-ticker.C: - c.cleanup() - case <-j.stop: - ticker.Stop() - return - } - } -} - -func stopJanitor[K comparable, V any](c *Cache[K, V]) { - c.janitor.stop <- struct{}{} -} - -// New return *Cache -func New[K comparable, V any](interval time.Duration) *Cache[K, V] { - j := &janitor[K, V]{ - interval: interval, - stop: make(chan struct{}), - } - c := &cache[K, V]{janitor: j} - go j.process(c) - C := &Cache[K, V]{c} - runtime.SetFinalizer(C, stopJanitor[K, V]) - return C -} diff --git a/common/cache/cache_test.go b/common/cache/cache_test.go deleted file mode 100644 index 0945d905e1..0000000000 --- a/common/cache/cache_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package cache - -import ( - "runtime" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestCache_Basic(t *testing.T) { - interval := 200 * time.Millisecond - ttl := 20 * time.Millisecond - c := New[string, int](interval) - c.Put("int", 1, ttl) - - d := New[string, string](interval) - d.Put("string", "a", ttl) - - i := c.Get("int") - assert.Equal(t, i, 1, "should recv 1") - - s := d.Get("string") - assert.Equal(t, s, "a", "should recv 'a'") -} - -func TestCache_TTL(t *testing.T) { - interval := 200 * time.Millisecond - ttl := 20 * time.Millisecond - now := time.Now() - c := New[string, int](interval) - c.Put("int", 1, ttl) - c.Put("int2", 2, ttl) - - i := c.Get("int") - _, expired := c.GetWithExpire("int2") - assert.Equal(t, i, 1, "should recv 1") - assert.True(t, now.Before(expired)) - - time.Sleep(ttl * 2) - i = c.Get("int") - j, _ := c.GetWithExpire("int2") - assert.True(t, i == 0, "should recv 0") - assert.True(t, j == 0, "should recv 0") -} - -func TestCache_AutoCleanup(t *testing.T) { - interval := 10 * time.Millisecond - ttl := 15 * time.Millisecond - c := New[string, int](interval) - c.Put("int", 1, ttl) - - time.Sleep(ttl * 2) - i := c.Get("int") - j, _ := c.GetWithExpire("int") - assert.True(t, i == 0, "should recv 0") - assert.True(t, j == 0, "should recv 0") -} - -func TestCache_AutoGC(t *testing.T) { - sign := make(chan struct{}) - go func() { - interval := 10 * time.Millisecond - ttl := 15 * time.Millisecond - c := New[string, int](interval) - c.Put("int", 1, ttl) - sign <- struct{}{} - }() - - <-sign - runtime.GC() -} diff --git a/common/cache/lrucache.go b/common/cache/lrucache.go index 5fef9445a0..73600e7194 100644 --- a/common/cache/lrucache.go +++ b/common/cache/lrucache.go @@ -65,8 +65,8 @@ type LruCache[K comparable, V any] struct { onEvict EvictCallback[K, V] } -// NewLRUCache creates an LruCache -func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { +// New creates an LruCache +func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { lc := &LruCache[K, V]{ lru: list.New[*entry[K, V]](), cache: make(map[K]*list.Element[*entry[K, V]]), diff --git a/common/cache/lrucache_test.go b/common/cache/lrucache_test.go index 1a4c68ae89..4cbc1ff8a1 100644 --- a/common/cache/lrucache_test.go +++ b/common/cache/lrucache_test.go @@ -19,7 +19,7 @@ var entries = []struct { } func TestLRUCache(t *testing.T) { - c := NewLRUCache[string, string]() + c := New[string, string]() for _, e := range entries { c.Set(e.key, e.value) @@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) { } func TestLRUMaxAge(t *testing.T) { - c := NewLRUCache[string, string](WithAge[string, string](86400)) + c := New[string, string](WithAge[string, string](86400)) now := time.Now().Unix() expected := now + 86400 @@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) { } func TestLRUpdateOnGet(t *testing.T) { - c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]()) + c := New[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]()) now := time.Now().Unix() expires := now + 86400/2 @@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) { } func TestMaxSize(t *testing.T) { - c := NewLRUCache[string, string](WithSize[string, string](2)) + c := New[string, string](WithSize[string, string](2)) // Add one expired entry c.Set("foo", "bar") _, ok := c.Get("foo") @@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) { } func TestExist(t *testing.T) { - c := NewLRUCache[int, int](WithSize[int, int](1)) + c := New[int, int](WithSize[int, int](1)) c.Set(1, 2) assert.True(t, c.Exist(1)) c.Set(2, 3) @@ -130,7 +130,7 @@ func TestEvict(t *testing.T) { temp = key + value } - c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) + c := New[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) c.Set(1, 2) c.Set(2, 3) @@ -138,7 +138,7 @@ func TestEvict(t *testing.T) { } func TestSetWithExpire(t *testing.T) { - c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1)) + c := New[int, *struct{}](WithAge[int, *struct{}](1)) now := time.Now().Unix() tenSecBefore := time.Unix(now-10, 0) @@ -153,7 +153,7 @@ func TestSetWithExpire(t *testing.T) { } func TestStale(t *testing.T) { - c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true)) + c := New[int, int](WithAge[int, int](1), WithStale[int, int](true)) now := time.Now().Unix() tenSecBefore := time.Unix(now-10, 0) @@ -166,11 +166,11 @@ func TestStale(t *testing.T) { } func TestCloneTo(t *testing.T) { - o := NewLRUCache[string, int](WithSize[string, int](10)) + o := New[string, int](WithSize[string, int](10)) o.Set("1", 1) o.Set("2", 2) - n := NewLRUCache[string, int](WithSize[string, int](2)) + n := New[string, int](WithSize[string, int](2)) n.Set("3", 3) n.Set("4", 4) diff --git a/common/convert/converter.go b/common/convert/converter.go index 238e241b14..24043a41bb 100644 --- a/common/convert/converter.go +++ b/common/convert/converter.go @@ -287,7 +287,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { } } - ss := make(map[string]any, 20) + ss := make(map[string]any, 10) ss["name"] = name ss["type"] = scheme @@ -297,6 +297,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { ss["password"] = password query := urlSS.Query() ss["udp"] = true + if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" { + ss["udp-over-tcp"] = true + } if strings.Contains(query.Get("plugin"), "obfs") { obfsParams := strings.Split(query.Get("plugin"), ";") ss["plugin"] = "obfs" diff --git a/common/net/bind.go b/common/net/bind.go new file mode 100644 index 0000000000..1e20a8c0e0 --- /dev/null +++ b/common/net/bind.go @@ -0,0 +1,36 @@ +package net + +import "net" + +type bindPacketConn struct { + net.PacketConn + rAddr net.Addr +} + +func (wpc *bindPacketConn) Read(b []byte) (n int, err error) { + n, _, err = wpc.PacketConn.ReadFrom(b) + return n, err +} + +func (wpc *bindPacketConn) Write(b []byte) (n int, err error) { + return wpc.PacketConn.WriteTo(b, wpc.rAddr) +} + +func (wpc *bindPacketConn) RemoteAddr() net.Addr { + return wpc.rAddr +} + +func (wpc *bindPacketConn) LocalAddr() net.Addr { + if wpc.PacketConn.LocalAddr() == nil { + return &net.UDPAddr{IP: net.IPv4zero, Port: 0} + } else { + return wpc.PacketConn.LocalAddr() + } +} + +func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn { + return &bindPacketConn{ + PacketConn: pc, + rAddr: rAddr, + } +} diff --git a/common/net/refconn.go b/common/net/refconn.go new file mode 100644 index 0000000000..6d28a2bfc8 --- /dev/null +++ b/common/net/refconn.go @@ -0,0 +1,100 @@ +package net + +import ( + "net" + "runtime" + "time" +) + +type refConn struct { + conn net.Conn + ref any +} + +func (c *refConn) Read(b []byte) (n int, err error) { + defer runtime.KeepAlive(c.ref) + return c.conn.Read(b) +} + +func (c *refConn) Write(b []byte) (n int, err error) { + defer runtime.KeepAlive(c.ref) + return c.conn.Write(b) +} + +func (c *refConn) Close() error { + defer runtime.KeepAlive(c.ref) + return c.conn.Close() +} + +func (c *refConn) LocalAddr() net.Addr { + defer runtime.KeepAlive(c.ref) + return c.conn.LocalAddr() +} + +func (c *refConn) RemoteAddr() net.Addr { + defer runtime.KeepAlive(c.ref) + return c.conn.RemoteAddr() +} + +func (c *refConn) SetDeadline(t time.Time) error { + defer runtime.KeepAlive(c.ref) + return c.conn.SetDeadline(t) +} + +func (c *refConn) SetReadDeadline(t time.Time) error { + defer runtime.KeepAlive(c.ref) + return c.conn.SetReadDeadline(t) +} + +func (c *refConn) SetWriteDeadline(t time.Time) error { + defer runtime.KeepAlive(c.ref) + return c.conn.SetWriteDeadline(t) +} + +func NewRefConn(conn net.Conn, ref any) net.Conn { + return &refConn{conn: conn, ref: ref} +} + +type refPacketConn struct { + pc net.PacketConn + ref any +} + +func (pc *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + defer runtime.KeepAlive(pc.ref) + return pc.pc.ReadFrom(p) +} + +func (pc *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + defer runtime.KeepAlive(pc.ref) + return pc.pc.WriteTo(p, addr) +} + +func (pc *refPacketConn) Close() error { + defer runtime.KeepAlive(pc.ref) + return pc.pc.Close() +} + +func (pc *refPacketConn) LocalAddr() net.Addr { + defer runtime.KeepAlive(pc.ref) + return pc.pc.LocalAddr() +} + +func (pc *refPacketConn) SetDeadline(t time.Time) error { + defer runtime.KeepAlive(pc.ref) + return pc.pc.SetDeadline(t) +} + +func (pc *refPacketConn) SetReadDeadline(t time.Time) error { + defer runtime.KeepAlive(pc.ref) + return pc.pc.SetReadDeadline(t) +} + +func (pc *refPacketConn) SetWriteDeadline(t time.Time) error { + defer runtime.KeepAlive(pc.ref) + return pc.pc.SetWriteDeadline(t) +} + +func NewRefPacketConn(pc net.PacketConn, ref any) net.PacketConn { + return &refPacketConn{pc: pc, ref: ref} +} diff --git a/common/net/tls.go b/common/net/tls.go new file mode 100644 index 0000000000..4f5263b8f3 --- /dev/null +++ b/common/net/tls.go @@ -0,0 +1,19 @@ +package net + +import ( + "crypto/tls" + "fmt" +) + +func ParseCert(certificate, privateKey string) (tls.Certificate, error) { + cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) + if painTextErr == nil { + return cert, nil + } + + 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()) + } + return cert, nil +} diff --git a/common/structure/structure.go b/common/structure/structure.go index e74c349a92..78f344a4c5 100644 --- a/common/structure/structure.go +++ b/common/structure/structure.go @@ -3,6 +3,7 @@ package structure // references: https://github.com/mitchellh/mapstructure import ( + "encoding/base64" "fmt" "reflect" "strconv" @@ -13,8 +14,11 @@ import ( type Option struct { TagName string WeaklyTypedInput bool + KeyReplacer *strings.Replacer } +var DefaultKeyReplacer = strings.NewReplacer("_", "-") + // Decoder is the core of structure type Decoder struct { option *Option @@ -49,6 +53,23 @@ func (d *Decoder) Decode(src map[string]any, dst any) error { omitempty := found && omitKey == "omitempty" value, ok := src[key] + if !ok { + if d.option.KeyReplacer != nil { + key = d.option.KeyReplacer.Replace(key) + } + + for _strKey := range src { + strKey := _strKey + if d.option.KeyReplacer != nil { + strKey = d.option.KeyReplacer.Replace(strKey) + } + if strings.EqualFold(key, strKey) { + value = src[_strKey] + ok = true + break + } + } + } if !ok || value == nil { if omitempty { continue @@ -65,9 +86,16 @@ func (d *Decoder) Decode(src map[string]any, dst any) error { } func (d *Decoder) decode(name string, data any, val reflect.Value) error { - switch val.Kind() { - case reflect.Int: + kind := val.Kind() + switch { + case isInt(kind): return d.decodeInt(name, data, val) + case isUint(kind): + return d.decodeUint(name, data, val) + case isFloat(kind): + return d.decodeFloat(name, data, val) + } + switch kind { case reflect.String: return d.decodeString(name, data, val) case reflect.Bool: @@ -85,13 +113,42 @@ func (d *Decoder) decode(name string, data any, val reflect.Value) error { } } +func isInt(kind reflect.Kind) bool { + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} + +func isUint(kind reflect.Kind) bool { + switch kind { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + default: + return false + } +} + +func isFloat(kind reflect.Kind) bool { + switch kind { + case reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { - case kind == reflect.Int: + case isInt(kind): val.SetInt(dataVal.Int()) - case kind == reflect.Float64 && d.option.WeaklyTypedInput: + case isUint(kind) && d.option.WeaklyTypedInput: + val.SetInt(int64(dataVal.Uint())) + case isFloat(kind) && d.option.WeaklyTypedInput: val.SetInt(int64(dataVal.Float())) case kind == reflect.String && d.option.WeaklyTypedInput: var i int64 @@ -110,14 +167,72 @@ func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error return err } +func (d *Decoder) decodeUint(name string, data any, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case isUint(kind): + val.SetUint(dataVal.Uint()) + case isInt(kind) && d.option.WeaklyTypedInput: + val.SetUint(uint64(dataVal.Int())) + case isFloat(kind) && d.option.WeaklyTypedInput: + val.SetUint(uint64(dataVal.Float())) + case kind == reflect.String && d.option.WeaklyTypedInput: + var i uint64 + i, err = strconv.ParseUint(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetUint(i) + } else { + err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + err = fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeFloat(name string, data any, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case isFloat(kind): + val.SetFloat(dataVal.Float()) + case isUint(kind): + val.SetFloat(float64(dataVal.Uint())) + case isInt(kind) && d.option.WeaklyTypedInput: + val.SetFloat(float64(dataVal.Int())) + case kind == reflect.String && d.option.WeaklyTypedInput: + var i float64 + i, err = strconv.ParseFloat(dataVal.String(), val.Type().Bits()) + if err == nil { + val.SetFloat(i) + } else { + err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + err = fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) { dataVal := reflect.ValueOf(data) kind := dataVal.Kind() switch { case kind == reflect.String: val.SetString(dataVal.String()) - case kind == reflect.Int && d.option.WeaklyTypedInput: + case isInt(kind) && d.option.WeaklyTypedInput: val.SetString(strconv.FormatInt(dataVal.Int(), 10)) + case isUint(kind) && d.option.WeaklyTypedInput: + val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) + case isFloat(kind) && d.option.WeaklyTypedInput: + val.SetString(strconv.FormatFloat(dataVal.Float(), 'E', -1, dataVal.Type().Bits())) default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", @@ -133,8 +248,10 @@ func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err erro switch { case kind == reflect.Bool: val.SetBool(dataVal.Bool()) - case kind == reflect.Int && d.option.WeaklyTypedInput: + case isInt(kind) && d.option.WeaklyTypedInput: val.SetBool(dataVal.Int() != 0) + case isUint(kind) && d.option.WeaklyTypedInput: + val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) default: err = fmt.Errorf( "'%s' expected type '%s', got unconvertible type '%s'", @@ -149,6 +266,17 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error { valType := val.Type() valElemType := valType.Elem() + if dataVal.Kind() == reflect.String && valElemType.Kind() == reflect.Uint8 { // from encoding/json + s := []byte(dataVal.String()) + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + return fmt.Errorf("try decode '%s' by base64 error: %w", name, err) + } + val.SetBytes(b[:n]) + return nil + } + if dataVal.Kind() != reflect.Slice { return fmt.Errorf("'%s' is not a slice", name) } @@ -353,12 +481,18 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e if !rawMapVal.IsValid() { // Do a slower search by iterating over each key and // doing case-insensitive search. + if d.option.KeyReplacer != nil { + fieldName = d.option.KeyReplacer.Replace(fieldName) + } for dataValKey := range dataValKeys { mK, ok := dataValKey.Interface().(string) if !ok { // Not a string key continue } + if d.option.KeyReplacer != nil { + mK = d.option.KeyReplacer.Replace(mK) + } if strings.EqualFold(mK, fieldName) { rawMapKey = dataValKey diff --git a/common/structure/structure_test.go b/common/structure/structure_test.go index b3bc52a644..9f31d3d111 100644 --- a/common/structure/structure_test.go +++ b/common/structure/structure_test.go @@ -137,3 +137,45 @@ func TestStructure_Nest(t *testing.T) { assert.Nil(t, err) assert.Equal(t, s.BazOptional, goal) } + +func TestStructure_SliceNilValue(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + "bar": []any{"bar", nil}, + } + + goal := &BazSlice{ + Foo: 1, + Bar: []string{"bar", ""}, + } + + s := &BazSlice{} + err := weakTypeDecoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, goal.Bar, s.Bar) + + s = &BazSlice{} + err = decoder.Decode(rawMap, s) + assert.NotNil(t, err) +} + +func TestStructure_SliceNilValueComplex(t *testing.T) { + rawMap := map[string]any{ + "bar": []any{map[string]any{"bar": "foo"}, nil}, + } + + s := &struct { + Bar []map[string]any `test:"bar"` + }{} + + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Nil(t, s.Bar[1]) + + ss := &struct { + Bar []Baz `test:"bar"` + }{} + + err = decoder.Decode(rawMap, ss) + assert.NotNil(t, err) +} diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index ea4d2ece62..d7c0f3dbcd 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -4,12 +4,15 @@ import ( "context" "errors" "fmt" - "github.com/Dreamacro/clash/component/resolver" - "go.uber.org/atomic" "net" "net/netip" + "runtime" "strings" "sync" + + "github.com/Dreamacro/clash/component/resolver" + + "go.uber.org/atomic" ) var ( @@ -22,7 +25,18 @@ var ( ErrorDisableIPv6 = errors.New("IPv6 is disabled, dialer cancel") ) -func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { +func ParseNetwork(network string, addr netip.Addr) string { + if runtime.GOOS == "windows" { // fix bindIfaceToListenConfig() in windows force bind to an ipv4 address + if !strings.HasSuffix(network, "4") && + !strings.HasSuffix(network, "6") && + addr.Unmap().Is6() { + network += "6" + } + } + return network +} + +func applyOptions(options ...Option) *option { opt := &option{ interfaceName: DefaultInterface.Load(), routingMark: int(DefaultRoutingMark.Load()), @@ -36,6 +50,12 @@ func DialContext(ctx context.Context, network, address string, options ...Option o(opt) } + return opt +} + +func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { + opt := applyOptions(options...) + if opt.network == 4 || opt.network == 6 { if strings.Contains(network, "tcp") { network = "tcp" @@ -143,7 +163,7 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt results := make(chan dialResult) var primary, fallback dialResult - startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) { + startRacer := func(ctx context.Context, network, host string, r resolver.Resolver, ipv6 bool) { result := dialResult{ipv6: ipv6, done: true} defer func() { select { @@ -157,16 +177,16 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt var ip netip.Addr if ipv6 { - if !direct { - ip, result.error = resolver.ResolveIPv6ProxyServerHost(host) + if r == nil { + ip, result.error = resolver.ResolveIPv6ProxyServerHost(ctx, host) } else { - ip, result.error = resolver.ResolveIPv6(host) + ip, result.error = resolver.ResolveIPv6WithResolver(ctx, host, r) } } else { - if !direct { - ip, result.error = resolver.ResolveIPv4ProxyServerHost(host) + if r == nil { + ip, result.error = resolver.ResolveIPv4ProxyServerHost(ctx, host) } else { - ip, result.error = resolver.ResolveIPv4(host) + ip, result.error = resolver.ResolveIPv4WithResolver(ctx, host, r) } } if result.error != nil { @@ -177,8 +197,8 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt result.Conn, result.error = dialContext(ctx, network, ip, port, opt) } - go startRacer(ctx, network+"4", host, opt.direct, false) - go startRacer(ctx, network+"6", host, opt.direct, true) + go startRacer(ctx, network+"4", host, opt.resolver, false) + go startRacer(ctx, network+"6", host, opt.resolver, true) count := 2 for i := 0; i < count; i++ { @@ -204,11 +224,17 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt } } case <-ctx.Done(): + err = ctx.Err() break } } - return nil, errors.New("dual stack tcp shake hands failed") + if err == nil { + err = fmt.Errorf("dual stack dial failed") + } else { + err = fmt.Errorf("dual stack dial failed:%w", err) + } + return nil, err } func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { @@ -218,10 +244,10 @@ func concurrentDualStackDialContext(ctx context.Context, network, address string } var ips []netip.Addr - if opt.direct { - ips, err = resolver.ResolveAllIP(host) + if opt.resolver != nil { + ips, err = resolver.LookupIPWithResolver(ctx, host, opt.resolver) } else { - ips, err = resolver.ResolveAllIPProxyServerHost(host) + ips, err = resolver.LookupIPProxyServerHost(ctx, host) } if err != nil { @@ -291,6 +317,7 @@ func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr connCount := len(ips) var fallback dialResult var primaryError error + var finalError error for i := 0; i < connCount; i++ { select { case res := <-results: @@ -315,6 +342,7 @@ func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr if fallback.done && fallback.error == nil { return fallback.Conn, nil } + finalError = ctx.Err() break } } @@ -331,7 +359,13 @@ func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr return nil, fallback.error } - return nil, fmt.Errorf("all ips %v tcp shake hands failed", ips) + if finalError == nil { + finalError = fmt.Errorf("all ips %v tcp shake hands failed", ips) + } else { + finalError = fmt.Errorf("concurrent dial failed:%w", finalError) + } + + return nil, finalError } func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { @@ -343,16 +377,16 @@ func singleDialContext(ctx context.Context, network string, address string, opt var ip netip.Addr switch network { case "tcp4", "udp4": - if !opt.direct { - ip, err = resolver.ResolveIPv4ProxyServerHost(host) + if opt.resolver == nil { + ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host) } else { - ip, err = resolver.ResolveIPv4(host) + ip, err = resolver.ResolveIPv4WithResolver(ctx, host, opt.resolver) } default: - if !opt.direct { - ip, err = resolver.ResolveIPv6ProxyServerHost(host) + if opt.resolver == nil { + ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host) } else { - ip, err = resolver.ResolveIPv6(host) + ip, err = resolver.ResolveIPv6WithResolver(ctx, host, opt.resolver) } } if err != nil { @@ -378,10 +412,10 @@ func concurrentIPv4DialContext(ctx context.Context, network, address string, opt } var ips []netip.Addr - if !opt.direct { - ips, err = resolver.ResolveAllIPv4ProxyServerHost(host) + if opt.resolver == nil { + ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host) } else { - ips, err = resolver.ResolveAllIPv4(host) + ips, err = resolver.LookupIPv4WithResolver(ctx, host, opt.resolver) } if err != nil { @@ -398,10 +432,10 @@ func concurrentIPv6DialContext(ctx context.Context, network, address string, opt } var ips []netip.Addr - if !opt.direct { - ips, err = resolver.ResolveAllIPv6ProxyServerHost(host) + if opt.resolver == nil { + ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host) } else { - ips, err = resolver.ResolveAllIPv6(host) + ips, err = resolver.LookupIPv6WithResolver(ctx, host, opt.resolver) } if err != nil { @@ -410,3 +444,20 @@ func concurrentIPv6DialContext(ctx context.Context, network, address string, opt return concurrentDialContext(ctx, network, ips, port, opt) } + +type Dialer struct { + Opt option +} + +func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return DialContext(ctx, network, address, WithOption(d.Opt)) +} + +func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { + return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, WithOption(d.Opt)) +} + +func NewDialer(options ...Option) Dialer { + opt := applyOptions(options...) + return Dialer{Opt: *opt} +} diff --git a/component/dialer/mark_linux.go b/component/dialer/mark_linux.go index 41b6186310..eaba5cf7a1 100644 --- a/component/dialer/mark_linux.go +++ b/component/dialer/mark_linux.go @@ -29,13 +29,13 @@ func bindMarkToControl(mark int, chain controlFn) controlFn { return } - return c.Control(func(fd uintptr) { - switch network { - case "tcp4", "udp4": - _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) - case "tcp6", "udp6": - _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) - } + var innerErr error + err = c.Control(func(fd uintptr) { + innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) }) + if innerErr != nil { + err = innerErr + } + return } } diff --git a/component/dialer/options.go b/component/dialer/options.go index ce911035b1..27adc84518 100644 --- a/component/dialer/options.go +++ b/component/dialer/options.go @@ -1,6 +1,8 @@ package dialer import ( + "github.com/Dreamacro/clash/component/resolver" + "go.uber.org/atomic" ) @@ -14,9 +16,9 @@ type option struct { interfaceName string addrReuse bool routingMark int - direct bool network int prefer int + resolver resolver.Resolver } type Option func(opt *option) @@ -39,9 +41,9 @@ func WithRoutingMark(mark int) Option { } } -func WithDirect() Option { +func WithResolver(r resolver.Resolver) Option { return func(opt *option) { - opt.direct = true + opt.resolver = r } } @@ -66,3 +68,9 @@ func WithOnlySingleStack(isIPv4 bool) Option { } } } + +func WithOption(o option) Option { + return func(opt *option) { + *opt = o + } +} diff --git a/component/ebpf/ebpf_linux.go b/component/ebpf/ebpf_linux.go index bf41d6cb4b..2ffd4bd5be 100644 --- a/component/ebpf/ebpf_linux.go +++ b/component/ebpf/ebpf_linux.go @@ -6,13 +6,12 @@ import ( "fmt" "net/netip" - "github.com/vishvananda/netlink" - "github.com/Dreamacro/clash/common/cmd" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/ebpf/redir" "github.com/Dreamacro/clash/component/ebpf/tc" C "github.com/Dreamacro/clash/constant" + "github.com/sagernet/netlink" ) func GetAutoDetectInterface() (string, error) { diff --git a/component/ebpf/redir/auto_redirect.go b/component/ebpf/redir/auto_redirect.go index 2d1b48780c..4fd8b78500 100644 --- a/component/ebpf/redir/auto_redirect.go +++ b/component/ebpf/redir/auto_redirect.go @@ -13,7 +13,7 @@ import ( "github.com/cilium/ebpf" "github.com/cilium/ebpf/rlimit" - "github.com/vishvananda/netlink" + "github.com/sagernet/netlink" "golang.org/x/sys/unix" "github.com/Dreamacro/clash/component/ebpf/byteorder" diff --git a/component/ebpf/tc/redirect_to_tun.go b/component/ebpf/tc/redirect_to_tun.go index 3fbaeb1bc4..1edc1781f7 100644 --- a/component/ebpf/tc/redirect_to_tun.go +++ b/component/ebpf/tc/redirect_to_tun.go @@ -11,7 +11,7 @@ import ( "github.com/cilium/ebpf" "github.com/cilium/ebpf/rlimit" - "github.com/vishvananda/netlink" + "github.com/sagernet/netlink" "golang.org/x/sys/unix" C "github.com/Dreamacro/clash/constant" diff --git a/component/fakeip/memory.go b/component/fakeip/memory.go index 5566ce48b9..249c5e2a1b 100644 --- a/component/fakeip/memory.go +++ b/component/fakeip/memory.go @@ -73,7 +73,7 @@ func (m *memoryStore) FlushFakeIP() error { func newMemoryStore(size int) *memoryStore { return &memoryStore{ - cacheIP: cache.NewLRUCache[string, netip.Addr](cache.WithSize[string, netip.Addr](size)), - cacheHost: cache.NewLRUCache[netip.Addr, string](cache.WithSize[netip.Addr, string](size)), + cacheIP: cache.New[string, netip.Addr](cache.WithSize[string, netip.Addr](size)), + cacheHost: cache.New[netip.Addr, string](cache.WithSize[netip.Addr, string](size)), } } diff --git a/component/fakeip/pool.go b/component/fakeip/pool.go index 58a75a9448..ee11fedd1f 100644 --- a/component/fakeip/pool.go +++ b/component/fakeip/pool.go @@ -3,6 +3,7 @@ package fakeip import ( "errors" "net/netip" + "strings" "sync" "github.com/Dreamacro/clash/common/nnip" @@ -26,7 +27,7 @@ type store interface { FlushFakeIP() error } -// Pool is a implementation about fake ip generator without storage +// Pool is an implementation about fake ip generator without storage type Pool struct { gateway netip.Addr first netip.Addr @@ -34,7 +35,7 @@ type Pool struct { offset netip.Addr cycle bool mux sync.Mutex - host *trie.DomainTrie[bool] + host *trie.DomainTrie[struct{}] ipnet *netip.Prefix store store } @@ -43,6 +44,9 @@ type Pool struct { func (p *Pool) Lookup(host string) netip.Addr { p.mux.Lock() defer p.mux.Unlock() + + // RFC4343: DNS Case Insensitive, we SHOULD return result with all cases. + host = strings.ToLower(host) if ip, exist := p.store.GetByHost(host); exist { return ip } @@ -150,7 +154,7 @@ func (p *Pool) restoreState() { type Options struct { IPNet *netip.Prefix - Host *trie.DomainTrie[bool] + Host *trie.DomainTrie[struct{}] // Size sets the maximum number of entries in memory // and does not work if Persistence is true diff --git a/component/fakeip/pool_test.go b/component/fakeip/pool_test.go index 0e9dd630fa..ae343f96c9 100644 --- a/component/fakeip/pool_test.go +++ b/component/fakeip/pool_test.go @@ -104,6 +104,27 @@ func TestPool_BasicV6(t *testing.T) { } } +func TestPool_Case_Insensitive(t *testing.T) { + ipnet := netip.MustParsePrefix("192.168.0.1/29") + pools, tempfile, err := createPools(Options{ + IPNet: &ipnet, + Size: 10, + }) + assert.Nil(t, err) + defer os.Remove(tempfile) + + for _, pool := range pools { + first := pool.Lookup("foo.com") + last := pool.Lookup("Foo.Com") + foo, exist := pool.LookBack(last) + + assert.Equal(t, first, pool.Lookup("Foo.Com")) + assert.Equal(t, pool.Lookup("fOo.cOM"), first) + assert.True(t, exist) + assert.Equal(t, foo, "foo.com") + } +} + func TestPool_CycleUsed(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.16/28") pools, tempfile, err := createPools(Options{ @@ -128,8 +149,8 @@ func TestPool_CycleUsed(t *testing.T) { func TestPool_Skip(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/29") - tree := trie.New[bool]() - tree.Insert("example.com", true) + tree := trie.New[struct{}]() + tree.Insert("example.com", struct{}{}) pools, tempfile, err := createPools(Options{ IPNet: &ipnet, Size: 10, diff --git a/component/process/process.go b/component/process/process.go index a44af6c07d..fe4c5d2a72 100644 --- a/component/process/process.go +++ b/component/process/process.go @@ -16,14 +16,14 @@ const ( UDP = "udp" ) -func FindProcessName(network string, srcIP netip.Addr, srcPort int) (int32, string, error) { +func FindProcessName(network string, srcIP netip.Addr, srcPort int) (*uint32, string, error) { return findProcessName(network, srcIP, srcPort) } -func FindUid(network string, srcIP netip.Addr, srcPort int) (int32, error) { +func FindUid(network string, srcIP netip.Addr, srcPort int) (*uint32, error) { _, uid, err := resolveSocketByNetlink(network, srcIP, srcPort) if err != nil { - return -1, err + return nil, err } - return uid, nil + return &uid, nil } diff --git a/component/process/process_darwin.go b/component/process/process_darwin.go index c27aed04af..39e56dd2a8 100644 --- a/component/process/process_darwin.go +++ b/component/process/process_darwin.go @@ -33,11 +33,11 @@ var structSize = func() int { } }() -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) { +func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { return 0, 0, ErrPlatformNotSupport } -func findProcessName(network string, ip netip.Addr, port int) (int32, string, error) { +func findProcessName(network string, ip netip.Addr, port int) (*uint32, string, error) { var spath string switch network { case TCP: @@ -45,14 +45,14 @@ func findProcessName(network string, ip netip.Addr, port int) (int32, string, er case UDP: spath = "net.inet.udp.pcblist_n" default: - return -1, "", ErrInvalidNetwork + return nil, "", ErrInvalidNetwork } isIPv4 := ip.Is4() value, err := syscall.Sysctl(spath) if err != nil { - return -1, "", err + return nil, "", err } buf := []byte(value) @@ -96,7 +96,7 @@ func findProcessName(network string, ip netip.Addr, port int) (int32, string, er // xsocket_n.so_last_pid pid := readNativeUint32(buf[so+68 : so+72]) pp, err := getExecPathFromPID(pid) - return -1, pp, err + return nil, pp, err } // udp packet connection may be not equal with srcIP @@ -106,10 +106,10 @@ func findProcessName(network string, ip netip.Addr, port int) (int32, string, er } if network == UDP && fallbackUDPProcess != "" { - return -1, fallbackUDPProcess, nil + return nil, fallbackUDPProcess, nil } - return -1, "", ErrNotFound + return nil, "", ErrNotFound } func getExecPathFromPID(pid uint32) (string, error) { diff --git a/component/process/process_freebsd_amd64.go b/component/process/process_freebsd_amd64.go index 6b5f51f74b..ffbe15152e 100644 --- a/component/process/process_freebsd_amd64.go +++ b/component/process/process_freebsd_amd64.go @@ -21,11 +21,11 @@ var ( once sync.Once ) -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) { +func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { return 0, 0, ErrPlatformNotSupport } -func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) { +func findProcessName(network string, ip netip.Addr, srcPort int) (*uint32, string, error) { once.Do(func() { if err := initSearcher(); err != nil { log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error()) @@ -35,7 +35,7 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, }) if defaultSearcher == nil { - return -1, "", ErrPlatformNotSupport + return nil, "", ErrPlatformNotSupport } var spath string @@ -46,22 +46,22 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, case UDP: spath = "net.inet.udp.pcblist" default: - return -1, "", ErrInvalidNetwork + return nil, "", ErrInvalidNetwork } value, err := syscall.Sysctl(spath) if err != nil { - return -1, "", err + return nil, "", err } buf := []byte(value) pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP) if err != nil { - return -1, "", err + return nil, "", err } pp, err := getExecPathFromPID(pid) - return -1, pp, err + return nil, pp, err } func getExecPathFromPID(pid uint32) (string, error) { diff --git a/component/process/process_linux.go b/component/process/process_linux.go index 01da1d3399..9b1a484483 100644 --- a/component/process/process_linux.go +++ b/component/process/process_linux.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/binary" "fmt" - "net" "net/netip" "os" "path" @@ -15,162 +14,125 @@ import ( "unicode" "unsafe" - "github.com/Dreamacro/clash/common/pool" + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" ) -// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62 -var nativeEndian = func() binary.ByteOrder { - var x uint32 = 0x01020304 - if *(*byte)(unsafe.Pointer(&x)) == 0x01 { - return binary.BigEndian - } - - return binary.LittleEndian -}() - const ( - sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48 - socketDiagByFamily = 20 - pathProc = "/proc" + SOCK_DIAG_BY_FAMILY = 20 + inetDiagRequestSize = int(unsafe.Sizeof(inetDiagRequest{})) + inetDiagResponseSize = int(unsafe.Sizeof(inetDiagResponse{})) ) -func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) { +type inetDiagRequest struct { + Family byte + Protocol byte + Ext byte + Pad byte + States uint32 + + SrcPort [2]byte + DstPort [2]byte + Src [16]byte + Dst [16]byte + If uint32 + Cookie [2]uint32 +} + +type inetDiagResponse struct { + Family byte + State byte + Timer byte + ReTrans byte + + SrcPort [2]byte + DstPort [2]byte + Src [16]byte + Dst [16]byte + If uint32 + Cookie [2]uint32 + + Expires uint32 + RQueue uint32 + WQueue uint32 + UID uint32 + INode uint32 +} + +func findProcessName(network string, ip netip.Addr, srcPort int) (*uint32, string, error) { inode, uid, err := resolveSocketByNetlink(network, ip, srcPort) if err != nil { - return -1, "", err + return nil, "", err } pp, err := resolveProcessNameByProcSearch(inode, uid) - return uid, pp, err + return &uid, pp, err } -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) { - var family byte - var protocol byte - - switch network { - case TCP: - protocol = syscall.IPPROTO_TCP - case UDP: - protocol = syscall.IPPROTO_UDP - default: - return 0, 0, ErrInvalidNetwork +func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { + request := &inetDiagRequest{ + States: 0xffffffff, + Cookie: [2]uint32{0xffffffff, 0xffffffff}, } if ip.Is4() { - family = syscall.AF_INET + request.Family = unix.AF_INET } else { - family = syscall.AF_INET6 + request.Family = unix.AF_INET6 } - req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort)) - - socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG) - if err != nil { - return 0, 0, fmt.Errorf("dial netlink: %w", err) - } - defer func() { - _ = syscall.Close(socket) - }() - - _ = syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100}) - _ = syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100}) - - if err := syscall.Connect(socket, &syscall.SockaddrNetlink{ - Family: syscall.AF_NETLINK, - Pad: 0, - Pid: 0, - Groups: 0, - }); err != nil { - return 0, 0, err - } - - if _, err := syscall.Write(socket, req); err != nil { - return 0, 0, fmt.Errorf("write request: %w", err) + if strings.HasPrefix(network, "tcp") { + request.Protocol = unix.IPPROTO_TCP + } else if strings.HasPrefix(network, "udp") { + request.Protocol = unix.IPPROTO_UDP + } else { + return 0, 0, ErrInvalidNetwork } - rb := pool.Get(pool.RelayBufferSize) - defer func() { - _ = pool.Put(rb) - }() + copy(request.Src[:], ip.AsSlice()) - n, err := syscall.Read(socket, rb) - if err != nil { - return 0, 0, fmt.Errorf("read response: %w", err) - } + binary.BigEndian.PutUint16(request.SrcPort[:], uint16(srcPort)) - messages, err := syscall.ParseNetlinkMessage(rb[:n]) + conn, err := netlink.Dial(unix.NETLINK_INET_DIAG, nil) if err != nil { - return 0, 0, fmt.Errorf("parse netlink message: %w", err) - } else if len(messages) == 0 { - return 0, 0, fmt.Errorf("unexcepted netlink response") + return 0, 0, err } - - message := messages[0] - if message.Header.Type&syscall.NLMSG_ERROR != 0 { - return 0, 0, fmt.Errorf("netlink message: NLMSG_ERROR") + defer conn.Close() + + message := netlink.Message{ + Header: netlink.Header{ + Type: SOCK_DIAG_BY_FAMILY, + Flags: netlink.Request | netlink.Dump, + }, + Data: (*(*[inetDiagRequestSize]byte)(unsafe.Pointer(request)))[:], } - inode, uid := unpackSocketDiagResponse(&messages[0]) - if inode < 0 || uid < 0 { - return 0, 0, fmt.Errorf("invalid inode(%d) or uid(%d)", inode, uid) + messages, err := conn.Execute(message) + if err != nil { + return 0, 0, err } - return inode, uid, nil -} - -func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort uint16) []byte { - s := make([]byte, 16) - - copy(s, source.AsSlice()) - - buf := make([]byte, sizeOfSocketDiagRequest) - - nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest) - nativeEndian.PutUint16(buf[4:6], socketDiagByFamily) - nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP) - nativeEndian.PutUint32(buf[8:12], 0) - nativeEndian.PutUint32(buf[12:16], 0) - - buf[16] = family - buf[17] = protocol - buf[18] = 0 - buf[19] = 0 - nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF) - - binary.BigEndian.PutUint16(buf[24:26], sourcePort) - binary.BigEndian.PutUint16(buf[26:28], 0) - - copy(buf[28:44], s) - copy(buf[44:60], net.IPv6zero) + for _, msg := range messages { + if len(msg.Data) < inetDiagResponseSize { + continue + } - nativeEndian.PutUint32(buf[60:64], 0) - nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF) + response := (*inetDiagResponse)(unsafe.Pointer(&msg.Data[0])) - return buf -} - -func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid int32) { - if len(msg.Data) < 72 { - return 0, 0 + return response.INode, response.UID, nil } - data := msg.Data - - uid = int32(nativeEndian.Uint32(data[64:68])) - inode = int32(nativeEndian.Uint32(data[68:72])) - - return + return 0, 0, ErrNotFound } -func resolveProcessNameByProcSearch(inode, uid int32) (string, error) { - files, err := os.ReadDir(pathProc) +func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { + files, err := os.ReadDir("/proc") if err != nil { return "", err } - buffer := make([]byte, syscall.PathMax) - socket := []byte(fmt.Sprintf("socket:[%d]", inode)) + buffer := make([]byte, unix.PathMax) + socket := fmt.Appendf(nil, "socket:[%d]", inode) for _, f := range files { if !f.IsDir() || !isPid(f.Name()) { @@ -181,12 +143,12 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) { if err != nil { return "", err } - if info.Sys().(*syscall.Stat_t).Uid != uint32(uid) { + if info.Sys().(*syscall.Stat_t).Uid != uid { continue } - processPath := path.Join(pathProc, f.Name()) - fdPath := path.Join(processPath, "fd") + processPath := filepath.Join("/proc", f.Name()) + fdPath := filepath.Join(processPath, "fd") fds, err := os.ReadDir(fdPath) if err != nil { @@ -194,7 +156,7 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) { } for _, fd := range fds { - n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer) + n, err := unix.Readlink(filepath.Join(fdPath, fd.Name()), buffer) if err != nil { continue } @@ -209,9 +171,10 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) { } } else { if bytes.Equal(buffer[:n], socket) { - return os.Readlink(path.Join(processPath, "exe")) + return os.Readlink(filepath.Join(processPath, "exe")) } } + } } @@ -222,7 +185,7 @@ func splitCmdline(cmdline []byte) string { cmdline = bytes.Trim(cmdline, " ") idx := bytes.IndexFunc(cmdline, func(r rune) bool { - return unicode.IsControl(r) || unicode.IsSpace(r) + return unicode.IsControl(r) || unicode.IsSpace(r) || r == ':' }) if idx == -1 { diff --git a/component/process/process_other.go b/component/process/process_other.go index 77dad25084..32614b2676 100644 --- a/component/process/process_other.go +++ b/component/process/process_other.go @@ -4,10 +4,10 @@ package process import "net/netip" -func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) { - return -1, "", ErrPlatformNotSupport +func findProcessName(network string, ip netip.Addr, srcPort int) (*uint32, string, error) { + return nil, "", ErrPlatformNotSupport } -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) { +func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { return 0, 0, ErrPlatformNotSupport } diff --git a/component/process/process_windows.go b/component/process/process_windows.go index 03935c37df..7e073c8e41 100644 --- a/component/process/process_windows.go +++ b/component/process/process_windows.go @@ -29,7 +29,7 @@ var ( once sync.Once ) -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) { +func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { return 0, 0, ErrPlatformNotSupport } @@ -62,7 +62,7 @@ func initWin32API() error { return nil } -func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) { +func findProcessName(network string, ip netip.Addr, srcPort int) (*uint32, string, error) { once.Do(func() { err := initWin32API() if err != nil { @@ -86,22 +86,22 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, fn = getExUDPTable class = udpTablePid default: - return -1, "", ErrInvalidNetwork + return nil, "", ErrInvalidNetwork } buf, err := getTransportTable(fn, family, class) if err != nil { - return -1, "", err + return nil, "", err } s := newSearcher(family == windows.AF_INET, network == TCP) pid, err := s.Search(buf, ip, uint16(srcPort)) if err != nil { - return -1, "", err + return nil, "", err } pp, err := getExecPathFromPID(pid) - return -1, pp, err + return nil, pp, err } type searcher struct { @@ -220,7 +220,8 @@ func getExecPathFromPID(pid uint32) (string, error) { uintptr(h), uintptr(1), uintptr(unsafe.Pointer(&buf[0])), - uintptr(unsafe.Pointer(&size))) + uintptr(unsafe.Pointer(&size)), + ) if r1 == 0 { return "", err } diff --git a/component/resolver/local.go b/component/resolver/local.go index be84e69311..e8505118b4 100644 --- a/component/resolver/local.go +++ b/component/resolver/local.go @@ -1,17 +1,21 @@ package resolver -import D "github.com/miekg/dns" +import ( + "context" + + D "github.com/miekg/dns" +) var DefaultLocalServer LocalServer type LocalServer interface { - ServeMsg(msg *D.Msg) (*D.Msg, error) + ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) } // ServeMsg with a dns.Msg, return resolve dns.Msg -func ServeMsg(msg *D.Msg) (*D.Msg, error) { +func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { if server := DefaultLocalServer; server != nil { - return server.ServeMsg(msg) + return server.ServeMsg(ctx, msg) } return nil, ErrIPNotFound diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go index abb455640d..55f98d3c87 100644 --- a/component/resolver/resolver.go +++ b/component/resolver/resolver.go @@ -3,12 +3,13 @@ package resolver import ( "context" "errors" + "fmt" "math/rand" "net" "net/netip" + "strings" "time" - "github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/component/trie" ) @@ -37,227 +38,228 @@ var ( ) type Resolver interface { - ResolveIP(host string) (ip netip.Addr, err error) - ResolveIPv4(host string) (ip netip.Addr, err error) - ResolveIPv6(host string) (ip netip.Addr, err error) - ResolveAllIP(host string) (ip []netip.Addr, err error) - ResolveAllIPv4(host string) (ips []netip.Addr, err error) - ResolveAllIPv6(host string) (ips []netip.Addr, err error) + 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) + ResolveIP(ctx context.Context, host string) (ip netip.Addr, err error) + ResolveIPv4(ctx context.Context, host string) (ip netip.Addr, err error) + ResolveIPv6(ctx context.Context, host string) (ip netip.Addr, err error) } -// ResolveIPv4 with a host, return ipv4 -func ResolveIPv4(host string) (netip.Addr, error) { - return ResolveIPv4WithResolver(host, DefaultResolver) -} +// LookupIPv4WithResolver same as LookupIPv4, but with a resolver +func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { + if node := DefaultHosts.Search(host); node != nil { + if ip := node.Data(); ip.Is4() { + return []netip.Addr{node.Data()}, nil + } + } -func ResolveIPv4WithResolver(host string, r Resolver) (netip.Addr, error) { - if ips, err := ResolveAllIPv4WithResolver(host, r); err == nil { - return ips[rand.Intn(len(ips))], nil - } else { - return netip.Addr{}, err + ip, err := netip.ParseAddr(host) + if err == nil { + if ip.Is4() || ip.Is4In6() { + return []netip.Addr{ip}, nil + } + return []netip.Addr{}, ErrIPVersion } -} -// ResolveIPv6 with a host, return ipv6 -func ResolveIPv6(host string) (netip.Addr, error) { - return ResolveIPv6WithResolver(host, DefaultResolver) -} + if r != nil { + return r.LookupIPv4(ctx, host) + } -func ResolveIPv6WithResolver(host string, r Resolver) (netip.Addr, error) { - if ips, err := ResolveAllIPv6WithResolver(host, r); err == nil { - return ips[rand.Intn(len(ips))], nil - } else { - return netip.Addr{}, err + if DefaultResolver != nil { + return DefaultResolver.LookupIPv4(ctx, host) } -} -// ResolveIPWithResolver same as ResolveIP, but with a resolver -func ResolveIPWithResolver(host string, r Resolver) (netip.Addr, error) { - if ip, err := ResolveIPv4WithResolver(host, r); err == nil { - return ip, nil - } else { - return ResolveIPv6WithResolver(host, r) + ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", host) + if err != nil { + return nil, err + } else if len(ipAddrs) == 0 { + return nil, ErrIPNotFound } -} -// ResolveIP with a host, return ip -func ResolveIP(host string) (netip.Addr, error) { - return ResolveIPWithResolver(host, DefaultResolver) + return ipAddrs, nil } -// ResolveIPv4ProxyServerHost proxies server host only -func ResolveIPv4ProxyServerHost(host string) (netip.Addr, error) { - if ProxyServerHostResolver != nil { - if ip, err := ResolveIPv4WithResolver(host, ProxyServerHostResolver); err != nil { - return ResolveIPv4(host) - } else { - return ip, nil - } - } - return ResolveIPv4(host) +// LookupIPv4 with a host, return ipv4 list +func LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { + return LookupIPv4WithResolver(ctx, host, DefaultResolver) } -// ResolveIPv6ProxyServerHost proxies server host only -func ResolveIPv6ProxyServerHost(host string) (netip.Addr, error) { - if ProxyServerHostResolver != nil { - if ip, err := ResolveIPv6WithResolver(host, ProxyServerHostResolver); err != nil { - return ResolveIPv6(host) - } else { - return ip, nil - } +// ResolveIPv4WithResolver same as ResolveIPv4, but with a resolver +func ResolveIPv4WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { + ips, err := LookupIPv4WithResolver(ctx, host, r) + if err != nil { + return netip.Addr{}, err + } else if len(ips) == 0 { + return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ResolveIPv6(host) + return ips[rand.Intn(len(ips))], nil } -// ResolveProxyServerHost proxies server host only -func ResolveProxyServerHost(host string) (netip.Addr, error) { - if ProxyServerHostResolver != nil { - if ip, err := ResolveIPWithResolver(host, ProxyServerHostResolver); err != nil { - return ResolveIP(host) - } else { - return ip, err - } - } - return ResolveIP(host) +// ResolveIPv4 with a host, return ipv4 +func ResolveIPv4(ctx context.Context, host string) (netip.Addr, error) { + return ResolveIPv4WithResolver(ctx, host, DefaultResolver) } -func ResolveAllIPv6WithResolver(host string, r Resolver) ([]netip.Addr, error) { +// LookupIPv6WithResolver same as LookupIPv6, but with a resolver +func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { if DisableIPv6 { - return []netip.Addr{}, ErrIPv6Disabled + return nil, ErrIPv6Disabled } if node := DefaultHosts.Search(host); node != nil { - if ip := node.Data; ip.Is6() { + if ip := node.Data(); ip.Is6() { return []netip.Addr{ip}, nil } } - ip, err := netip.ParseAddr(host) - if err == nil { - if ip.Is6() { + if ip, err := netip.ParseAddr(host); err == nil { + if strings.Contains(host, ":") { return []netip.Addr{ip}, nil } - return []netip.Addr{}, ErrIPVersion + return nil, ErrIPVersion } if r != nil { - return r.ResolveAllIPv6(host) + return r.LookupIPv6(ctx, host) + } + if DefaultResolver != nil { + return DefaultResolver.LookupIPv6(ctx, host) } - if DefaultResolver == nil { - ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout) - defer cancel() - ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host) - if err != nil { - return []netip.Addr{}, err - } else if len(ipAddrs) == 0 { - return []netip.Addr{}, ErrIPNotFound - } + ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip6", host) + if err != nil { + return nil, err + } else if len(ipAddrs) == 0 { + return nil, ErrIPNotFound + } - return []netip.Addr{netip.AddrFrom16(*(*[16]byte)(ipAddrs[rand.Intn(len(ipAddrs))]))}, nil + return ipAddrs, nil +} + +// LookupIPv6 with a host, return ipv6 list +func LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { + return LookupIPv6WithResolver(ctx, host, DefaultResolver) +} + +// ResolveIPv6WithResolver same as ResolveIPv6, but with a resolver +func ResolveIPv6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { + ips, err := LookupIPv6WithResolver(ctx, host, r) + if err != nil { + return netip.Addr{}, err + } else if len(ips) == 0 { + return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return []netip.Addr{}, ErrIPNotFound + return ips[rand.Intn(len(ips))], nil } -func ResolveAllIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) { +func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { + return ResolveIPv6WithResolver(ctx, host, DefaultResolver) +} + +// LookupIPWithResolver same as LookupIP, but with a resolver +func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { if node := DefaultHosts.Search(host); node != nil { - if ip := node.Data; ip.Is4() { - return []netip.Addr{node.Data}, nil - } + return []netip.Addr{node.Data()}, nil } - ip, err := netip.ParseAddr(host) - if err == nil { - if ip.Is4() || ip.Is4In6() { - return []netip.Addr{ip}, nil + if r != nil { + if DisableIPv6 { + return r.LookupIPv4(ctx, host) } - return []netip.Addr{}, ErrIPVersion + return r.LookupIP(ctx, host) + } else if DisableIPv6 { + return LookupIPv4(ctx, host) } - if r != nil { - return r.ResolveAllIPv4(host) + if ip, err := netip.ParseAddr(host); err == nil { + return []netip.Addr{ip}, nil } - if DefaultResolver == nil { - ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout) - defer cancel() - ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host) - if err != nil { - return []netip.Addr{}, err - } else if len(ipAddrs) == 0 { - return []netip.Addr{}, ErrIPNotFound - } + ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, ErrIPNotFound + } - ip := ipAddrs[rand.Intn(len(ipAddrs))].To4() - if ip == nil { - return []netip.Addr{}, ErrIPVersion - } + return ips, nil +} - return []netip.Addr{netip.AddrFrom4(*(*[4]byte)(ip))}, nil - } - return []netip.Addr{}, ErrIPNotFound +// LookupIP with a host, return ip +func LookupIP(ctx context.Context, host string) ([]netip.Addr, error) { + return LookupIPWithResolver(ctx, host, DefaultResolver) } -func ResolveAllIPWithResolver(host string, r Resolver) ([]netip.Addr, error) { - if node := DefaultHosts.Search(host); node != nil { - return []netip.Addr{node.Data}, nil +// ResolveIPWithResolver same as ResolveIP, but with a resolver +func ResolveIPWithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { + ips, err := LookupIPWithResolver(ctx, host, r) + if err != nil { + return netip.Addr{}, err + } else if len(ips) == 0 { + return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } + return ips[rand.Intn(len(ips))], nil +} - ip, err := netip.ParseAddr(host) - if err == nil { - return []netip.Addr{ip}, nil - } +// ResolveIP with a host, return ip +func ResolveIP(ctx context.Context, host string) (netip.Addr, error) { + return ResolveIPWithResolver(ctx, host, DefaultResolver) +} - if r != nil { - if DisableIPv6 { - return r.ResolveAllIPv4(host) +// ResolveIPv4ProxyServerHost proxies server host only +func ResolveIPv4ProxyServerHost(ctx context.Context, host string) (netip.Addr, error) { + if ProxyServerHostResolver != nil { + if ip, err := ResolveIPv4WithResolver(ctx, host, ProxyServerHostResolver); err != nil { + return ResolveIPv4(ctx, host) + } else { + return ip, nil } - - return r.ResolveAllIP(host) - } else if DisableIPv6 { - return ResolveAllIPv4(host) } + return ResolveIPv4(ctx, host) +} - if DefaultResolver == nil { - ipAddr, err := net.ResolveIPAddr("ip", host) - if err != nil { - return []netip.Addr{}, err +// ResolveIPv6ProxyServerHost proxies server host only +func ResolveIPv6ProxyServerHost(ctx context.Context, host string) (netip.Addr, error) { + if ProxyServerHostResolver != nil { + if ip, err := ResolveIPv6WithResolver(ctx, host, ProxyServerHostResolver); err != nil { + return ResolveIPv6(ctx, host) + } else { + return ip, nil } - - return []netip.Addr{nnip.IpToAddr(ipAddr.IP)}, nil } - return []netip.Addr{}, ErrIPNotFound -} - -func ResolveAllIP(host string) ([]netip.Addr, error) { - return ResolveAllIPWithResolver(host, DefaultResolver) -} - -func ResolveAllIPv4(host string) ([]netip.Addr, error) { - return ResolveAllIPv4WithResolver(host, DefaultResolver) + return ResolveIPv6(ctx, host) } -func ResolveAllIPv6(host string) ([]netip.Addr, error) { - return ResolveAllIPv6WithResolver(host, DefaultResolver) +// ResolveProxyServerHost proxies server host only +func ResolveProxyServerHost(ctx context.Context, host string) (netip.Addr, error) { + if ProxyServerHostResolver != nil { + if ip, err := ResolveIPWithResolver(ctx, host, ProxyServerHostResolver); err != nil { + return ResolveIP(ctx, host) + } else { + return ip, err + } + } + return ResolveIP(ctx, host) } -func ResolveAllIPv6ProxyServerHost(host string) ([]netip.Addr, error) { +func LookupIPv6ProxyServerHost(ctx context.Context, host string) ([]netip.Addr, error) { if ProxyServerHostResolver != nil { - return ResolveAllIPv6WithResolver(host, ProxyServerHostResolver) + return LookupIPv6WithResolver(ctx, host, ProxyServerHostResolver) } - return ResolveAllIPv6(host) + return LookupIPv6(ctx, host) } -func ResolveAllIPv4ProxyServerHost(host string) ([]netip.Addr, error) { +func LookupIPv4ProxyServerHost(ctx context.Context, host string) ([]netip.Addr, error) { if ProxyServerHostResolver != nil { - return ResolveAllIPv4WithResolver(host, ProxyServerHostResolver) + return LookupIPv4WithResolver(ctx, host, ProxyServerHostResolver) } - return ResolveAllIPv4(host) + return LookupIPv4(ctx, host) } -func ResolveAllIPProxyServerHost(host string) ([]netip.Addr, error) { +func LookupIPProxyServerHost(ctx context.Context, host string) ([]netip.Addr, error) { if ProxyServerHostResolver != nil { - return ResolveAllIPWithResolver(host, ProxyServerHostResolver) + return LookupIPWithResolver(ctx, host, ProxyServerHostResolver) } - return ResolveAllIP(host) + return LookupIP(ctx, host) } diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go index 529e01b7a9..df8e9a54d0 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -35,6 +35,10 @@ func (f *Fetcher[V]) Name() string { return f.name } +func (f *Fetcher[V]) Vehicle() types.Vehicle { + return f.vehicle +} + func (f *Fetcher[V]) VehicleType() types.VehicleType { return f.vehicle.Type() } diff --git a/component/resource/vehicle.go b/component/resource/vehicle.go index c6e92e5216..927a9604aa 100644 --- a/component/resource/vehicle.go +++ b/component/resource/vehicle.go @@ -2,7 +2,7 @@ package resource import ( "context" - netHttp "github.com/Dreamacro/clash/component/http" + clashHttp "github.com/Dreamacro/clash/component/http" types "github.com/Dreamacro/clash/constant/provider" "io" "net/http" @@ -35,6 +35,10 @@ type HTTPVehicle struct { path string } +func (h *HTTPVehicle) Url() string { + return h.url +} + func (h *HTTPVehicle) Type() types.VehicleType { return types.HTTP } @@ -46,7 +50,7 @@ func (h *HTTPVehicle) Path() string { func (h *HTTPVehicle) Read() ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() - resp, err := netHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil) + resp, err := clashHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil) if err != nil { return nil, err } diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index 6a5e632a3a..fd4fe98e97 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -31,8 +31,8 @@ type SnifferDispatcher struct { sniffers []sniffer.Sniffer - forceDomain *trie.DomainTrie[bool] - skipSNI *trie.DomainTrie[bool] + forceDomain *trie.DomainTrie[struct{}] + skipSNI *trie.DomainTrie[struct{}] portRanges *[]utils.Range[uint16] skipList *cache.LruCache[string, uint8] rwMux sync.RWMutex @@ -112,7 +112,6 @@ func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string) { metadata.Host, host) } - metadata.AddrType = C.AtypDomainName metadata.Host = host metadata.DNSMode = C.DNSNormal } @@ -183,15 +182,15 @@ func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) { return &dispatcher, nil } -func NewSnifferDispatcher(needSniffer []sniffer.Type, forceDomain *trie.DomainTrie[bool], - skipSNI *trie.DomainTrie[bool], ports *[]utils.Range[uint16], +func NewSnifferDispatcher(needSniffer []sniffer.Type, forceDomain *trie.DomainTrie[struct{}], + skipSNI *trie.DomainTrie[struct{}], ports *[]utils.Range[uint16], forceDnsMapping bool, parsePureIp bool) (*SnifferDispatcher, error) { dispatcher := SnifferDispatcher{ enable: true, forceDomain: forceDomain, skipSNI: skipSNI, portRanges: ports, - skipList: cache.NewLRUCache[string, uint8](cache.WithSize[string, uint8](128), cache.WithAge[string, uint8](600)), + skipList: cache.New[string, uint8](cache.WithSize[string, uint8](128), cache.WithAge[string, uint8](600)), forceDnsMapping: forceDnsMapping, parsePureIp: parsePureIp, } diff --git a/component/trie/domain.go b/component/trie/domain.go index 16dd9ae96d..d9463c6e1b 100644 --- a/component/trie/domain.go +++ b/component/trie/domain.go @@ -17,7 +17,7 @@ var ErrInvalidDomain = errors.New("invalid domain") // DomainTrie contains the main logic for adding and searching nodes for domain segments. // support wildcard domain (e.g *.google.com) -type DomainTrie[T comparable] struct { +type DomainTrie[T any] struct { root *Node[T] } @@ -73,14 +73,10 @@ func (t *DomainTrie[T]) insert(parts []string, data T) { // reverse storage domain part to save space for i := len(parts) - 1; i >= 0; i-- { part := parts[i] - if !node.hasChild(part) { - node.addChild(part, newNode(getZero[T]())) - } - - node = node.getChild(part) + node = node.getOrNewChild(part) } - node.Data = data + node.setData(data) } // Search is the most important part of the Trie. @@ -96,7 +92,7 @@ func (t *DomainTrie[T]) Search(domain string) *Node[T] { n := t.search(t.root, parts) - if n == nil || n.Data == getZero[T]() { + if n.isEmpty() { return nil } @@ -109,13 +105,13 @@ func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] { } if c := node.getChild(parts[len(parts)-1]); c != nil { - if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() { + if n := t.search(c, parts[:len(parts)-1]); !n.isEmpty() { return n } } if c := node.getChild(wildcard); c != nil { - if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() { + if n := t.search(c, parts[:len(parts)-1]); !n.isEmpty() { return n } } @@ -123,7 +119,11 @@ func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] { return node.getChild(dotWildcard) } +func (t *DomainTrie[T]) Optimize() { + t.root.optimize() +} + // New returns a new, empty Trie. -func New[T comparable]() *DomainTrie[T] { - return &DomainTrie[T]{root: newNode[T](getZero[T]())} +func New[T any]() *DomainTrie[T] { + return &DomainTrie[T]{root: newNode[T]()} } diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go index ced44d0355..c54b3d3b86 100644 --- a/component/trie/domain_test.go +++ b/component/trie/domain_test.go @@ -23,7 +23,7 @@ func TestTrie_Basic(t *testing.T) { node := tree.Search("example.com") assert.NotNil(t, node) - assert.True(t, node.Data == localIP) + assert.True(t, node.Data() == localIP) assert.NotNil(t, tree.Insert("", localIP)) assert.Nil(t, tree.Search("")) assert.NotNil(t, tree.Search("localhost")) @@ -75,7 +75,7 @@ func TestTrie_Priority(t *testing.T) { assertFn := func(domain string, data int) { node := tree.Search(domain) assert.NotNil(t, node) - assert.Equal(t, data, node.Data) + assert.Equal(t, data, node.Data()) } for idx, domain := range domains { diff --git a/component/trie/node.go b/component/trie/node.go index 1545d88050..e19b40acd2 100644 --- a/component/trie/node.go +++ b/component/trie/node.go @@ -1,13 +1,24 @@ package trie +import "strings" + // Node is the trie's node -type Node[T comparable] struct { - children map[string]*Node[T] - Data T +type Node[T any] struct { + childMap map[string]*Node[T] + childNode *Node[T] // optimize for only one child + childStr string + inited bool + data T } func (n *Node[T]) getChild(s string) *Node[T] { - return n.children[s] + if n.childMap == nil { + if n.childNode != nil && n.childStr == s { + return n.childNode + } + return nil + } + return n.childMap[s] } func (n *Node[T]) hasChild(s string) bool { @@ -15,17 +26,100 @@ func (n *Node[T]) hasChild(s string) bool { } func (n *Node[T]) addChild(s string, child *Node[T]) { - n.children[s] = child + if n.childMap == nil { + if n.childNode == nil { + n.childStr = s + n.childNode = child + return + } + n.childMap = map[string]*Node[T]{} + if n.childNode != nil { + n.childMap[n.childStr] = n.childNode + } + n.childStr = "" + n.childNode = nil + } + + n.childMap[s] = child } -func newNode[T comparable](data T) *Node[T] { - return &Node[T]{ - Data: data, - children: map[string]*Node[T]{}, +func (n *Node[T]) getOrNewChild(s string) *Node[T] { + node := n.getChild(s) + if node == nil { + node = newNode[T]() + n.addChild(s, node) + } + return node +} + +func (n *Node[T]) optimize() { + if len(n.childStr) > 0 { + n.childStr = strClone(n.childStr) + } + if n.childNode != nil { + n.childNode.optimize() + } + if n.childMap == nil { + return + } + switch len(n.childMap) { + case 0: + n.childMap = nil + return + case 1: + for key := range n.childMap { + n.childStr = key + n.childNode = n.childMap[key] + } + n.childMap = nil + n.optimize() + return + } + children := make(map[string]*Node[T], len(n.childMap)) // avoid map reallocate memory + for key := range n.childMap { + child := n.childMap[key] + if child == nil { + continue + } + key = strClone(key) + children[key] = child + child.optimize() } + n.childMap = children +} + +func strClone(key string) string { + switch key { // try to save string's memory + case wildcard: + key = wildcard + case dotWildcard: + key = dotWildcard + case complexWildcard: + key = complexWildcard + case domainStep: + key = domainStep + default: + key = strings.Clone(key) + } + return key +} + +func (n *Node[T]) isEmpty() bool { + if n == nil || n.inited == false { + return true + } + return false +} + +func (n *Node[T]) setData(data T) { + n.data = data + n.inited = true +} + +func (n *Node[T]) Data() T { + return n.data } -func getZero[T comparable]() T { - var result T - return result +func newNode[T any]() *Node[T] { + return &Node[T]{} } diff --git a/config/config.go b/config/config.go index ee4fbb1682..da05877fe1 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,6 @@ package config import ( "container/list" - "encoding/json" "errors" "fmt" "net" @@ -14,14 +13,11 @@ import ( "strings" "time" - "github.com/Dreamacro/clash/common/utils" - R "github.com/Dreamacro/clash/rules" - RP "github.com/Dreamacro/clash/rules/provider" - "github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/adapter/provider" + "github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/fakeip" @@ -30,10 +26,13 @@ import ( "github.com/Dreamacro/clash/component/trie" C "github.com/Dreamacro/clash/constant" providerTypes "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/constant/sniffer" snifferTypes "github.com/Dreamacro/clash/constant/sniffer" "github.com/Dreamacro/clash/dns" + L "github.com/Dreamacro/clash/listener" + LC "github.com/Dreamacro/clash/listener/config" "github.com/Dreamacro/clash/log" + R "github.com/Dreamacro/clash/rules" + RP "github.com/Dreamacro/clash/rules/provider" T "github.com/Dreamacro/clash/tunnel" "gopkg.in/yaml.v3" @@ -53,29 +52,33 @@ type General struct { GeodataLoader string `json:"geodata-loader"` TCPConcurrent bool `json:"tcp-concurrent"` EnableProcess bool `json:"enable-process"` - Tun Tun `json:"tun"` Sniffing bool `json:"sniffing"` EBpf EBpf `json:"-"` } // Inbound config type Inbound struct { - Port int `json:"port"` - SocksPort int `json:"socks-port"` - RedirPort int `json:"redir-port"` - TProxyPort int `json:"tproxy-port"` - MixedPort int `json:"mixed-port"` - Authentication []string `json:"authentication"` - AllowLan bool `json:"allow-lan"` - BindAddress string `json:"bind-address"` - InboundTfo bool `json:"inbound-tfo"` + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` + Tun LC.Tun `json:"tun"` + TuicServer LC.TuicServer `json:"tuic-server"` + ShadowSocksConfig string `json:"ss-config"` + VmessConfig string `json:"vmess-config"` + Authentication []string `json:"authentication"` + AllowLan bool `json:"allow-lan"` + BindAddress string `json:"bind-address"` + InboundTfo bool `json:"inbound-tfo"` } // Controller config type Controller struct { - ExternalController string `json:"-"` - ExternalUI string `json:"-"` - Secret string `json:"-"` + ExternalController string `json:"-"` + ExternalControllerTLS string `json:"-"` + ExternalUI string `json:"-"` + Secret string `json:"-"` } // DNS config @@ -110,81 +113,9 @@ type Profile struct { StoreFakeIP bool `yaml:"store-fake-ip"` } -// Tun config -type Tun struct { - Enable bool `yaml:"enable" json:"enable"` - Device string `yaml:"device" json:"device"` - Stack C.TUNStack `yaml:"stack" json:"stack"` - DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"` - AutoRoute bool `yaml:"auto-route" json:"auto-route"` - AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` - RedirectToTun []string `yaml:"-" json:"-"` - - MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - Inet4Address []ListenPrefix `yaml:"inet4-address" json:"inet4_address,omitempty"` - Inet6Address []ListenPrefix `yaml:"inet6-address" json:"inet6_address,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"` - Inet4RouteAddress []ListenPrefix `yaml:"inet4_route_address" json:"inet4_route_address,omitempty"` - Inet6RouteAddress []ListenPrefix `yaml:"inet6_route_address" json:"inet6_route_address,omitempty"` - IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"` - IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"` - ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"` - ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"` - IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"` - IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"` - ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"` - EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` - UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"` -} - -type ListenPrefix netip.Prefix - -func (p ListenPrefix) MarshalJSON() ([]byte, error) { - prefix := netip.Prefix(p) - if !prefix.IsValid() { - return json.Marshal(nil) - } - return json.Marshal(prefix.String()) -} - -func (p ListenPrefix) MarshalYAML() (interface{}, error) { - prefix := netip.Prefix(p) - if !prefix.IsValid() { - return nil, nil - } - return prefix.String(), nil -} - -func (p *ListenPrefix) UnmarshalJSON(bytes []byte) error { - var value string - err := json.Unmarshal(bytes, &value) - if err != nil { - return err - } - prefix, err := netip.ParsePrefix(value) - if err != nil { - return err - } - *p = ListenPrefix(prefix) - return nil -} - -func (p *ListenPrefix) UnmarshalYAML(node *yaml.Node) error { - var value string - err := node.Decode(&value) - if err != nil { - return err - } - prefix, err := netip.ParsePrefix(value) - if err != nil { - return err - } - *p = ListenPrefix(prefix) - return nil -} - -func (p ListenPrefix) Build() netip.Prefix { - return netip.Prefix(p) +type TLS struct { + Certificate string `yaml:"certificate"` + PrivateKey string `yaml:"private-key"` } // IPTables config @@ -196,10 +127,10 @@ type IPTables struct { type Sniffer struct { Enable bool - Sniffers []sniffer.Type - Reverses *trie.DomainTrie[bool] - ForceDomain *trie.DomainTrie[bool] - SkipDomain *trie.DomainTrie[bool] + Sniffers []snifferTypes.Type + Reverses *trie.DomainTrie[struct{}] + ForceDomain *trie.DomainTrie[struct{}] + SkipDomain *trie.DomainTrie[struct{}] Ports *[]utils.Range[uint16] ForceDnsMapping bool ParsePureIp bool @@ -213,19 +144,21 @@ type Experimental struct { // Config is clash config manager type Config struct { General *General - Tun *Tun IPTables *IPTables DNS *DNS Experimental *Experimental Hosts *trie.DomainTrie[netip.Addr] Profile *Profile Rules []C.Rule - SubRules *map[string][]C.Rule + SubRules map[string][]C.Rule Users []auth.AuthUser Proxies map[string]C.Proxy + Listeners map[string]C.InboundListener Providers map[string]providerTypes.ProxyProvider RuleProviders map[string]providerTypes.RuleProvider + Tunnels []LC.Tunnel Sniffer *Sniffer + TLS *TLS } type RawDNS struct { @@ -263,45 +196,62 @@ type RawTun struct { RedirectToTun []string `yaml:"-" json:"-"` MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - //Inet4Address []ListenPrefix `yaml:"inet4-address" json:"inet4_address,omitempty"` - Inet6Address []ListenPrefix `yaml:"inet6-address" json:"inet6_address,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"` - Inet4RouteAddress []ListenPrefix `yaml:"inet4_route_address" json:"inet4_route_address,omitempty"` - Inet6RouteAddress []ListenPrefix `yaml:"inet6_route_address" json:"inet6_route_address,omitempty"` - IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"` - IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"` - ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"` - ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"` - IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"` - IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"` - ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"` - EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` - UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"` + //Inet4Address []LC.ListenPrefix `yaml:"inet4-address" json:"inet4_address,omitempty"` + Inet6Address []LC.ListenPrefix `yaml:"inet6-address" json:"inet6_address,omitempty"` + StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"` + Inet4RouteAddress []LC.ListenPrefix `yaml:"inet4_route_address" json:"inet4_route_address,omitempty"` + Inet6RouteAddress []LC.ListenPrefix `yaml:"inet6_route_address" json:"inet6_route_address,omitempty"` + IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"` + IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"` + ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"` + ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"` + IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"` + IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"` + ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"` + EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` + UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"` +} + +type RawTuicServer struct { + Enable bool `yaml:"enable" json:"enable"` + Listen string `yaml:"listen" json:"listen"` + Token []string `yaml:"token" json:"token"` + Certificate string `yaml:"certificate" json:"certificate"` + PrivateKey string `yaml:"private-key" json:"private-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"` + ALPN []string `yaml:"alpn" json:"alpn,omitempty"` + MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` } type RawConfig struct { - Port int `yaml:"port"` - SocksPort int `yaml:"socks-port"` - RedirPort int `yaml:"redir-port"` - TProxyPort int `yaml:"tproxy-port"` - MixedPort int `yaml:"mixed-port"` - InboundTfo bool `yaml:"inbound-tfo"` - Authentication []string `yaml:"authentication"` - AllowLan bool `yaml:"allow-lan"` - BindAddress string `yaml:"bind-address"` - Mode T.TunnelMode `yaml:"mode"` - UnifiedDelay bool `yaml:"unified-delay"` - LogLevel log.LogLevel `yaml:"log-level"` - IPv6 bool `yaml:"ipv6"` - ExternalController string `yaml:"external-controller"` - ExternalUI string `yaml:"external-ui"` - Secret string `yaml:"secret"` - Interface string `yaml:"interface-name"` - RoutingMark int `yaml:"routing-mark"` - GeodataMode bool `yaml:"geodata-mode"` - GeodataLoader string `yaml:"geodata-loader"` - TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` - EnableProcess bool `yaml:"enable-process" json:"enable-process"` + Port int `yaml:"port"` + SocksPort int `yaml:"socks-port"` + RedirPort int `yaml:"redir-port"` + TProxyPort int `yaml:"tproxy-port"` + MixedPort int `yaml:"mixed-port"` + ShadowSocksConfig string `yaml:"ss-config"` + VmessConfig string `yaml:"vmess-config"` + InboundTfo bool `yaml:"inbound-tfo"` + Authentication []string `yaml:"authentication"` + AllowLan bool `yaml:"allow-lan"` + BindAddress string `yaml:"bind-address"` + Mode T.TunnelMode `yaml:"mode"` + UnifiedDelay bool `yaml:"unified-delay"` + LogLevel log.LogLevel `yaml:"log-level"` + IPv6 bool `yaml:"ipv6"` + ExternalController string `yaml:"external-controller"` + ExternalControllerTLS string `yaml:"external-controller-tls"` + ExternalUI string `yaml:"external-ui"` + Secret string `yaml:"secret"` + Interface string `yaml:"interface-name"` + RoutingMark int `yaml:"routing-mark"` + Tunnels []LC.Tunnel `yaml:"tunnels"` + GeodataMode bool `yaml:"geodata-mode"` + GeodataLoader string `yaml:"geodata-loader"` + TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` + EnableProcess bool `yaml:"enable-process" json:"enable-process"` Sniffer RawSniffer `yaml:"sniffer"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` @@ -309,6 +259,7 @@ type RawConfig struct { Hosts map[string]string `yaml:"hosts"` DNS RawDNS `yaml:"dns"` Tun RawTun `yaml:"tun"` + TuicServer RawTuicServer `yaml:"tuic-server"` EBpf EBpf `yaml:"ebpf"` IPTables IPTables `yaml:"iptables"` Experimental Experimental `yaml:"experimental"` @@ -318,6 +269,8 @@ type RawConfig struct { ProxyGroup []map[string]any `yaml:"proxy-groups"` Rule []string `yaml:"rules"` SubRules map[string][]string `yaml:"sub-rules"` + RawTLS TLS `yaml:"tls"` + Listeners []map[string]any `yaml:"listeners"` } type RawGeoXUrl struct { @@ -383,7 +336,19 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query AutoRoute: true, AutoDetectInterface: true, - Inet6Address: []ListenPrefix{ListenPrefix(netip.MustParsePrefix("fdfe:dcba:9876::1/126"))}, + Inet6Address: []LC.ListenPrefix{LC.ListenPrefix(netip.MustParsePrefix("fdfe:dcba:9876::1/126"))}, + }, + TuicServer: RawTuicServer{ + Enable: false, + Token: nil, + Certificate: "", + PrivateKey: "", + Listen: "", + CongestionController: "", + MaxIdleTime: 15000, + AuthenticationTimeout: 1000, + ALPN: []string{"h3"}, + MaxUdpRelayPacketSize: 1500, }, EBpf: EBpf{ RedirectToTun: []string{}, @@ -455,6 +420,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config.Experimental = &rawCfg.Experimental config.Profile = &rawCfg.Profile config.IPTables = &rawCfg.IPTables + config.TLS = &rawCfg.RawTLS general, err := parseGeneral(rawCfg) if err != nil { @@ -463,7 +429,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config.General = general dialer.DefaultInterface.Store(config.General.Interface) - proxies, providers, err := parseProxies(rawCfg) if err != nil { return nil, err @@ -471,14 +436,26 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config.Proxies = proxies config.Providers = providers - subRules, ruleProviders, err := parseSubRules(rawCfg, proxies) + listener, err := parseListeners(rawCfg) + if err != nil { + return nil, err + } + config.Listeners = listener + + log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) + ruleProviders, err := parseRuleProviders(rawCfg) if err != nil { return nil, err } - config.SubRules = subRules config.RuleProviders = ruleProviders - rules, err := parseRules(rawCfg, proxies, subRules) + subRules, err := parseSubRules(rawCfg, proxies) + if err != nil { + return nil, err + } + config.SubRules = subRules + + rules, err := parseRules(rawCfg.Rule, proxies, subRules, "rules") if err != nil { return nil, err } @@ -496,14 +473,28 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.DNS = dnsCfg - tunCfg, err := parseTun(rawCfg.Tun, config.General, dnsCfg) + err = parseTun(rawCfg.Tun, config.General) + if err != nil { + return nil, err + } + + err = parseTuicServer(rawCfg.TuicServer, config.General) if err != nil { return nil, err } - config.Tun = tunCfg config.Users = parseAuthentication(rawCfg.Authentication) + config.Tunnels = rawCfg.Tunnels + // verify tunnels + for _, t := range config.Tunnels { + if len(t.Proxy) > 0 { + if _, ok := config.Proxies[t.Proxy]; !ok { + return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy) + } + } + } + config.Sniffer, err = parseSniffer(rawCfg.Sniffer) if err != nil { return nil, err @@ -511,6 +502,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm + return config, nil } @@ -520,7 +512,6 @@ func parseGeneral(cfg *RawConfig) (*General, error) { // checkout externalUI exist if externalUI != "" { externalUI = C.Path.Resolve(externalUI) - if _, err := os.Stat(externalUI); os.IsNotExist(err) { return nil, fmt.Errorf("external-ui: %s not exist", externalUI) } @@ -528,19 +519,22 @@ func parseGeneral(cfg *RawConfig) (*General, error) { cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun return &General{ Inbound: Inbound{ - Port: cfg.Port, - SocksPort: cfg.SocksPort, - RedirPort: cfg.RedirPort, - TProxyPort: cfg.TProxyPort, - MixedPort: cfg.MixedPort, - AllowLan: cfg.AllowLan, - BindAddress: cfg.BindAddress, - InboundTfo: cfg.InboundTfo, + Port: cfg.Port, + SocksPort: cfg.SocksPort, + RedirPort: cfg.RedirPort, + TProxyPort: cfg.TProxyPort, + MixedPort: cfg.MixedPort, + ShadowSocksConfig: cfg.ShadowSocksConfig, + VmessConfig: cfg.VmessConfig, + AllowLan: cfg.AllowLan, + BindAddress: cfg.BindAddress, + InboundTfo: cfg.InboundTfo, }, Controller: Controller{ - ExternalController: cfg.ExternalController, - ExternalUI: cfg.ExternalUI, - Secret: cfg.Secret, + ExternalController: cfg.ExternalController, + ExternalUI: cfg.ExternalUI, + Secret: cfg.Secret, + ExternalControllerTLS: cfg.ExternalControllerTLS, }, UnifiedDelay: cfg.UnifiedDelay, Mode: cfg.Mode, @@ -659,79 +653,62 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ return proxies, providersMap, nil } -func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules *map[string][]C.Rule, ruleProviders map[string]providerTypes.RuleProvider, err error) { +func parseListeners(cfg *RawConfig) (listeners map[string]C.InboundListener, err error) { + listeners = make(map[string]C.InboundListener) + for index, mapping := range cfg.Listeners { + listener, err := L.ParseListener(mapping) + if err != nil { + return nil, fmt.Errorf("proxy %d: %w", index, err) + } + + if _, exist := mapping[listener.Name()]; exist { + return nil, fmt.Errorf("listener %s is the duplicate name", listener.Name()) + } + + listeners[listener.Name()] = listener + + } + return +} + +func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes.RuleProvider, err error) { ruleProviders = map[string]providerTypes.RuleProvider{} - subRules = &map[string][]C.Rule{} - log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) // parse rule provider for name, mapping := range cfg.RuleProvider { rp, err := RP.ParseRuleProvider(name, mapping, R.ParseRule) if err != nil { - return nil, nil, err + return nil, err } ruleProviders[name] = rp RP.SetRuleProvider(rp) } + return +} +func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) { + subRules = map[string][]C.Rule{} for name, rawRules := range cfg.SubRules { + if len(name) == 0 { + return nil, fmt.Errorf("sub-rule name is empty") + } var rules []C.Rule - for idx, line := range rawRules { - rawRule := trimArr(strings.Split(line, ",")) - var ( - payload string - target string - params []string - ruleName = strings.ToUpper(rawRule[0]) - ) - - l := len(rawRule) - - if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" { - target = rawRule[l-1] - payload = strings.Join(rawRule[1:l-1], ",") - } else { - if l < 2 { - return nil, nil, fmt.Errorf("sub-rules[%d] [%s] error: format invalid", idx, line) - } - if l < 4 { - rawRule = append(rawRule, make([]string, 4-l)...) - } - if ruleName == "MATCH" { - l = 2 - } - if l >= 3 { - l = 3 - payload = rawRule[1] - } - target = rawRule[l-1] - params = rawRule[l:] - } - - if _, ok := proxies[target]; !ok && ruleName != "SUB-RULE" { - return nil, nil, fmt.Errorf("sub-rules[%d:%s] [%s] error: proxy [%s] not found", idx, name, line, target) - } - - params = trimArr(params) - parsed, parseErr := R.ParseRule(ruleName, payload, target, params, subRules) - if parseErr != nil { - return nil, nil, fmt.Errorf("sub-rules[%d] [%s] error: %s", idx, line, parseErr.Error()) - } - - rules = append(rules, parsed) + rules, err = parseRules(rawRules, proxies, subRules, fmt.Sprintf("sub-rules[%s]", name)) + if err != nil { + return nil, err } - (*subRules)[name] = rules + subRules[name] = rules } if err = verifySubRule(subRules); err != nil { - return nil, nil, err + return nil, err } return } -func verifySubRule(subRules *map[string][]C.Rule) error { - for name := range *subRules { +func verifySubRule(subRules map[string][]C.Rule) error { + for name := range subRules { err := verifySubRuleCircularReferences(name, subRules, []string{}) if err != nil { return err @@ -740,7 +717,7 @@ func verifySubRule(subRules *map[string][]C.Rule) error { return nil } -func verifySubRuleCircularReferences(n string, subRules *map[string][]C.Rule, arr []string) error { +func verifySubRuleCircularReferences(n string, subRules map[string][]C.Rule, arr []string) error { isInArray := func(v string, array []string) bool { for _, c := range array { if v == c { @@ -751,9 +728,9 @@ func verifySubRuleCircularReferences(n string, subRules *map[string][]C.Rule, ar } arr = append(arr, n) - for i, rule := range (*subRules)[n] { + for i, rule := range subRules[n] { if rule.RuleType() == C.SubRules { - if _, ok := (*subRules)[rule.Adapter()]; !ok { + if _, ok := subRules[rule.Adapter()]; !ok { return fmt.Errorf("sub-rule[%d:%s] error: [%s] not found", i, n, rule.Adapter()) } if isInArray(rule.Adapter(), arr) { @@ -769,9 +746,8 @@ func verifySubRuleCircularReferences(n string, subRules *map[string][]C.Rule, ar return nil } -func parseRules(cfg *RawConfig, proxies map[string]C.Proxy, subRules *map[string][]C.Rule) ([]C.Rule, error) { +func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[string][]C.Rule, format string) ([]C.Rule, error) { var rules []C.Rule - rulesConfig := cfg.Rule // parse rules for idx, line := range rulesConfig { @@ -790,7 +766,7 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy, subRules *map[string payload = strings.Join(rule[1:l-1], ",") } else { if l < 2 { - return nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line) + return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) } if l < 4 { rule = append(rule, make([]string, 4-l)...) @@ -807,16 +783,16 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy, subRules *map[string } if _, ok := proxies[target]; !ok { if ruleName != "SUB-RULE" { - return nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target) - } else if _, ok = (*subRules)[target]; !ok { - return nil, fmt.Errorf("rules[%d] [%s] error: sub-rule [%s] not found", idx, line, target) + return nil, fmt.Errorf("%s[%d] [%s] error: proxy [%s] not found", format, idx, line, target) + } else if _, ok = subRules[target]; !ok { + return nil, fmt.Errorf("%s[%d] [%s] error: sub-rule [%s] not found", format, idx, line, target) } } params = trimArr(params) parsed, parseErr := R.ParseRule(ruleName, payload, target, params, subRules) if parseErr != nil { - return nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error()) + return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error()) } rules = append(rules, parsed) @@ -844,6 +820,7 @@ func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) { _ = tree.Insert(domain, ip) } } + tree.Optimize() return tree, nil } @@ -865,7 +842,7 @@ func hostWithDefaultPort(host string, defPort string) (string, error) { return net.JoinHostPort(hostname, port), nil } -func parseNameServer(servers []string) ([]dns.NameServer, error) { +func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) { var nameservers []dns.NameServer for idx, server := range servers { @@ -878,7 +855,9 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) { return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } - var addr, dnsNetType, proxyAdapter string + proxyAdapter := u.Fragment + + var addr, dnsNetType string params := map[string]string{} switch u.Scheme { case "udp": @@ -891,7 +870,16 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) { addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "tcp-tls" // DNS over TLS case "https": - clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path} + host := u.Host + proxyAdapter = "" + if _, _, err := net.SplitHostPort(host); err != nil && strings.Contains(err.Error(), "missing port in address") { + host = net.JoinHostPort(host, "443") + } else { + if err != nil { + return nil, err + } + } + clearURL := url.URL{Scheme: "https", Host: host, Path: u.Path} addr = clearURL.String() dnsNetType = "https" // DNS over HTTPS if len(u.Fragment) != 0 { @@ -930,17 +918,18 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) { ProxyAdapter: proxyAdapter, Interface: dialer.DefaultInterface, Params: params, + PreferH3: preferH3, }, ) } return nameservers, nil } -func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServer, error) { +func parseNameServerPolicy(nsPolicy map[string]string, preferH3 bool) (map[string]dns.NameServer, error) { policy := map[string]dns.NameServer{} for domain, server := range nsPolicy { - nameservers, err := parseNameServer([]string{server}) + nameservers, err := parseNameServer([]string{server}, preferH3) if err != nil { return nil, err } @@ -1020,26 +1009,26 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R }, } var err error - if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer); err != nil { + if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer, cfg.PreferH3); err != nil { return nil, err } - if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback); err != nil { + if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback, cfg.PreferH3); err != nil { return nil, err } - if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy); err != nil { + if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, cfg.PreferH3); err != nil { return nil, err } - if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver); err != nil { + if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver, cfg.PreferH3); err != nil { return nil, err } if len(cfg.DefaultNameserver) == 0 { return nil, errors.New("default nameserver should have at least one nameserver") } - if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver); err != nil { + if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver, cfg.PreferH3); err != nil { return nil, err } // check default nameserver is pure ip addr @@ -1055,35 +1044,38 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R } } + fakeIPRange, err := netip.ParsePrefix(cfg.FakeIPRange) + T.SetFakeIPRange(fakeIPRange) if cfg.EnhancedMode == C.DNSFakeIP { - ipnet, err := netip.ParsePrefix(cfg.FakeIPRange) if err != nil { return nil, err } - var host *trie.DomainTrie[bool] + var host *trie.DomainTrie[struct{}] // fake ip skip host filter if len(cfg.FakeIPFilter) != 0 { - host = trie.New[bool]() + host = trie.New[struct{}]() for _, domain := range cfg.FakeIPFilter { - _ = host.Insert(domain, true) + _ = host.Insert(domain, struct{}{}) } + host.Optimize() } if len(dnsCfg.Fallback) != 0 { if host == nil { - host = trie.New[bool]() + host = trie.New[struct{}]() } for _, fb := range dnsCfg.Fallback { if net.ParseIP(fb.Addr) != nil { continue } - _ = host.Insert(fb.Addr, true) + _ = host.Insert(fb.Addr, struct{}{}) } + host.Optimize() } pool, err := fakeip.New(fakeip.Options{ - IPNet: &ipnet, + IPNet: &fakeIPRange, Size: 1000, Host: host, Persistence: rawCfg.Profile.StoreFakeIP, @@ -1126,41 +1118,28 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser { return users } -func parseTun(rawTun RawTun, general *General, dnsCfg *DNS) (*Tun, error) { - var dnsHijack []netip.AddrPort - - for _, d := range rawTun.DNSHijack { - if _, after, ok := strings.Cut(d, "://"); ok { - d = after - } - d = strings.Replace(d, "any", "0.0.0.0", 1) - addrPort, err := netip.ParseAddrPort(d) - if err != nil { - return nil, fmt.Errorf("parse dns-hijack url error: %w", err) - } - - dnsHijack = append(dnsHijack, addrPort) - } - - var tunAddressPrefix netip.Prefix - if dnsCfg.FakeIPRange != nil { - tunAddressPrefix = *dnsCfg.FakeIPRange.IPNet() - } else { +func parseTun(rawTun RawTun, general *General) error { + tunAddressPrefix := T.FakeIPRange() + if !tunAddressPrefix.IsValid() { tunAddressPrefix = netip.MustParsePrefix("198.18.0.1/16") } tunAddressPrefix = netip.PrefixFrom(tunAddressPrefix.Addr(), 30) - return &Tun{ + if !general.IPv6 || !verifyIP6() { + rawTun.Inet6Address = nil + } + + general.Tun = LC.Tun{ Enable: rawTun.Enable, Device: rawTun.Device, Stack: rawTun.Stack, - DNSHijack: dnsHijack, + DNSHijack: rawTun.DNSHijack, AutoRoute: rawTun.AutoRoute, AutoDetectInterface: rawTun.AutoDetectInterface, RedirectToTun: rawTun.RedirectToTun, MTU: rawTun.MTU, - Inet4Address: []ListenPrefix{ListenPrefix(tunAddressPrefix)}, + Inet4Address: []LC.ListenPrefix{LC.ListenPrefix(tunAddressPrefix)}, Inet6Address: rawTun.Inet6Address, StrictRoute: rawTun.StrictRoute, Inet4RouteAddress: rawTun.Inet4RouteAddress, @@ -1174,7 +1153,25 @@ func parseTun(rawTun RawTun, general *General, dnsCfg *DNS) (*Tun, error) { ExcludePackage: rawTun.ExcludePackage, EndpointIndependentNat: rawTun.EndpointIndependentNat, UDPTimeout: rawTun.UDPTimeout, - }, nil + } + + return nil +} + +func parseTuicServer(rawTuic RawTuicServer, general *General) error { + general.TuicServer = LC.TuicServer{ + Enable: rawTuic.Enable, + Listen: rawTuic.Listen, + Token: rawTuic.Token, + Certificate: rawTuic.Certificate, + PrivateKey: rawTuic.PrivateKey, + CongestionController: rawTuic.CongestionController, + MaxIdleTime: rawTuic.MaxIdleTime, + AuthenticationTimeout: rawTuic.AuthenticationTimeout, + ALPN: rawTuic.ALPN, + MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize, + } + return nil } func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { @@ -1232,21 +1229,23 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { for st := range loadSniffer { sniffer.Sniffers = append(sniffer.Sniffers, st) } - sniffer.ForceDomain = trie.New[bool]() + sniffer.ForceDomain = trie.New[struct{}]() for _, domain := range snifferRaw.ForceDomain { - err := sniffer.ForceDomain.Insert(domain, true) + err := sniffer.ForceDomain.Insert(domain, struct{}{}) if err != nil { return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) } } + sniffer.ForceDomain.Optimize() - sniffer.SkipDomain = trie.New[bool]() + sniffer.SkipDomain = trie.New[struct{}]() for _, domain := range snifferRaw.SkipDomain { - err := sniffer.SkipDomain.Insert(domain, true) + err := sniffer.SkipDomain.Insert(domain, struct{}{}) if err != nil { return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) } } + sniffer.SkipDomain.Optimize() return sniffer, nil } diff --git a/config/utils.go b/config/utils.go index 1d49552fea..2c470618a2 100644 --- a/config/utils.go +++ b/config/utils.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net" "strings" "github.com/Dreamacro/clash/adapter/outboundgroup" @@ -146,3 +147,25 @@ func proxyGroupsDagSort(groupsConfig []map[string]any) error { } return fmt.Errorf("loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements) } + +func verifyIP6() bool { + addrs, err := net.InterfaceAddrs() + if err != nil { + return false + } + for _, addr := range addrs { + ipNet, isIpNet := addr.(*net.IPNet) + if isIpNet && !ipNet.IP.IsLoopback() { + if ipNet.IP.To16() != nil { + s := ipNet.IP.String() + for i := 0; i < len(s); i++ { + switch s[i] { + case ':': + return true + } + } + } + } + } + return false +} diff --git a/constant/adapters.go b/constant/adapters.go index 7fc31667b9..c13a85c44d 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "time" "github.com/Dreamacro/clash/component/dialer" @@ -31,6 +32,8 @@ const ( Vless Trojan Hysteria + WireGuard + Tuic ) const ( @@ -79,13 +82,20 @@ type PacketConn interface { // WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error) } +type Dialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) + ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) +} + type ProxyAdapter interface { Name() string Type() AdapterType Addr() string SupportUDP() bool + SupportTFO() bool MarshalJSON() ([]byte, error) + // Deprecated: use DialContextWithDialer and ListenPacketWithDialer instead. // StreamConn wraps a protocol around net.Conn with Metadata. // // Examples: @@ -103,7 +113,10 @@ type ProxyAdapter interface { // SupportUOT return UDP over TCP support SupportUOT() bool - ListenPacketOnStreamConn(c net.Conn, metadata *Metadata) (PacketConn, error) + + SupportWithDialer() bool + DialContextWithDialer(ctx context.Context, dialer Dialer, metadata *Metadata) (Conn, error) + ListenPacketWithDialer(ctx context.Context, dialer Dialer, metadata *Metadata) (PacketConn, error) // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. Unwrap(metadata *Metadata, touch bool) Proxy @@ -165,6 +178,10 @@ func (at AdapterType) String() string { return "Trojan" case Hysteria: return "Hysteria" + case WireGuard: + return "WireGuard" + case Tuic: + return "Tuic" case Relay: return "Relay" @@ -199,3 +216,13 @@ type UDPPacket interface { // LocalAddr returns the source IP/Port of packet LocalAddr() net.Addr } + +type UDPPacketInAddr interface { + InAddr() net.Addr +} + +// PacketAdapter is a UDP Packet adapter for socks/redir/tun +type PacketAdapter interface { + UDPPacket + Metadata() *Metadata +} diff --git a/constant/dns.go b/constant/dns.go index be8b4a1738..da68753c86 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -114,3 +114,14 @@ func NewDNSPrefer(prefer string) DNSPrefer { return DualStack } } + +type HTTPVersion string + +const ( + // HTTPVersion11 is HTTP/1.1. + HTTPVersion11 HTTPVersion = "http/1.1" + // HTTPVersion2 is HTTP/2. + HTTPVersion2 HTTPVersion = "h2" + // HTTPVersion3 is HTTP/3. + HTTPVersion3 HTTPVersion = "h3" +) \ No newline at end of file diff --git a/constant/listener.go b/constant/listener.go index 07782a9e00..a52c194647 100644 --- a/constant/listener.go +++ b/constant/listener.go @@ -1,7 +1,29 @@ package constant +import "net" + type Listener interface { RawAddress() string Address() string Close() error } + +type MultiAddrListener interface { + Close() error + Config() string + AddrList() (addrList []net.Addr) +} + +type InboundListener interface { + Name() string + Listen(tcpIn chan<- ConnContext, udpIn chan<- PacketAdapter) error + Close() error + Address() string + RawAddress() string + Config() InboundConfig +} + +type InboundConfig interface { + Name() string + Equal(config InboundConfig) bool +} diff --git a/constant/metadata.go b/constant/metadata.go index f248cd21df..359329f9cb 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -6,14 +6,12 @@ import ( "net" "net/netip" "strconv" + + "github.com/Dreamacro/clash/transport/socks5" ) // Socks addr type const ( - AtypIPv4 = 1 - AtypDomainName = 3 - AtypIPv6 = 4 - TCP NetWork = iota UDP ALLNet @@ -22,9 +20,13 @@ const ( HTTPS SOCKS4 SOCKS5 + SHADOWSOCKS + VMESS REDIR TPROXY + TUNNEL TUN + TUIC INNER ) @@ -55,12 +57,20 @@ func (t Type) String() string { return "Socks4" case SOCKS5: return "Socks5" + case SHADOWSOCKS: + return "ShadowSocks" + case VMESS: + return "Vmess" case REDIR: return "Redir" case TPROXY: return "TProxy" + case TUNNEL: + return "Tunnel" case TUN: return "Tun" + case TUIC: + return "Tuic" case INNER: return "Inner" default: @@ -79,12 +89,20 @@ func ParseType(t string) (*Type, error) { res = SOCKS4 case "SOCKS5": res = SOCKS5 + case "SHADOWSOCKS": + res = SHADOWSOCKS + case "VMESS": + res = VMESS case "REDIR": res = REDIR case "TPROXY": res = TPROXY + case "TUNNEL": + res = TUNNEL case "TUN": res = TUN + case "TUIC": + res = TUIC case "INNER": res = INNER default: @@ -99,19 +117,23 @@ func (t Type) MarshalJSON() ([]byte, error) { // Metadata is used to store connection address type Metadata struct { - NetWork NetWork `json:"network"` - Type Type `json:"type"` - SrcIP netip.Addr `json:"sourceIP"` - DstIP netip.Addr `json:"destinationIP"` - SrcPort string `json:"sourcePort"` - DstPort string `json:"destinationPort"` - AddrType int `json:"-"` - Host string `json:"host"` - DNSMode DNSMode `json:"dnsMode"` - Uid *int32 `json:"uid"` - Process string `json:"process"` - ProcessPath string `json:"processPath"` - RemoteDst string `json:"remoteDestination"` + NetWork NetWork `json:"network"` + Type Type `json:"type"` + SrcIP netip.Addr `json:"sourceIP"` + DstIP netip.Addr `json:"destinationIP"` + SrcPort string `json:"sourcePort"` + DstPort string `json:"destinationPort"` + InIP netip.Addr `json:"inboundIP"` + InPort string `json:"inboundPort"` + InName string `json:"inboundName"` + Host string `json:"host"` + DNSMode DNSMode `json:"dnsMode"` + Uid *uint32 `json:"uid"` + Process string `json:"process"` + ProcessPath string `json:"processPath"` + SpecialProxy string `json:"specialProxy"` + SpecialRules string `json:"specialRules"` + RemoteDst string `json:"remoteDestination"` } func (m *Metadata) RemoteAddress() string { @@ -138,6 +160,17 @@ func (m *Metadata) SourceDetail() string { } } +func (m *Metadata) AddrType() int { + switch true { + case m.Host != "" || !m.DstIP.IsValid(): + return socks5.AtypDomainName + case m.DstIP.Is4(): + return socks5.AtypIPv4 + default: + return socks5.AtypIPv6 + } +} + func (m *Metadata) Resolved() bool { return m.DstIP.IsValid() } @@ -148,11 +181,6 @@ func (m *Metadata) Pure() *Metadata { if (m.DNSMode == DNSMapping || m.DNSMode == DNSHosts) && m.DstIP.IsValid() { copyM := *m copyM.Host = "" - if copyM.DstIP.Is4() { - copyM.AddrType = AtypIPv4 - } else { - copyM.AddrType = AtypIPv6 - } return ©M } diff --git a/constant/mime/mime.go b/constant/mime/mime.go deleted file mode 100644 index 431457be8a..0000000000 --- a/constant/mime/mime.go +++ /dev/null @@ -1,16 +0,0 @@ -package mime - -import ( - "mime" -) - -var consensusMimes = map[string]string{ - // rfc4329: text/javascript is obsolete, so we need to overwrite mime's builtin - ".js": "application/javascript; charset=utf-8", -} - -func init() { - for ext, typ := range consensusMimes { - mime.AddExtensionType(ext, typ) - } -} diff --git a/constant/provider/interface.go b/constant/provider/interface.go index a56bc0a37b..8bc3c0fed5 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -1,7 +1,7 @@ package provider import ( - C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant" ) // Vehicle Type @@ -65,7 +65,9 @@ type Provider interface { // ProxyProvider interface type ProxyProvider interface { Provider - Proxies() []C.Proxy + Proxies() []constant.Proxy + // Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies. + // Commonly used in DialContext and DialPacketConn Touch() HealthCheck() Version() uint32 @@ -98,7 +100,7 @@ func (rt RuleType) String() string { type RuleProvider interface { Provider Behavior() RuleType - Match(*C.Metadata) bool + Match(*constant.Metadata) bool ShouldResolveIP() bool - AsRule(adaptor string) C.Rule + AsRule(adaptor string) constant.Rule } diff --git a/constant/rule.go b/constant/rule.go index 223dec6e9d..28c629a025 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -13,6 +13,7 @@ const ( SrcIPSuffix SrcPort DstPort + InPort Process ProcessPath RuleSet @@ -52,6 +53,8 @@ func (rt RuleType) String() string { return "SrcPort" case DstPort: return "DstPort" + case InPort: + return "InPort" case Process: return "Process" case ProcessPath: diff --git a/context/dns.go b/context/dns.go index 0be4a1fc2f..5913096187 100644 --- a/context/dns.go +++ b/context/dns.go @@ -1,6 +1,8 @@ package context import ( + "context" + "github.com/gofrs/uuid" "github.com/miekg/dns" ) @@ -12,14 +14,18 @@ const ( ) type DNSContext struct { + context.Context + id uuid.UUID msg *dns.Msg tp string } -func NewDNSContext(msg *dns.Msg) *DNSContext { +func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext { id, _ := uuid.NewV4() return &DNSContext{ + Context: ctx, + id: id, msg: msg, } diff --git a/dns/client.go b/dns/client.go index a377ee42c2..a7bf5eb334 100644 --- a/dns/client.go +++ b/dns/client.go @@ -6,6 +6,7 @@ import ( "fmt" tlsC "github.com/Dreamacro/clash/component/tls" "go.uber.org/atomic" + "math/rand" "net" "net/netip" "strings" @@ -34,15 +35,19 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) ip netip.Addr err error ) - if ip, err = netip.ParseAddr(c.host); err != nil { - if c.r == nil { + if c.r == nil { + // a default ip dns + if ip, err = netip.ParseAddr(c.host); err != nil { return nil, fmt.Errorf("dns %s not a valid ip", c.host) - } else { - if ip, err = resolver.ResolveIPWithResolver(c.host, c.r); err != nil { - return nil, fmt.Errorf("use default dns resolve failed: %w", err) - } - c.host = ip.String() } + } else { + ips, err := resolver.LookupIPWithResolver(ctx, c.host, c.r) + if err != nil { + return nil, fmt.Errorf("use default dns resolve failed: %w", err) + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host) + } + ip = ips[rand.Intn(len(ips))] } network := "udp" @@ -55,13 +60,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) options = append(options, dialer.WithInterface(c.iface.Load())) } - var conn net.Conn - if c.proxyAdapter != "" { - conn, err = dialContextExtra(ctx, c.proxyAdapter, network, ip, c.port, options...) - } else { - conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...) - } - + conn, err := getDialHandler(c.r, c.proxyAdapter, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) if err != nil { return nil, err } diff --git a/dns/dhcp.go b/dns/dhcp.go index a4ad70a52e..1efa3bd19f 100644 --- a/dns/dhcp.go +++ b/dns/dhcp.go @@ -30,7 +30,7 @@ type dhcpClient struct { ifaceAddr *netip.Prefix done chan struct{} - resolver *Resolver + clients []dnsClient err error } @@ -42,15 +42,15 @@ func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { } func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - res, err := d.resolve(ctx) + clients, err := d.resolve(ctx) if err != nil { return nil, err } - return res.ExchangeContext(ctx, m) + return batchExchange(ctx, clients, m) } -func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) { +func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) { d.lock.Lock() invalidated, err := d.invalidate() @@ -65,8 +65,9 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) { ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout) defer cancel() - var res *Resolver + var res []dnsClient dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName) + // dns never empty if err is nil if err == nil { nameserver := make([]NameServer, 0, len(dns)) for _, item := range dns { @@ -76,9 +77,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) { }) } - res = NewResolver(Config{ - Main: nameserver, - }) + res = transform(nameserver, nil) } d.lock.Lock() @@ -87,7 +86,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) { close(done) d.done = nil - d.resolver = res + d.clients = res d.err = err }() } @@ -97,7 +96,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) { for { d.lock.Lock() - res, err, done := d.resolver, d.err, d.done + res, err, done := d.clients, d.err, d.done d.lock.Unlock() diff --git a/dns/doh.go b/dns/doh.go index 8403f7d150..346855787a 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -1,164 +1,706 @@ package dns import ( - "bytes" "context" "crypto/tls" + "encoding/base64" + "errors" "fmt" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - tlsC "github.com/Dreamacro/clash/component/tls" - "github.com/lucas-clemente/quic-go" - "github.com/lucas-clemente/quic-go/http3" - D "github.com/miekg/dns" "io" "net" "net/http" + "net/url" + "runtime" "strconv" + "sync" + "time" + + tlsC "github.com/Dreamacro/clash/component/tls" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/metacubex/quic-go" + "github.com/metacubex/quic-go/http3" + D "github.com/miekg/dns" + "golang.org/x/net/http2" ) +// Values to configure HTTP and HTTP/2 transport. const ( - // dotMimeType is the DoH mimetype that should be used. - dotMimeType = "application/dns-message" + // transportDefaultReadIdleTimeout is the default timeout for pinging + // idle connections in HTTP/2 transport. + transportDefaultReadIdleTimeout = 30 * time.Second + + // transportDefaultIdleConnTimeout is the default timeout for idle + // connections in HTTP transport. + transportDefaultIdleConnTimeout = 5 * time.Minute + + // dohMaxConnsPerHost controls the maximum number of connections for + // each host. + dohMaxConnsPerHost = 1 + dialTimeout = 10 * time.Second + + // dohMaxIdleConns controls the maximum number of connections being idle + // at the same time. + dohMaxIdleConns = 1 + maxElapsedTime = time.Second * 30 ) -type dohClient struct { - url string - transport http.RoundTripper +var DefaultHTTPVersions = []C.HTTPVersion{C.HTTPVersion11, C.HTTPVersion2} + +// dnsOverHTTPS is a struct that implements the Upstream interface for the +// DNS-over-HTTPS protocol. +type dnsOverHTTPS struct { + // The Client's Transport typically has internal state (cached TCP + // connections), so Clients should be reused instead of created as + // needed. Clients are safe for concurrent use by multiple goroutines. + client *http.Client + clientMu sync.Mutex + + // quicConfig is the QUIC configuration that is used if HTTP/3 is enabled + // for this upstream. + quicConfig *quic.Config + quicConfigGuard sync.Mutex + url *url.URL + r *Resolver + httpVersions []C.HTTPVersion + proxyAdapter string } -func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { - return dc.ExchangeContext(context.Background(), m) +// type check +var _ dnsClient = (*dnsOverHTTPS)(nil) + +// newDoH returns the DNS-over-HTTPS Upstream. +func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter string) dnsClient { + u, _ := url.Parse(urlString) + httpVersions := DefaultHTTPVersions + if preferH3 { + httpVersions = append(httpVersions, C.HTTPVersion3) + } + + if params["h3"] == "true" { + httpVersions = []C.HTTPVersion{C.HTTPVersion3} + } + + doh := &dnsOverHTTPS{ + url: u, + r: r, + proxyAdapter: proxyAdapter, + quicConfig: &quic.Config{ + KeepAlivePeriod: QUICKeepAlivePeriod, + TokenStore: newQUICTokenStore(), + }, + httpVersions: httpVersions, + } + + runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) + + return doh } -func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - // https://datatracker.ietf.org/doc/html/rfc8484#section-4.1 - // In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request. - newM := *m - newM.Id = 0 - req, err := dc.newRequest(&newM) +// Address implements the Upstream interface for *dnsOverHTTPS. +func (doh *dnsOverHTTPS) Address() string { return doh.url.String() } +func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: + // In order to maximize HTTP cache friendliness, DoH clients using media + // formats that include the ID field from the DNS message header, such + // as "application/dns-message", SHOULD use a DNS ID of 0 in every DNS + // request. + id := m.Id + m.Id = 0 + defer func() { + // Restore the original ID to not break compatibility with proxies. + m.Id = id + if msg != nil { + msg.Id = id + } + }() + + // Check if there was already an active client before sending the request. + // We'll only attempt to re-connect if there was one. + client, isCached, err := doh.getClient(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to init http client: %w", err) + } + + // Make the first attempt to send the DNS query. + msg, err = doh.exchangeHTTPS(ctx, client, m) + + // Make up to 2 attempts to re-create the HTTP client and send the request + // again. There are several cases (mostly, with QUIC) where this workaround + // is necessary to make HTTP client usable. We need to make 2 attempts in + // the case when the connection was closed (due to inactivity for example) + // AND the server refuses to open a 0-RTT connection. + for i := 0; isCached && doh.shouldRetry(err) && i < 2; i++ { + client, err = doh.resetClient(ctx, err) + if err != nil { + return nil, fmt.Errorf("failed to reset http client: %w", err) + } + + msg, err = doh.exchangeHTTPS(ctx, client, m) + } + + if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { + // If the request failed anyway, make sure we don't use this client. + _, resErr := doh.resetClient(ctx, err) + + return nil, fmt.Errorf("%w (resErr:%v)", err, resErr) + } + + return msg, err +} + +// Exchange implements the Upstream interface for *dnsOverHTTPS. +func (doh *dnsOverHTTPS) Exchange(m *D.Msg) (*D.Msg, error) { + return doh.ExchangeContext(context.Background(), m) +} + +// Close implements the Upstream interface for *dnsOverHTTPS. +func (doh *dnsOverHTTPS) Close() (err error) { + doh.clientMu.Lock() + defer doh.clientMu.Unlock() + + runtime.SetFinalizer(doh, nil) + + if doh.client == nil { + return nil + } + + return doh.closeClient(doh.client) +} + +// closeClient cleans up resources used by client if necessary. Note, that at +// this point it should only be done for HTTP/3 as it may leak due to keep-alive +// connections. +func (doh *dnsOverHTTPS) closeClient(client *http.Client) (err error) { + if isHTTP3(client) { + return client.Transport.(io.Closer).Close() + } + + return nil +} + +// exchangeHTTPS logs the request and its result and calls exchangeHTTPSClient. +func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { + resp, err = doh.exchangeHTTPSClient(ctx, client, req) + return resp, err +} + +// exchangeHTTPSClient sends the DNS query to a DoH resolver using the specified +// http.Client instance. +func (doh *dnsOverHTTPS) exchangeHTTPSClient( + ctx context.Context, + client *http.Client, + req *D.Msg, +) (resp *D.Msg, err error) { + buf, err := req.Pack() + if err != nil { + return nil, fmt.Errorf("packing message: %w", err) + } + + // It appears, that GET requests are more memory-efficient with Golang + // implementation of HTTP/2. + method := http.MethodGet + if isHTTP3(client) { + // If we're using HTTP/3, use http3.MethodGet0RTT to force using 0-RTT. + method = http3.MethodGet0RTT + } + + url := doh.url + url.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) + httpReq, err := http.NewRequestWithContext(ctx, method, url.String(), nil) + if err != nil { + return nil, fmt.Errorf("creating http request to %s: %w", url, err) + } + + httpReq.Header.Set("Accept", "application/dns-message") + httpReq.Header.Set("User-Agent", "") + httpResp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("requesting %s: %w", url, err) } + defer httpResp.Body.Close() - req = req.WithContext(ctx) - msg, err = dc.doRequest(req) + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", url, err) + } + + if httpResp.StatusCode != http.StatusOK { + return nil, + fmt.Errorf( + "expected status %d, got %d from %s", + http.StatusOK, + httpResp.StatusCode, + url, + ) + } + + resp = &D.Msg{} + err = resp.Unpack(body) + if err != nil { + return nil, fmt.Errorf( + "unpacking response from %s: body is %s: %w", + url, + body, + err, + ) + } + + if resp.Id != req.Id { + err = D.ErrId + } + + return resp, err +} + +// shouldRetry checks what error we have received and returns true if we should +// re-create the HTTP client and retry the request. +func (doh *dnsOverHTTPS) shouldRetry(err error) (ok bool) { if err == nil { - msg.Id = m.Id + return false + } + + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // If this is a timeout error, trying to forcibly re-create the HTTP + // client instance. This is an attempt to fix an issue with DoH client + // stalling after a network change. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/3217. + return true + } + + if isQUICRetryError(err) { + return true + } + + return false +} + +// resetClient triggers re-creation of the *http.Client that is used by this +// upstream. This method accepts the error that caused resetting client as +// depending on the error we may also reset the QUIC config. +func (doh *dnsOverHTTPS) resetClient(ctx context.Context, resetErr error) (client *http.Client, err error) { + doh.clientMu.Lock() + defer doh.clientMu.Unlock() + + if errors.Is(resetErr, quic.Err0RTTRejected) { + // Reset the TokenStore only if 0-RTT was rejected. + doh.resetQUICConfig() } - return + + oldClient := doh.client + if oldClient != nil { + closeErr := doh.closeClient(oldClient) + if closeErr != nil { + log.Warnln("warning: failed to close the old http client: %v", closeErr) + } + } + + log.Debugln("re-creating the http client due to %v", resetErr) + doh.client, err = doh.createClient(ctx) + + return doh.client, err +} + +// getQUICConfig returns the QUIC config in a thread-safe manner. Note, that +// this method returns a pointer, it is forbidden to change its properties. +func (doh *dnsOverHTTPS) getQUICConfig() (c *quic.Config) { + doh.quicConfigGuard.Lock() + defer doh.quicConfigGuard.Unlock() + + return doh.quicConfig } -// newRequest returns a new DoH request given a dns.Msg. -func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) { - buf, err := m.Pack() +// resetQUICConfig Re-create the token store to make sure we're not trying to +// use invalid for 0-RTT. +func (doh *dnsOverHTTPS) resetQUICConfig() { + doh.quicConfigGuard.Lock() + defer doh.quicConfigGuard.Unlock() + + doh.quicConfig = doh.quicConfig.Clone() + doh.quicConfig.TokenStore = newQUICTokenStore() +} + +// getClient gets or lazily initializes an HTTP client (and transport) that will +// be used for this DoH resolver. +func (doh *dnsOverHTTPS) getClient(ctx context.Context) (c *http.Client, isCached bool, err error) { + startTime := time.Now() + + doh.clientMu.Lock() + defer doh.clientMu.Unlock() + if doh.client != nil { + return doh.client, true, nil + } + + // Timeout can be exceeded while waiting for the lock. This happens quite + // often on mobile devices. + elapsed := time.Since(startTime) + if elapsed > maxElapsedTime { + return nil, false, fmt.Errorf("timeout exceeded: %s", elapsed) + } + + log.Debugln("creating a new http client") + doh.client, err = doh.createClient(ctx) + + return doh.client, false, err +} + +// createClient creates a new *http.Client instance. The HTTP protocol version +// will depend on whether HTTP3 is allowed and provided by this upstream. Note, +// that we'll attempt to establish a QUIC connection when creating the client in +// order to check whether HTTP3 is supported. +func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error) { + transport, err := doh.createTransport(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("[%s] initializing http transport: %w", doh.url.String(), err) + } + + client := &http.Client{ + Transport: transport, + Timeout: DefaultTimeout, + Jar: nil, } - req, err := http.NewRequest(http.MethodPost, dc.url, bytes.NewReader(buf)) + doh.client = client + + return doh.client, nil +} + +// createTransport initializes an HTTP transport that will be used specifically +// for this DoH resolver. This HTTP transport ensures that the HTTP requests +// will be sent exactly to the IP address got from the bootstrap resolver. Note, +// that this function will first attempt to establish a QUIC connection (if +// HTTP3 is enabled in the upstream options). If this attempt is successful, +// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. +func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) { + tlsConfig := tlsC.GetGlobalFingerprintTLCConfig( + &tls.Config{ + InsecureSkipVerify: false, + MinVersion: tls.VersionTLS12, + SessionTicketsDisabled: false, + }) + var nextProtos []string + for _, v := range doh.httpVersions { + nextProtos = append(nextProtos, string(v)) + } + tlsConfig.NextProtos = nextProtos + dialContext := getDialHandler(doh.r, doh.proxyAdapter) + // First, we attempt to create an HTTP3 transport. If the probe QUIC + // connection is established successfully, we'll be using HTTP3 for this + // upstream. + transportH3, err := doh.createTransportH3(ctx, tlsConfig, dialContext) + if err == nil { + log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) + return transportH3, nil + } + + log.Debugln("[%s] using HTTP/2 for this upstream: %v", doh.url.String(), err) + + if !doh.supportsHTTP() { + return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream") + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + DisableCompression: true, + DialContext: dialContext, + IdleConnTimeout: transportDefaultIdleConnTimeout, + MaxConnsPerHost: dohMaxConnsPerHost, + MaxIdleConns: dohMaxIdleConns, + // Since we have a custom DialContext, we need to use this field to + // make golang http.Client attempt to use HTTP/2. Otherwise, it would + // only be used when negotiated on the TLS level. + ForceAttemptHTTP2: true, + } + + // Explicitly configure transport to use HTTP/2. + // + // See https://github.com/AdguardTeam/dnsproxy/issues/11. + var transportH2 *http2.Transport + transportH2, err = http2.ConfigureTransports(transport) if err != nil { - return req, err + return nil, err + } + + // Enable HTTP/2 pings on idle connections. + transportH2.ReadIdleTimeout = transportDefaultReadIdleTimeout + + return transport, nil +} + +// http3Transport is a wrapper over *http3.RoundTripper that tries to optimize +// its behavior. The main thing that it does is trying to force use a single +// connection to a host instead of creating a new one all the time. It also +// helps mitigate race issues with quic-go. +type http3Transport struct { + baseTransport *http3.RoundTripper + + closed bool + mu sync.RWMutex +} + +// type check +var _ http.RoundTripper = (*http3Transport)(nil) + +// RoundTrip implements the http.RoundTripper interface for *http3Transport. +func (h *http3Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + h.mu.RLock() + defer h.mu.RUnlock() + + if h.closed { + return nil, net.ErrClosed + } + + // Try to use cached connection to the target host if it's available. + resp, err = h.baseTransport.RoundTripOpt(req, http3.RoundTripOpt{OnlyCachedConn: true}) + + if errors.Is(err, http3.ErrNoCachedConn) { + // If there are no cached connection, trigger creating a new one. + resp, err = h.baseTransport.RoundTrip(req) } - req.Header.Set("content-type", dotMimeType) - req.Header.Set("accept", dotMimeType) - return req, nil + return resp, err } -func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) { - client := &http.Client{Transport: dc.transport} - resp, err := client.Do(req) +// type check +var _ io.Closer = (*http3Transport)(nil) + +// Close implements the io.Closer interface for *http3Transport. +func (h *http3Transport) Close() (err error) { + h.mu.Lock() + defer h.mu.Unlock() + + h.closed = true + + return h.baseTransport.Close() +} + +// createTransportH3 tries to create an HTTP/3 transport for this upstream. +// We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or +// if it is too slow. In order to do that, this method will run two probes +// in parallel (one for TLS, the other one for QUIC) and if QUIC is faster it +// will create the *http3.RoundTripper instance. +func (doh *dnsOverHTTPS) createTransportH3( + ctx context.Context, + tlsConfig *tls.Config, + dialContext dialHandler, +) (roundTripper http.RoundTripper, err error) { + if !doh.supportsH3() { + return nil, errors.New("HTTP3 support is not enabled") + } + + addr, err := doh.probeH3(ctx, tlsConfig, dialContext) if err != nil { return nil, err } - defer resp.Body.Close() + rt := &http3.RoundTripper{ + Dial: func( + ctx context.Context, + + // Ignore the address and always connect to the one that we got + // from the bootstrapper. + _ string, + tlsCfg *tls.Config, + cfg *quic.Config, + ) (c quic.EarlyConnection, err error) { + return doh.dialQuic(ctx, addr, tlsCfg, cfg) + }, + DisableCompression: true, + TLSClientConfig: tlsConfig, + QuicConfig: doh.getQUICConfig(), + } + + return &http3Transport{baseTransport: rt}, nil +} - buf, err := io.ReadAll(resp.Body) +func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + ip, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } - msg = &D.Msg{} - err = msg.Unpack(buf) - return msg, err + portInt, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + udpAddr := net.UDPAddr{ + IP: net.ParseIP(ip), + Port: portInt, + } + conn, err := listenPacket(ctx, doh.proxyAdapter, "udp", addr, doh.r) + if err != nil { + return nil, err + } + return quic.DialEarlyContext(ctx, conn, &udpAddr, doh.url.Host, tlsCfg, cfg) } -func newDoHClient(url string, r *Resolver, params map[string]string, proxyAdapter string) *dohClient { - useH3 := params["h3"] == "true" - TLCConfig := tlsC.GetDefaultTLSConfig() - var transport http.RoundTripper - if useH3 { - transport = &http3.RoundTripper{ - Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - - ip, err := resolver.ResolveIPWithResolver(host, r) - if err != nil { - return nil, err - } - - portInt, err := strconv.Atoi(port) - if err != nil { - return nil, err - } - - udpAddr := net.UDPAddr{ - IP: net.ParseIP(ip.String()), - Port: portInt, - } - - var conn net.PacketConn - if proxyAdapter == "" { - conn, err = dialer.ListenPacket(ctx, "udp", "") - if err != nil { - return nil, err - } - } else { - if wrapConn, err := dialContextExtra(ctx, proxyAdapter, "udp", ip, port); err == nil { - if pc, ok := wrapConn.(*wrapPacketConn); ok { - conn = pc - } else { - return nil, fmt.Errorf("conn isn't wrapPacketConn") - } - } else { - return nil, err - } - } - - return quic.DialEarlyContext(ctx, conn, &udpAddr, host, tlsCfg, cfg) - }, - TLSClientConfig: TLCConfig, +// probeH3 runs a test to check whether QUIC is faster than TLS for this +// upstream. If the test is successful it will return the address that we +// should use to establish the QUIC connections. +func (doh *dnsOverHTTPS) probeH3( + ctx context.Context, + tlsConfig *tls.Config, + dialContext dialHandler, +) (addr string, err error) { + // We're using bootstrapped address instead of what's passed to the function + // it does not create an actual connection, but it helps us determine + // what IP is actually reachable (when there are v4/v6 addresses). + rawConn, err := dialContext(ctx, "udp", doh.url.Host) + if err != nil { + return "", fmt.Errorf("failed to dial: %w", err) + } + addr = rawConn.RemoteAddr().String() + // It's never actually used. + _ = rawConn.Close() + + // Avoid spending time on probing if this upstream only supports HTTP/3. + if doh.supportsH3() && !doh.supportsHTTP() { + return addr, nil + } + + // Use a new *tls.Config with empty session cache for probe connections. + // Surprisingly, this is really important since otherwise it invalidates + // the existing cache. + // TODO(ameshkov): figure out why the sessions cache invalidates here. + probeTLSCfg := tlsConfig.Clone() + probeTLSCfg.ClientSessionCache = nil + + // Do not expose probe connections to the callbacks that are passed to + // the bootstrap options to avoid side-effects. + // TODO(ameshkov): consider exposing, somehow mark that this is a probe. + probeTLSCfg.VerifyPeerCertificate = nil + probeTLSCfg.VerifyConnection = nil + + // Run probeQUIC and probeTLS in parallel and see which one is faster. + chQuic := make(chan error, 1) + chTLS := make(chan error, 1) + go doh.probeQUIC(ctx, addr, probeTLSCfg, chQuic) + go doh.probeTLS(ctx, dialContext, probeTLSCfg, chTLS) + + select { + case quicErr := <-chQuic: + if quicErr != nil { + // QUIC failed, return error since HTTP3 was not preferred. + return "", quicErr } - } else { - transport = &http.Transport{ - ForceAttemptHTTP2: true, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - - ip, err := resolver.ResolveIPWithResolver(host, r) - if err != nil { - return nil, err - } - - if proxyAdapter == "" { - return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port)) - } else { - return dialContextExtra(ctx, proxyAdapter, "tcp", ip, port) - } - }, - TLSClientConfig: TLCConfig, + + // Return immediately, QUIC was faster. + return addr, quicErr + case tlsErr := <-chTLS: + if tlsErr != nil { + // Return immediately, TLS failed. + log.Debugln("probing TLS: %v", tlsErr) + return addr, nil } + + return "", errors.New("TLS was faster than QUIC, prefer it") } +} - return &dohClient{ - url: url, - transport: transport, +// probeQUIC attempts to establish a QUIC connection to the specified address. +// We run probeQUIC and probeTLS in parallel and see which one is faster. +func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig *tls.Config, ch chan error) { + startTime := time.Now() + conn, err := doh.dialQuic(ctx, addr, tlsConfig, doh.getQUICConfig()) + if err != nil { + ch <- fmt.Errorf("opening QUIC connection to %s: %w", doh.Address(), err) + return } + + // Ignore the error since there's no way we can use it for anything useful. + _ = conn.CloseWithError(QUICCodeNoError, "") + + ch <- nil + + elapsed := time.Now().Sub(startTime) + log.Debugln("elapsed on establishing a QUIC connection: %s", elapsed) +} + +// probeTLS attempts to establish a TLS connection to the specified address. We +// run probeQUIC and probeTLS in parallel and see which one is faster. +func (doh *dnsOverHTTPS) probeTLS(ctx context.Context, dialContext dialHandler, tlsConfig *tls.Config, ch chan error) { + startTime := time.Now() + + conn, err := doh.tlsDial(ctx, dialContext, "tcp", tlsConfig) + if err != nil { + ch <- fmt.Errorf("opening TLS connection: %w", err) + return + } + + // Ignore the error since there's no way we can use it for anything useful. + _ = conn.Close() + + ch <- nil + + elapsed := time.Now().Sub(startTime) + log.Debugln("elapsed on establishing a TLS connection: %s", elapsed) +} + +// supportsH3 returns true if HTTP/3 is supported by this upstream. +func (doh *dnsOverHTTPS) supportsH3() (ok bool) { + for _, v := range doh.supportedHTTPVersions() { + if v == C.HTTPVersion3 { + return true + } + } + + return false +} + +// supportsHTTP returns true if HTTP/1.1 or HTTP2 is supported by this upstream. +func (doh *dnsOverHTTPS) supportsHTTP() (ok bool) { + for _, v := range doh.supportedHTTPVersions() { + if v == C.HTTPVersion11 || v == C.HTTPVersion2 { + return true + } + } + + return false +} + +// supportedHTTPVersions returns the list of supported HTTP versions. +func (doh *dnsOverHTTPS) supportedHTTPVersions() (v []C.HTTPVersion) { + v = doh.httpVersions + if v == nil { + v = DefaultHTTPVersions + } + + return v +} + +// isHTTP3 checks if the *http.Client is an HTTP/3 client. +func isHTTP3(client *http.Client) (ok bool) { + _, ok = client.Transport.(*http3Transport) + + return ok +} + +// tlsDial is basically the same as tls.DialWithDialer, but we will call our own +// dialContext function to get connection. +func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, dialContext dialHandler, network string, config *tls.Config) (*tls.Conn, error) { + // We're using bootstrapped address instead of what's passed + // to the function. + rawConn, err := dialContext(ctx, network, doh.url.Host) + if err != nil { + return nil, err + } + + // We want the timeout to cover the whole process: TCP connection and + // TLS handshake dialTimeout will be used as connection deadLine. + conn := tls.Client(rawConn, config) + + err = conn.SetDeadline(time.Now().Add(dialTimeout)) + if err != nil { + // Must not happen in normal circumstances. + panic(fmt.Errorf("cannot set deadline: %w", err)) + } + + err = conn.Handshake() + if err != nil { + defer conn.Close() + return nil, err + } + + return conn, nil } diff --git a/dns/doq.go b/dns/doq.go index 7807de1cc1..1c5956af54 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -1,134 +1,303 @@ package dns import ( - "bytes" "context" "crypto/tls" + "encoding/binary" + "errors" "fmt" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - tlsC "github.com/Dreamacro/clash/component/tls" - "github.com/lucas-clemente/quic-go" "net" + "runtime" "strconv" "sync" "time" + tlsC "github.com/Dreamacro/clash/component/tls" + "github.com/metacubex/quic-go" + "github.com/Dreamacro/clash/log" D "github.com/miekg/dns" ) const NextProtoDQ = "doq" +const ( + // QUICCodeNoError is used when the connection or stream needs to be closed, + // but there is no error to signal. + QUICCodeNoError = quic.ApplicationErrorCode(0) + // QUICCodeInternalError signals that the DoQ implementation encountered + // an internal error and is incapable of pursuing the transaction or the + // connection. + QUICCodeInternalError = quic.ApplicationErrorCode(1) + // QUICKeepAlivePeriod is the value that we pass to *quic.Config and that + // controls the period with with keep-alive frames are being sent to the + // connection. We set it to 20s as it would be in the quic-go@v0.27.1 with + // KeepAlive field set to true This value is specified in + // https://pkg.go.dev/github.com/metacubex/quic-go/internal/protocol#MaxKeepAliveInterval. + // + // TODO(ameshkov): Consider making it configurable. + QUICKeepAlivePeriod = time.Second * 20 + DefaultTimeout = time.Second * 5 +) -var bytesPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} +// dnsOverQUIC is a struct that implements the Upstream interface for the +// DNS-over-QUIC protocol (spec: https://www.rfc-editor.org/rfc/rfc9250.html). +type dnsOverQUIC struct { + // quicConfig is the QUIC configuration that is used for establishing + // connections to the upstream. This configuration includes the TokenStore + // that needs to be stored for the lifetime of dnsOverQUIC since we can + // re-create the connection. + quicConfig *quic.Config + quicConfigGuard sync.Mutex + + // conn is the current active QUIC connection. It can be closed and + // re-opened when needed. + conn quic.Connection + connMu sync.RWMutex + + // bytesPool is a *sync.Pool we use to store byte buffers in. These byte + // buffers are used to read responses from the upstream. + bytesPool *sync.Pool + bytesPoolGuard sync.Mutex -type quicClient struct { addr string - r *Resolver - connection quic.Connection proxyAdapter string - udp net.PacketConn - sync.RWMutex // protects connection and bytesPool + r *Resolver } -func newDOQ(r *Resolver, addr, proxyAdapter string) *quicClient { - return &quicClient{ +// type check +var _ dnsClient = (*dnsOverQUIC)(nil) + +// newDoQ returns the DNS-over-QUIC Upstream. +func newDoQ(resolver *Resolver, addr string, adapter string) (dnsClient, error) { + doq := &dnsOverQUIC{ addr: addr, - r: r, - proxyAdapter: proxyAdapter, + proxyAdapter: adapter, + r: resolver, + quicConfig: &quic.Config{ + KeepAlivePeriod: QUICKeepAlivePeriod, + TokenStore: newQUICTokenStore(), + }, } -} -func (dc *quicClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { - return dc.ExchangeContext(context.Background(), m) + runtime.SetFinalizer(doq, (*dnsOverQUIC).Close) + return doq, nil } -func (dc *quicClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - stream, err := dc.openStream(ctx) +// Address implements the Upstream interface for *dnsOverQUIC. +func (doq *dnsOverQUIC) Address() string { return doq.addr } + +func (doq *dnsOverQUIC) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // When sending queries over a QUIC connection, the DNS Message ID MUST be + // set to zero. + id := m.Id + m.Id = 0 + defer func() { + // Restore the original ID to not break compatibility with proxies. + m.Id = id + if msg != nil { + msg.Id = id + } + }() + + // Check if there was already an active conn before sending the request. + // We'll only attempt to re-connect if there was one. + hasConnection := doq.hasConnection() + + // Make the first attempt to send the DNS query. + msg, err = doq.exchangeQUIC(ctx, m) + + // Make up to 2 attempts to re-open the QUIC connection and send the request + // again. There are several cases where this workaround is necessary to + // make DoQ usable. We need to make 2 attempts in the case when the + // connection was closed (due to inactivity for example) AND the server + // refuses to open a 0-RTT connection. + for i := 0; hasConnection && doq.shouldRetry(err) && i < 2; i++ { + log.Debugln("re-creating the QUIC connection and retrying due to %v", err) + + // Close the active connection to make sure we'll try to re-connect. + doq.closeConnWithError(err) + + // Retry sending the request. + msg, err = doq.exchangeQUIC(ctx, m) + } + if err != nil { - return nil, fmt.Errorf("failed to open new stream to %s", dc.addr) + // If we're unable to exchange messages, make sure the connection is + // closed and signal about an internal error. + doq.closeConnWithError(err) + } + + return msg, err +} + +// Exchange implements the Upstream interface for *dnsOverQUIC. +func (doq *dnsOverQUIC) Exchange(m *D.Msg) (msg *D.Msg, err error) { + return doq.ExchangeContext(context.Background(), m) +} + +// Close implements the Upstream interface for *dnsOverQUIC. +func (doq *dnsOverQUIC) Close() (err error) { + doq.connMu.Lock() + defer doq.connMu.Unlock() + + runtime.SetFinalizer(doq, nil) + + if doq.conn != nil { + err = doq.conn.CloseWithError(QUICCodeNoError, "") } - buf, err := m.Pack() + return err +} + +// exchangeQUIC attempts to open a QUIC connection, send the DNS message +// through it and return the response it got from the server. +func (doq *dnsOverQUIC) exchangeQUIC(ctx context.Context, msg *D.Msg) (resp *D.Msg, err error) { + var conn quic.Connection + conn, err = doq.getConnection(ctx, true) if err != nil { return nil, err } - _, err = stream.Write(buf) + var buf []byte + buf, err = msg.Pack() + if err != nil { + return nil, fmt.Errorf("failed to pack DNS message for DoQ: %w", err) + } + + var stream quic.Stream + stream, err = doq.openStream(ctx, conn) if err != nil { return nil, err } + _, err = stream.Write(AddPrefix(buf)) + if err != nil { + return nil, fmt.Errorf("failed to write to a QUIC stream: %w", err) + } + // The client MUST send the DNS query over the selected stream, and MUST // indicate through the STREAM FIN mechanism that no further data will - // be sent on that stream. - // stream.Close() -- closes the write-direction of the stream. + // be sent on that stream. Note, that stream.Close() closes the + // write-direction of the stream, but does not prevent reading from it. _ = stream.Close() - respBuf := bytesPool.Get().(*bytes.Buffer) - defer bytesPool.Put(respBuf) - defer respBuf.Reset() + return doq.readMsg(stream) +} - n, err := respBuf.ReadFrom(stream) - if err != nil && n == 0 { - return nil, err +// AddPrefix adds a 2-byte prefix with the DNS message length. +func AddPrefix(b []byte) (m []byte) { + m = make([]byte, 2+len(b)) + binary.BigEndian.PutUint16(m, uint16(len(b))) + copy(m[2:], b) + + return m +} + +// shouldRetry checks what error we received and decides whether it is required +// to re-open the connection and retry sending the request. +func (doq *dnsOverQUIC) shouldRetry(err error) (ok bool) { + return isQUICRetryError(err) +} + +// getBytesPool returns (creates if needed) a pool we store byte buffers in. +func (doq *dnsOverQUIC) getBytesPool() (pool *sync.Pool) { + doq.bytesPoolGuard.Lock() + defer doq.bytesPoolGuard.Unlock() + + if doq.bytesPool == nil { + doq.bytesPool = &sync.Pool{ + New: func() interface{} { + b := make([]byte, MaxMsgSize) + + return &b + }, + } + } + + return doq.bytesPool +} + +// getConnection opens or returns an existing quic.Connection. useCached +// argument controls whether we should try to use the existing cached +// connection. If it is false, we will forcibly create a new connection and +// close the existing one if needed. +func (doq *dnsOverQUIC) getConnection(ctx context.Context, useCached bool) (quic.Connection, error) { + var conn quic.Connection + doq.connMu.RLock() + conn = doq.conn + if conn != nil && useCached { + doq.connMu.RUnlock() + + return conn, nil + } + if conn != nil { + // we're recreating the connection, let's create a new one. + _ = conn.CloseWithError(QUICCodeNoError, "") } + doq.connMu.RUnlock() - reply := new(D.Msg) - err = reply.Unpack(respBuf.Bytes()) + doq.connMu.Lock() + defer doq.connMu.Unlock() + + var err error + conn, err = doq.openConnection(ctx) if err != nil { return nil, err } + doq.conn = conn - return reply, nil + return conn, nil } -func isActive(s quic.Connection) bool { - select { - case <-s.Context().Done(): - return false - default: - return true - } +// hasConnection returns true if there's an active QUIC connection. +func (doq *dnsOverQUIC) hasConnection() (ok bool) { + doq.connMu.Lock() + defer doq.connMu.Unlock() + + return doq.conn != nil } -// getConnection - opens or returns an existing quic.Connection -// useCached - if true and cached connection exists, return it right away -// otherwise - forcibly creates a new connection -func (dc *quicClient) getConnection(ctx context.Context) (quic.Connection, error) { - var connection quic.Connection - dc.RLock() - connection = dc.connection +// getQUICConfig returns the QUIC config in a thread-safe manner. Note, that +// this method returns a pointer, it is forbidden to change its properties. +func (doq *dnsOverQUIC) getQUICConfig() (c *quic.Config) { + doq.quicConfigGuard.Lock() + defer doq.quicConfigGuard.Unlock() - if connection != nil && isActive(connection) { - dc.RUnlock() - return connection, nil - } - - dc.RUnlock() + return doq.quicConfig +} - dc.Lock() - defer dc.Unlock() - connection = dc.connection - if connection != nil { - if isActive(connection) { - return connection, nil - } else { - _ = connection.CloseWithError(quic.ApplicationErrorCode(0), "") - } - } +// resetQUICConfig re-creates the tokens store as we may need to use a new one +// if we failed to connect. +func (doq *dnsOverQUIC) resetQUICConfig() { + doq.quicConfigGuard.Lock() + defer doq.quicConfigGuard.Unlock() - var err error - connection, err = dc.openConnection(ctx) - dc.connection = connection - return connection, err + doq.quicConfig = doq.quicConfig.Clone() + doq.quicConfig.TokenStore = newQUICTokenStore() } -func (dc *quicClient) openConnection(ctx context.Context) (quic.Connection, error) { - if dc.udp != nil { - _ = dc.udp.Close() +// openStream opens a new QUIC stream for the specified connection. +func (doq *dnsOverQUIC) openStream(ctx context.Context, conn quic.Connection) (quic.Stream, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + stream, err := conn.OpenStreamSync(ctx) + if err == nil { + return stream, nil + } + + // We can get here if the old QUIC connection is not valid anymore. We + // should try to re-create the connection again in this case. + newConn, err := doq.getConnection(ctx, false) + if err != nil { + return nil, err } + // Open a new stream. + return newConn.OpenStreamSync(ctx) +} +// openConnection opens a new QUIC connection. +func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connection, err error) { tlsConfig := tlsC.GetGlobalFingerprintTLCConfig( &tls.Config{ InsecureSkipVerify: false, @@ -137,69 +306,161 @@ func (dc *quicClient) openConnection(ctx context.Context) (quic.Connection, erro }, SessionTicketsDisabled: false, }) - - quicConfig := &quic.Config{ - ConnectionIDLength: 12, - HandshakeIdleTimeout: time.Second * 8, - MaxIncomingStreams: 4, - KeepAlivePeriod: 10 * time.Second, - MaxIdleTimeout: time.Second * 120, + // we're using bootstrapped address instead of what's passed to the function + // it does not create an actual connection, but it helps us determine + // what IP is actually reachable (when there're v4/v6 addresses). + rawConn, err := getDialHandler(doq.r, doq.proxyAdapter)(ctx, "udp", doq.addr) + if err != nil { + return nil, fmt.Errorf("failed to open a QUIC connection: %w", err) } + addr := rawConn.RemoteAddr().String() + // It's never actually used + _ = rawConn.Close() - log.Debugln("opening new connection to %s", dc.addr) - var ( - udp net.PacketConn - err error - ) - - host, port, err := net.SplitHostPort(dc.addr) + ip, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + p, err := strconv.Atoi(port) + udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p} + udp, err := listenPacket(ctx, doq.proxyAdapter, "udp", addr, doq.r) if err != nil { return nil, err } - ip, err := resolver.ResolveIPv4WithResolver(host, dc.r) + host, _, err := net.SplitHostPort(doq.addr) if err != nil { return nil, err } - p, err := strconv.Atoi(port) - udpAddr := net.UDPAddr{IP: ip.AsSlice(), Port: p} + conn, err = quic.DialContext(ctx, udp, &udpAddr, host, tlsConfig, doq.getQUICConfig()) + if err != nil { + return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err) + } - if dc.proxyAdapter == "" { - udp, err = dialer.ListenPacket(ctx, "udp", "") - if err != nil { - return nil, err - } - } else { - conn, err := dialContextExtra(ctx, dc.proxyAdapter, "udp", ip, port) - if err != nil { - return nil, err - } + return conn, nil +} - wrapConn, ok := conn.(*wrapPacketConn) - if !ok { - return nil, fmt.Errorf("quic create packet failed") - } +// closeConnWithError closes the active connection with error to make sure that +// new queries were processed in another connection. We can do that in the case +// of a fatal error. +func (doq *dnsOverQUIC) closeConnWithError(err error) { + doq.connMu.Lock() + defer doq.connMu.Unlock() - udp = wrapConn + if doq.conn == nil { + // Do nothing, there's no active conn anyways. + return } - session, err := quic.DialContext(ctx, udp, &udpAddr, host, tlsConfig, quicConfig) + code := QUICCodeNoError if err != nil { - return nil, fmt.Errorf("failed to open QUIC connection: %w", err) + code = QUICCodeInternalError + } + + if errors.Is(err, quic.Err0RTTRejected) { + // Reset the TokenStore only if 0-RTT was rejected. + doq.resetQUICConfig() } - dc.udp = udp - return session, nil + err = doq.conn.CloseWithError(code, "") + if err != nil { + log.Errorln("failed to close the conn: %v", err) + } + doq.conn = nil } -func (dc *quicClient) openStream(ctx context.Context) (quic.Stream, error) { - session, err := dc.getConnection(ctx) +// readMsg reads the incoming DNS message from the QUIC stream. +func (doq *dnsOverQUIC) readMsg(stream quic.Stream) (m *D.Msg, err error) { + pool := doq.getBytesPool() + bufPtr := pool.Get().(*[]byte) + + defer pool.Put(bufPtr) + + respBuf := *bufPtr + n, err := stream.Read(respBuf) + if err != nil && n == 0 { + return nil, fmt.Errorf("reading response from %s: %w", doq.Address(), err) + } + + // All DNS messages (queries and responses) sent over DoQ connections MUST + // be encoded as a 2-octet length field followed by the message content as + // specified in [RFC1035]. + // IMPORTANT: Note, that we ignore this prefix here as this implementation + // does not support receiving multiple messages over a single connection. + m = new(D.Msg) + err = m.Unpack(respBuf[2:]) if err != nil { - return nil, err + return nil, fmt.Errorf("unpacking response from %s: %w", doq.Address(), err) + } + + return m, nil +} + +// newQUICTokenStore creates a new quic.TokenStore that is necessary to have +// in order to benefit from 0-RTT. +func newQUICTokenStore() (s quic.TokenStore) { + // You can read more on address validation here: + // https://datatracker.ietf.org/doc/html/rfc9000#section-8.1 + // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that this is + // more than enough for the way we use it (one connection per upstream). + return quic.NewLRUTokenStore(1, 10) +} + +// isQUICRetryError checks the error and determines whether it may signal that +// we should re-create the QUIC connection. This requirement is caused by +// quic-go issues, see the comments inside this function. +// TODO(ameshkov): re-test when updating quic-go. +func isQUICRetryError(err error) (ok bool) { + var qAppErr *quic.ApplicationError + if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { + // This error is often returned when the server has been restarted, + // and we try to use the same connection on the client-side. It seems, + // that the old connections aren't closed immediately on the server-side + // and that's why one can run into this. + // In addition to that, quic-go HTTP3 client implementation does not + // clean up dead connections (this one is specific to DoH3 upstream): + // https://github.com/metacubex/quic-go/issues/765 + return true + } + + var qIdleErr *quic.IdleTimeoutError + if errors.As(err, &qIdleErr) { + // This error means that the connection was closed due to being idle. + // In this case we should forcibly re-create the QUIC connection. + // Reproducing is rather simple, stop the server and wait for 30 seconds + // then try to send another request via the same upstream. + return true + } + + var resetErr *quic.StatelessResetError + if errors.As(err, &resetErr) { + // A stateless reset is sent when a server receives a QUIC packet that + // it doesn't know how to decrypt. For instance, it may happen when + // the server was recently rebooted. We should reconnect and try again + // in this case. + return true + } + + var qTransportError *quic.TransportError + if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError { + // A transport error with the NO_ERROR error code could be sent by the + // server when it considers that it's time to close the connection. + // For example, Google DNS eventually closes an active connection with + // the NO_ERROR code and "Connection max age expired" message: + // https://github.com/AdguardTeam/dnsproxy/issues/283 + return true + } + + if errors.Is(err, quic.Err0RTTRejected) { + // This error happens when we try to establish a 0-RTT connection with + // a token the server is no more aware of. This can be reproduced by + // restarting the QUIC server (it will clear its tokens cache). The + // next connection attempt will return this error until the client's + // tokens cache is purged. + return true } - // open a new stream - return session.OpenStreamSync(ctx) + return false } diff --git a/dns/enhancer.go b/dns/enhancer.go index 6e1d03acb3..8b3ce282ab 100644 --- a/dns/enhancer.go +++ b/dns/enhancer.go @@ -109,7 +109,7 @@ func NewEnhancer(cfg Config) *ResolverEnhancer { if cfg.EnhancedMode != C.DNSNormal { fakePool = cfg.Pool - mapping = cache.NewLRUCache[netip.Addr, string](cache.WithSize[netip.Addr, string](4096), cache.WithStale[netip.Addr, string](true)) + mapping = cache.New[netip.Addr, string](cache.WithSize[netip.Addr, string](4096), cache.WithStale[netip.Addr, string](true)) } return &ResolverEnhancer{ diff --git a/dns/filters.go b/dns/filters.go index 80b656c948..0dbfa31764 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -71,14 +71,15 @@ type fallbackDomainFilter interface { } type domainFilter struct { - tree *trie.DomainTrie[bool] + tree *trie.DomainTrie[struct{}] } func NewDomainFilter(domains []string) *domainFilter { - df := domainFilter{tree: trie.New[bool]()} + df := domainFilter{tree: trie.New[struct{}]()} for _, domain := range domains { - _ = df.tree.Insert(domain, true) + _ = df.tree.Insert(domain, struct{}{}) } + df.tree.Optimize() return &df } diff --git a/dns/middleware.go b/dns/middleware.go index 0bfc4977b8..28ced849db 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -37,7 +37,7 @@ func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[netip return next(ctx, r) } - ip := record.Data + ip := record.Data() msg := r.Copy() if ip.Is4() && q.Qtype == D.TypeA { @@ -156,7 +156,7 @@ func withResolver(resolver *Resolver) handler { return handleMsgWithEmptyAnswer(r), nil } - msg, err := resolver.Exchange(r) + msg, err := resolver.ExchangeContext(ctx, r) if err != nil { log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err) return msg, err diff --git a/dns/patch.go b/dns/patch.go index 769742430b..37b5d41b04 100644 --- a/dns/patch.go +++ b/dns/patch.go @@ -1,14 +1,18 @@ package dns -import D "github.com/miekg/dns" +import ( + "context" + + D "github.com/miekg/dns" +) type LocalServer struct { handler handler } // ServeMsg implement resolver.LocalServer ResolveMsg -func (s *LocalServer) ServeMsg(msg *D.Msg) (*D.Msg, error) { - return handlerWithContext(s.handler, msg) +func (s *LocalServer) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { + return handlerWithContext(ctx, s.handler, msg) } func NewLocalServer(resolver *Resolver, mapper *ResolverEnhancer) *LocalServer { diff --git a/dns/resolver.go b/dns/resolver.go index aac22cc86e..d99a465d0f 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -10,7 +10,6 @@ import ( "time" "github.com/Dreamacro/clash/common/cache" - "github.com/Dreamacro/clash/common/picker" "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/resolver" @@ -44,18 +43,18 @@ type Resolver struct { proxyServer []dnsClient } -func (r *Resolver) ResolveAllIPPrimaryIPv4(host string) (ips []netip.Addr, err error) { +func (r *Resolver) LookupIPPrimaryIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) { ch := make(chan []netip.Addr, 1) go func() { defer close(ch) - ip, err := r.resolveIP(host, D.TypeAAAA) + ip, err := r.lookupIP(ctx, host, D.TypeAAAA) if err != nil { return } ch <- ip }() - ips, err = r.resolveIP(host, D.TypeA) + ips, err = r.lookupIP(ctx, host, D.TypeA) if err == nil { return } @@ -68,11 +67,11 @@ func (r *Resolver) ResolveAllIPPrimaryIPv4(host string) (ips []netip.Addr, err e return ip, nil } -func (r *Resolver) ResolveAllIP(host string) (ips []netip.Addr, err error) { +func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) { ch := make(chan []netip.Addr, 1) go func() { defer close(ch) - ip, err := r.resolveIP(host, D.TypeAAAA) + ip, err := r.lookupIP(ctx, host, D.TypeAAAA) if err != nil { return } @@ -80,7 +79,7 @@ func (r *Resolver) ResolveAllIP(host string) (ips []netip.Addr, err error) { ch <- ip }() - ips, err = r.resolveIP(host, D.TypeA) + ips, err = r.lookupIP(ctx, host, D.TypeA) select { case ipv6s, open := <-ch: @@ -95,39 +94,47 @@ func (r *Resolver) ResolveAllIP(host string) (ips []netip.Addr, err error) { return ips, nil } -func (r *Resolver) ResolveAllIPv4(host string) (ips []netip.Addr, err error) { - return r.resolveIP(host, D.TypeA) -} - -func (r *Resolver) ResolveAllIPv6(host string) (ips []netip.Addr, err error) { - return r.resolveIP(host, D.TypeAAAA) -} - // ResolveIP request with TypeA and TypeAAAA, priority return TypeA -func (r *Resolver) ResolveIP(host string) (ip netip.Addr, err error) { - if ips, err := r.ResolveAllIPPrimaryIPv4(host); err == nil { - return ips[rand.Intn(len(ips))], nil - } else { +func (r *Resolver) ResolveIP(ctx context.Context, host string) (ip netip.Addr, err error) { + ips, err := r.LookupIPPrimaryIPv4(ctx, host) + if err != nil { return netip.Addr{}, err + } else if len(ips) == 0 { + return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) } + return ips[rand.Intn(len(ips))], nil +} + +// LookupIPv4 request with TypeA +func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { + return r.lookupIP(ctx, host, D.TypeA) } // ResolveIPv4 request with TypeA -func (r *Resolver) ResolveIPv4(host string) (ip netip.Addr, err error) { - if ips, err := r.ResolveAllIPv4(host); err == nil { - return ips[rand.Intn(len(ips))], nil - } else { +func (r *Resolver) ResolveIPv4(ctx context.Context, host string) (ip netip.Addr, err error) { + ips, err := r.lookupIP(ctx, host, D.TypeA) + if err != nil { return netip.Addr{}, err + } else if len(ips) == 0 { + return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) } + return ips[rand.Intn(len(ips))], nil +} + +// LookupIPv6 request with TypeAAAA +func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { + return r.lookupIP(ctx, host, D.TypeAAAA) } // ResolveIPv6 request with TypeAAAA -func (r *Resolver) ResolveIPv6(host string) (ip netip.Addr, err error) { - if ips, err := r.ResolveAllIPv6(host); err == nil { - return ips[rand.Intn(len(ips))], nil - } else { +func (r *Resolver) ResolveIPv6(ctx context.Context, host string) (ip netip.Addr, err error) { + ips, err := r.lookupIP(ctx, host, D.TypeAAAA) + if err != nil { return netip.Addr{}, err + } else if len(ips) == 0 { + return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) } + return ips[rand.Intn(len(ips))], nil } func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { @@ -149,6 +156,16 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e if len(m.Question) == 0 { return nil, errors.New("should have one question at least") } + continueFetch := false + defer func() { + if continueFetch || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + go func() { + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + defer cancel() + _, _ = r.exchangeWithoutCache(ctx, m) // ignore result, just for putMsgToCache + }() + } + }() q := m.Question[0] cacheM, expireTime, hit := r.lruCache.GetWithExpire(q.String()) @@ -157,7 +174,7 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e msg = cacheM.Copy() if expireTime.Before(now) { setMsgTTL(msg, uint32(1)) // Continue fetch - go r.exchangeWithoutCache(ctx, m) + continueFetch = true } else { setMsgTTL(msg, uint32(time.Until(expireTime).Seconds())) } @@ -170,9 +187,16 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { q := m.Question[0] - ret, err, shared := r.group.Do(q.String(), func() (result any, err error) { + retryNum := 0 + retryMax := 3 + fn := func() (result any, err error) { + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) // reset timeout in singleflight + defer cancel() + defer func() { if err != nil { + result = retryNum + retryNum++ return } @@ -190,7 +214,35 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M return r.batchExchange(ctx, matched, m) } return r.batchExchange(ctx, r.main, m) - }) + } + + ch := r.group.DoChan(q.String(), fn) + + var result singleflight.Result + + select { + case result = <-ch: + break + case <-ctx.Done(): + select { + case result = <-ch: // maybe ctxDone and chFinish in same time, get DoChan's result as much as possible + break + default: + go func() { // start a retrying monitor in background + result := <-ch + ret, err, shared := result.Val, result.Err, result.Shared + if err != nil && !shared && ret.(int) < retryMax { // retry + r.group.DoChan(q.String(), fn) + } + }() + return nil, ctx.Err() + } + } + + ret, err, shared := result.Val, result.Err, result.Shared + if err != nil && !shared && ret.(int) < retryMax { // retry + r.group.DoChan(q.String(), fn) + } if err == nil { msg = ret.(*D.Msg) @@ -203,31 +255,10 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M } func (r *Resolver) batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { - fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) - for _, client := range clients { - r := client - fast.Go(func() (*D.Msg, error) { - m, err := r.ExchangeContext(ctx, m) - if err != nil { - return nil, err - } else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused { - return nil, errors.New("server failure") - } - return m, nil - }) - } + ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDNSTimeout) + defer cancel() - elm := fast.Wait() - if elm == nil { - err := errors.New("all DNS requests failed") - if fErr := fast.Error(); fErr != nil { - err = fmt.Errorf("%w, first error: %s", err, fErr.Error()) - } - return nil, err - } - - msg = elm - return + return batchExchange(ctx, clients, m) } func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { @@ -245,7 +276,7 @@ func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { return nil } - p := record.Data + p := record.Data() return p.GetData() } @@ -305,7 +336,7 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er return } -func (r *Resolver) resolveIP(host string, dnsType uint16) (ips []netip.Addr, err error) { +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() @@ -321,7 +352,7 @@ func (r *Resolver) resolveIP(host string, dnsType uint16) (ips []netip.Addr, err query := &D.Msg{} query.SetQuestion(D.Fqdn(host), dnsType) - msg, err := r.Exchange(query) + msg, err := r.ExchangeContext(ctx, query) if err != nil { return []netip.Addr{}, err } @@ -355,6 +386,7 @@ type NameServer struct { Interface *atomic.String ProxyAdapter string Params map[string]string + PreferH3 bool } type FallbackFilter struct { @@ -380,13 +412,13 @@ type Config struct { func NewResolver(config Config) *Resolver { defaultResolver := &Resolver{ main: transform(config.Default, nil), - lruCache: cache.NewLRUCache[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + lruCache: cache.New[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), } r := &Resolver{ ipv6: config.IPv6, main: transform(config.Main, defaultResolver), - lruCache: cache.NewLRUCache[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + lruCache: cache.New[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), hosts: config.Hosts, } @@ -403,6 +435,7 @@ func NewResolver(config Config) *Resolver { for domain, nameserver := range config.Policy { _ = r.policy.Insert(domain, NewPolicy(transform([]NameServer{nameserver}, defaultResolver))) } + r.policy.Optimize() } fallbackIPFilters := []fallbackIPFilter{} diff --git a/dns/server.go b/dns/server.go index 1fbde824e7..5c5970db74 100644 --- a/dns/server.go +++ b/dns/server.go @@ -1,6 +1,7 @@ package dns import ( + stdContext "context" "errors" "net" @@ -25,7 +26,7 @@ type Server struct { // ServeDNS implement D.Handler ServeDNS func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { - msg, err := handlerWithContext(s.handler, r) + msg, err := handlerWithContext(stdContext.Background(), s.handler, r) if err != nil { D.HandleFailed(w, r) return @@ -34,12 +35,12 @@ func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { w.WriteMsg(msg) } -func handlerWithContext(handler handler, msg *D.Msg) (*D.Msg, error) { +func handlerWithContext(stdCtx stdContext.Context, handler handler, msg *D.Msg) (*D.Msg, error) { if len(msg.Question) == 0 { return nil, errors.New("at least one question is required") } - ctx := context.NewDNSContext(msg) + ctx := context.NewDNSContext(stdCtx, msg) return handler(ctx, msg) } diff --git a/dns/util.go b/dns/util.go index 50d9decd18..8259f22e86 100644 --- a/dns/util.go +++ b/dns/util.go @@ -3,6 +3,7 @@ package dns import ( "context" "crypto/tls" + "errors" "fmt" "net" "net/netip" @@ -10,8 +11,11 @@ import ( "time" "github.com/Dreamacro/clash/common/cache" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/common/nnip" + "github.com/Dreamacro/clash/common/picker" "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" @@ -19,7 +23,16 @@ import ( D "github.com/miekg/dns" ) +const ( + MaxMsgSize = 65535 +) + func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) { + // skip dns cache for acme challenge + if q := msg.Question[0]; q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge") { + log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name) + return + } var ttl uint32 switch { case len(msg.Answer) != 0: @@ -59,13 +72,17 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { for _, s := range servers { switch s.Net { case "https": - ret = append(ret, newDoHClient(s.Addr, resolver, s.Params, s.ProxyAdapter)) + ret = append(ret, newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter)) continue case "dhcp": ret = append(ret, newDHCPClient(s.Addr)) continue case "quic": - ret = append(ret, newDOQ(resolver, s.Addr, s.ProxyAdapter)) + if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter); err == nil { + ret = append(ret, doq) + } else { + log.Fatalln("DoQ format error: %v", err) + } continue } @@ -123,86 +140,121 @@ func msgToDomain(msg *D.Msg) string { return "" } -type wrapPacketConn struct { - net.PacketConn - rAddr net.Addr -} - -func (wpc *wrapPacketConn) Read(b []byte) (n int, err error) { - n, _, err = wpc.PacketConn.ReadFrom(b) - return n, err -} - -func (wpc *wrapPacketConn) Write(b []byte) (n int, err error) { - return wpc.PacketConn.WriteTo(b, wpc.rAddr) -} +type dialHandler func(ctx context.Context, network, addr string) (net.Conn, error) -func (wpc *wrapPacketConn) RemoteAddr() net.Addr { - return wpc.rAddr -} - -func (wpc *wrapPacketConn) LocalAddr() net.Addr { - if wpc.PacketConn.LocalAddr() == nil { - return &net.UDPAddr{IP: net.IPv4zero, Port: 0} - } else { - return wpc.PacketConn.LocalAddr() +func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dialHandler { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + if len(proxyAdapter) == 0 { + opts = append(opts, dialer.WithResolver(r)) + return dialer.DialContext(ctx, network, addr, opts...) + } else { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + adapter, ok := tunnel.Proxies()[proxyAdapter] + if !ok { + opts = append(opts, dialer.WithInterface(proxyAdapter)) + } + if strings.Contains(network, "tcp") { + // tcp can resolve host by remote + metadata := &C.Metadata{ + NetWork: C.TCP, + Host: host, + DstPort: port, + } + if ok { + return adapter.DialContext(ctx, metadata, opts...) + } + opts = append(opts, dialer.WithResolver(r)) + return dialer.DialContext(ctx, network, addr, opts...) + } else { + // udp must resolve host first + dstIP, err := resolver.ResolveIPWithResolver(ctx, host, r) + if err != nil { + return nil, err + } + metadata := &C.Metadata{ + NetWork: C.UDP, + Host: "", + DstIP: dstIP, + DstPort: port, + } + if !ok { + return dialer.DialContext(ctx, network, addr, opts...) + } + + if !adapter.SupportUDP() { + return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) + } + + packetConn, err := adapter.ListenPacketContext(ctx, metadata, opts...) + if err != nil { + return nil, err + } + + return N.NewBindPacketConn(packetConn, metadata.UDPAddr()), nil + } + } } } -func dialContextExtra(ctx context.Context, adapterName string, network string, dstIP netip.Addr, port string, opts ...dialer.Option) (net.Conn, error) { - networkType := C.TCP - if network == "udp" { - - networkType = C.UDP +func listenPacket(ctx context.Context, proxyAdapter string, network string, addr string, r *Resolver, opts ...dialer.Option) (net.PacketConn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err } - - addrType := C.AtypIPv4 - if dstIP.Is6() { - addrType = C.AtypIPv6 + adapter, ok := tunnel.Proxies()[proxyAdapter] + if !ok && len(proxyAdapter) != 0 { + opts = append(opts, dialer.WithInterface(proxyAdapter)) } + // udp must resolve host first + dstIP, err := resolver.ResolveIPWithResolver(ctx, host, r) + if err != nil { + return nil, err + } metadata := &C.Metadata{ - NetWork: networkType, - AddrType: addrType, - Host: "", - DstIP: dstIP, - DstPort: port, + NetWork: C.UDP, + Host: "", + DstIP: dstIP, + DstPort: port, } - - adapter, ok := tunnel.Proxies()[adapterName] if !ok { - opts = append(opts, dialer.WithInterface(adapterName)) - if C.TCP == networkType { - return dialer.DialContext(ctx, network, dstIP.String()+":"+port, opts...) - } else { - packetConn, err := dialer.ListenPacket(ctx, network, dstIP.String()+":"+port, opts...) - if err != nil { - return nil, err - } - - return &wrapPacketConn{ - PacketConn: packetConn, - rAddr: metadata.UDPAddr(), - }, nil + return dialer.ListenPacket(ctx, dialer.ParseNetwork(network, dstIP), "", opts...) + } - } + if !adapter.SupportUDP() { + return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } - if networkType == C.UDP && !adapter.SupportUDP() { - return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", adapterName) + return adapter.ListenPacketContext(ctx, metadata, opts...) +} + +func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { + fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) + for _, client := range clients { + r := client + fast.Go(func() (*D.Msg, error) { + m, err := r.ExchangeContext(ctx, m) + if err != nil { + return nil, err + } else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused { + return nil, errors.New("server failure") + } + return m, nil + }) } - if networkType == C.UDP { - packetConn, err := adapter.ListenPacketContext(ctx, metadata, opts...) - if err != nil { - return nil, err + elm := fast.Wait() + if elm == nil { + err := errors.New("all DNS requests failed") + if fErr := fast.Error(); fErr != nil { + err = fmt.Errorf("%w, first error: %s", err, fErr.Error()) } - - return &wrapPacketConn{ - PacketConn: packetConn, - rAddr: metadata.UDPAddr(), - }, nil + return nil, err } - return adapter.DialContext(ctx, metadata, opts...) + msg = elm + return } diff --git a/docs/config.yaml b/docs/config.yaml index 61fb319a41..b6fc74f91c 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -16,7 +16,7 @@ log-level: debug # 日志等级 silent/error/warning/info/debug ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 external-controller: 0.0.0.0:9093 # RESTful API 监听地址 - +external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要配置 tls 部分配置文件 # secret: "123456" # `Authorization: Bearer ${secret}` # tcp-concurrent: true # TCP并发连接所有IP, 将使用最快握手的TCP @@ -40,11 +40,39 @@ hosts: # Tun 配置 tun: enable: false - stack: system # gvisor + stack: system # gvisor / lwip dns-hijack: - - 198.18.0.2:53 # 需要劫持的 DNS + - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: true # 自动识别出口网卡 # auto-route: true # 配置路由表 + # mtu: 9000 # 最大传输单元 + # strict_route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 + inet4_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + - 0.0.0.0/1 + - 128.0.0.0/1 + inet6_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + - "::/1" + - "8000::/1" + # endpoint_independent_nat: false # 启用独立于端点的 NAT + # include_uid: # UID 规则仅在 Linux 下被支持,并且需要 auto_route + # - 0 + # include_uid_range: # 限制被路由的的用户范围 + # - 1000-99999 + # exclude_uid: # 排除路由的的用户 + #- 1000 + # exclude_uid_range: # 排除路由的的用户范围 + # - 1000-99999 + + # Android 用户和应用规则仅在 Android 下被支持 + # 并且需要 auto_route + + # include_android_user: # 限制被路由的 Android 用户 + # - 0 + # - 10 + # include_package: # 限制被路由的 Android 应用包名 + # - com.android.chrome + # exclude_package: # 排除被路由的 Android 应用包名 + # - com.android.captiveportallogin #ebpf配置 ebpf: @@ -69,6 +97,35 @@ sniffer: - "443" # - 8000-9999 +# shadowsocks,vmess 入口配置(传入流量将和socks,mixed等入口一样按照mode所指定的方式进行匹配处理) +# ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 +# vmess-config: vmess://1:9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68@:12345 + +# tuic服务器入口(传入流量将和socks,mixed等入口一样按照mode所指定的方式进行匹配处理) +#tuic-server: +# enable: true +# listen: 127.0.0.1:10443 +# token: +# - TOKEN +# certificate: ./server.crt +# private-key: ./server.key +# congestion-controller: bbr +# max-idle-time: 15000 +# authentication-timeout: 1000 +# alpn: +# - h3 +# max-udp-relay-packet-size: 1500 + +tunnels: + # one line config + - tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy + - tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn + # full yaml config + - network: [ tcp, udp ] + address: 127.0.0.1:7777 + target: target.com + proxy: proxy + profile: # 存储select选择记录 store-selected: false @@ -79,6 +136,7 @@ profile: # DNS配置 dns: enable: false # 关闭将使用系统 DNS + prefer-h3: true # 开启 DoH 支持 HTTP/3,将并发尝试 listen: 0.0.0.0:53 # 开启 DNS 服务器监听 # ipv6: false # false 将返回 AAAA 的空结果 @@ -108,7 +166,7 @@ dns: - 8.8.8.8 # default value - tls://223.5.5.5:853 # DNS over TLS - https://doh.pub/dns-query # DNS over HTTPS - - https://dns.alidns.com/dns-query#h3=true # 强制HTTP/3 + - https://dns.alidns.com/dns-query#h3=true # 强制 HTTP/3,与 perfer-h3 无关,强制开启 DoH 的 HTTP/3 支持,若不支持将无法使用 - https://mozilla.cloudflare-dns.com/dns-query#DNS&h3=true # 指定策略组和使用 HTTP/3 - dhcp://en0 # dns from dhcp - quic://dns.adguard.com:784 # DNS over QUIC @@ -161,7 +219,8 @@ proxies: server: server port: 443 cipher: chacha20-ietf-poly1305 - password: "password" + password: + "password" # udp: true # udp-over-tcp: false # ip-version: ipv4 # 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer。默认使用 dual @@ -391,27 +450,59 @@ proxies: path: "/" headers: Host: example.com - + #hysteria - name: "hysteria" type: hysteria server: server.com port: 443 - auth_str: yourpassword + auth_str: yourpassword # 将会在未来某个时候删除 + # auth-str: yourpassword # obfs: obfs_str # alpn: # - h3 protocol: udp # 支持 udp/wechat-video/faketcp up: "30 Mbps" # 若不写单位,默认为 Mbps down: "200 Mbps" # 若不写单位,默认为 Mbps - #sni: server.com - #skip-cert-verify: false - #recv_window_conn: 12582912 - #recv_window: 52428800 - #ca: "./my.ca" - #ca_str: "xyz" - #disable_mtu_discovery: false + # sni: server.com + # skip-cert-verify: false + # recv_window_conn: 12582912 # 将会在未来某个时候删除 + # recv-window-conn: 12582912 + # recv_window: 52428800 # 将会在未来某个时候删除 + # recv-window: 52428800 + # ca: "./my.ca" + # ca_str: "xyz" # 将会在未来某个时候删除 + # ca-str: "xyz" + # disable_mtu_discovery: false # fingerprint: xxxx + # fast-open: true # 支持 TCP 快速打开,默认为 false + + - name: "wg" + type: wireguard + server: 162.159.192.1 + port: 2480 + ip: 172.16.0.2 + ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5 + private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= + public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= + udp: true + + - name: tuic + server: www.example.com + port: 10443 + type: tuic + token: TOKEN + # ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server' + # heartbeat-interval: 10000 + # alpn: [h3] + # disable-sni: true + reduce-rtt: true + # request-timeout: 8000 + udp-relay-mode: native # Available: "native", "quic". Default: "native" + # congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic" + # max-udp-relay-packet-size: 1500 + # fast-open: true + # skip-cert-verify: true # ShadowsocksR # The supported ciphers (encryption methods): all stream ciphers in ss @@ -567,3 +658,136 @@ sub-rules: - IP-CIDR,1.1.1.1/32,REJECT - IP-CIDR,8.8.8.8/32,ss1 - DOMAIN,dns.alidns.com,REJECT + +tls: + certificate: string # 证书 PEM 格式,或者 证书的路径 + private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 + +# 流量入站 +listeners: + - name: socks5-in-1 + type: socks + port: 10808 + #listen: 0.0.0.0 # 默认监听 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理 + # udp: false # 默认 true + + - name: http-in-1 + type: http + port: 10809 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + + - name: mixed-in-1 + type: mixed # HTTP(S) 和 SOCKS 代理混合 + port: 10810 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # udp: false # 默认 true + + - name: reidr-in-1 + type: redir + port: 10811 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + + - name: tproxy-in-1 + type: tproxy + port: 10812 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # udp: false # 默认 true + + - name: shadowsocks-in-1 + type: shadowsocks + port: 10813 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= + cipher: 2022-blake3-aes-256-gcm + + - name: vmess-in-1 + type: vmess + port: 10814 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + users: + - username: 1 + uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 + alterId: 1 + + - name: tuic-in-1 + type: tuic + port: 10815 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # token: + # - TOKEN + # certificate: ./server.crt + # private-key: ./server.key + # congestion-controller: bbr + # max-idle-time: 15000 + # authentication-timeout: 1000 + # alpn: + # - h3 + # max-udp-relay-packet-size: 1500 + + - name: tunnel-in-1 + type: tunnel + port: 10816 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + network: [ tcp, udp ] + target: target.com + + - name: tun-in-1 + type: tun + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + stack: system # gvisor / lwip + dns-hijack: + - 0.0.0.0:53 # 需要劫持的 DNS + # auto-detect-interface: false # 自动识别出口网卡 + # auto-route: false # 配置路由表 + # mtu: 9000 # 最大传输单元 + inet4-address: # 必须手动设置ipv4地址段 + - 198.19.0.1/30 + inet6-address: # 必须手动设置ipv6地址段 + - "fdfe:dcba:9877::1/126" + # strict_route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 + # inet4_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + # - 0.0.0.0/1 + # - 128.0.0.0/1 + # inet6_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + # - "::/1" + # - "8000::/1" + # endpoint_independent_nat: false # 启用独立于端点的 NAT + # include_uid: # UID 规则仅在 Linux 下被支持,并且需要 auto_route + # - 0 + # include_uid_range: # 限制被路由的的用户范围 + # - 1000-99999 + # exclude_uid: # 排除路由的的用户 + #- 1000 + # exclude_uid_range: # 排除路由的的用户范围 + # - 1000-99999 + + # Android 用户和应用规则仅在 Android 下被支持 + # 并且需要 auto_route + + # include_android_user: # 限制被路由的 Android 用户 + # - 0 + # - 10 + # include_package: # 限制被路由的 Android 应用包名 + # - com.android.chrome + # exclude_package: # 排除被路由的 Android 应用包名 + # - com.android.captiveportallogin + diff --git a/flake.lock b/flake.lock index 7da95ac3f3..eba25bf64a 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1664705630, - "narHash": "sha256-MLi1J9tIZQFj8v9RKmG89HJAE5ja3z4ui4Tf9+wG/bM=", + "lastModified": 1671072901, + "narHash": "sha256-eyFdLtfxYyZnbJorRiZ2kP2kW4gEU76hLzpZGW9mcZg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f71b215225dec75df6266ff7764d54c2e44ef226", + "rev": "69ce4fbad877f91d4b9bc4cfedfb0ff1fe5043d5", "type": "github" }, "original": { @@ -24,11 +24,11 @@ }, "utils": { "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2b775e2f6d..c372ff4b80 100644 --- a/flake.nix +++ b/flake.nix @@ -23,12 +23,12 @@ { overlay = final: prev: { - clash-meta = final.buildGoModule { + clash-meta = final.buildGo119Module { pname = "clash-meta"; inherit version; src = ./.; - vendorSha256 = "sha256-yhq4WHQcS4CrdcO6KJ5tSn4m7l5g1lNgE9/2BWd9Iys="; + vendorSha256 = "sha256-XVz2vts4on42lfxnov4jnUrHzSFF05+i1TVY3C7bgdw="; # Do not build testing suit excludedPackages = [ "./test" ]; diff --git a/go.mod b/go.mod index 0c772909eb..3eb1fd9146 100644 --- a/go.mod +++ b/go.mod @@ -3,75 +3,76 @@ module github.com/Dreamacro/clash go 1.19 require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/cilium/ebpf v0.9.3 github.com/coreos/go-iptables v0.6.0 - github.com/database64128/tfo-go v1.1.2 + github.com/database64128/tfo-go/v2 v2.0.2 github.com/dlclark/regexp2 v1.7.0 github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.2 - github.com/gofrs/uuid v4.3.0+incompatible + github.com/gofrs/uuid v4.3.1+incompatible github.com/google/gopacket v1.1.19 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.4 github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c - github.com/lucas-clemente/quic-go v0.29.1 + github.com/jpillora/backoff v1.0.0 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 + github.com/mdlayher/netlink v1.7.0 + github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e + github.com/metacubex/sing-shadowsocks v0.1.0 + github.com/metacubex/sing-tun v0.1.0 + github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c github.com/miekg/dns v1.1.50 github.com/oschwald/geoip2-golang v1.8.0 - github.com/sagernet/sing v0.0.0-20220929000216-9a83e35b7186 - github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 - github.com/sagernet/sing-tun v0.0.0-20221012082254-488c3b75f6fd - github.com/sagernet/sing-vmess v0.0.0-20220921140858-b6a1bdee672f + github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 + github.com/sagernet/sing v0.1.0 + github.com/sagernet/sing-vmess v0.1.0 + github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c + github.com/samber/lo v1.35.0 github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.0 - github.com/vishvananda/netlink v1.2.1-beta.2 + github.com/stretchr/testify v1.8.1 github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 go.etcd.io/bbolt v1.3.6 go.uber.org/atomic v1.10.0 go.uber.org/automaxprocs v1.5.1 - golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be + golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 - golang.org/x/net v0.0.0-20221004154528-8021a29435af - golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 - golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 + golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e + golang.org/x/sync v0.1.0 + golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669 google.golang.org/protobuf v1.28.1 gopkg.in/yaml.v3 v3.0.1 - + lukechampine.com/blake3 v1.1.7 ) -replace github.com/vishvananda/netlink => github.com/MetaCubeX/netlink v1.2.0-beta.0.20220529072258-d6853f887820 - -replace github.com/lucas-clemente/quic-go => github.com/tobyxdd/quic-go v0.28.1-0.20220706211558-7780039ad599 - require ( github.com/ajg/form v1.5.1 // indirect - github.com/cheekybits/genny v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/klauspost/cpuid/v2 v2.1.1 // indirect - github.com/marten-seemann/qpack v0.2.1 // indirect - github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect - github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect - github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect - github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/josharian/native v1.0.0 // indirect + github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/marten-seemann/qpack v0.3.0 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect + github.com/mdlayher/socket v0.4.0 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect - github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect - github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect - github.com/vishvananda/netns v0.0.0-20220913150850-18c4f4234207 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/tools v0.1.12 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c // indirect - lukechampine.com/blake3 v1.1.7 // indirect ) + +replace gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c => github.com/metacubex/gvisor v0.0.0-20221217030112-bdcd835fd60e diff --git a/go.sum b/go.sum index d45870ef18..cc305000f1 100644 --- a/go.sum +++ b/go.sum @@ -1,183 +1,119 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/MetaCubeX/netlink v1.2.0-beta.0.20220529072258-d6853f887820 h1:fGKWZ25VApYnuPZoNeqdH/nZtHa2XMajwH6Yj/OgoVc= -github.com/MetaCubeX/netlink v1.2.0-beta.0.20220529072258-d6853f887820/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= -github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc= github.com/cilium/ebpf v0.9.3/go.mod h1:w27N4UjpaQ9X/DGrSugxUG+H+NhgntDuPb5lCzxCn8A= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/database64128/tfo-go v1.1.2 h1:GwxtJp09BdUTVEoeT421t231eNZoGOCRkklbl4WI1kU= -github.com/database64128/tfo-go v1.1.2/go.mod h1:jgrSUPyOvTGQyn6irCOpk7L2W/q/0VLZZcovQiMi+bI= +github.com/database64128/tfo-go/v2 v2.0.2 h1:5rGgkJeLEKlNaqredfrPQNLnctn1b+1fq/8tdKdOzJg= +github.com/database64128/tfo-go/v2 v2.0.2/go.mod h1:FDdt4JaAsRU66wsYHxSVytYimPkKIHupVsxM+5DhvjY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= -github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c h1:OCFM4+DXTWfNlyeoddrTwdup/ztkGSyAMR2UGcPckNQ= github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= +github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= -github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= -github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= -github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= -github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ= -github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= -github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= -github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= -github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= -github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/marten-seemann/qpack v0.3.0 h1:UiWstOgT8+znlkDPOg2+3rIuYXJ2CnGDkGUXN6ki6hE= +github.com/marten-seemann/qpack v0.3.0/go.mod h1:cGfKPBiP4a9EQdxCwEwI/GEeWAsjSekBvx/X8mh58+g= +github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI= +github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE= +github.com/marten-seemann/qtls-go1-19 v0.1.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/netlink v1.7.0 h1:ZNGI4V7i1fJ94DPYtWhI/R85i/Q7ZxnuhUJQcJMoodI= +github.com/mdlayher/netlink v1.7.0/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= +github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= +github.com/metacubex/gvisor v0.0.0-20221217030112-bdcd835fd60e h1:3PHqNvIAwYbv9cOQbRFIUgzJ+K6fhV1HHj+Vpg8U7g8= +github.com/metacubex/gvisor v0.0.0-20221217030112-bdcd835fd60e/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= +github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e h1:RnfC6+sShJ3biU2Q2wuh4FxZ8/3fp1QG+1zAfswVehA= +github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e/go.mod h1:7NPWVTLiX2Ss9q9gBNZaNHsPqZ3Tg/ApyrXxxUYbl78= +github.com/metacubex/sing-shadowsocks v0.1.0 h1:uGBtNkpy4QFlofaNkJf+iFegeLU11VzTUlkC46FHF8A= +github.com/metacubex/sing-shadowsocks v0.1.0/go.mod h1:8pBSYDKVxTtqUtGZyEh4ZpFJXwP6wBVVKrs6oQiOwmQ= +github.com/metacubex/sing-tun v0.1.0 h1:iQj0+0WjJynSKAtfv87wOZlVKWl3w9RvkOSkVe9zuMg= +github.com/metacubex/sing-tun v0.1.0/go.mod h1:l4JyI6RTrlHLQz5vSakg+wxA+LwGVI0Mz5ZtlOv67dA= +github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c h1:VHtXDny/TNOF7YDT9d9Qkr+x6K1O4cejXLlyPUXDeXQ= +github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c/go.mod h1:fULJ451x1/XlpIhl+Oo+EPGKla9tFZaqT5dKLrZ+NvM= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= @@ -185,105 +121,55 @@ github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.0.0-20220929000216-9a83e35b7186 h1:ZDlgH6dTozS3ODaYq1GxCj+H8NvYESaex90iX72gadw= -github.com/sagernet/sing v0.0.0-20220929000216-9a83e35b7186/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= -github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 h1:JJfDeYYhWunvtxsU/mOVNTmFQmnzGx9dY034qG6G3g4= -github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6/go.mod h1:EX3RbZvrwAkPI2nuGa78T2iQXmrkT+/VQtskjou42xM= -github.com/sagernet/sing-tun v0.0.0-20221012082254-488c3b75f6fd h1:TtoZDwg09Cpqi+gCmCtL6w4oEUZ5lHz+vHIjdr1UBNY= -github.com/sagernet/sing-tun v0.0.0-20221012082254-488c3b75f6fd/go.mod h1:1u3pjXA9HmH7kRiBJqM3C/zPxrxnCLd3svmqtub/RFU= -github.com/sagernet/sing-vmess v0.0.0-20220921140858-b6a1bdee672f h1:xyJ3Wbibcug4DxLi/FCHX2Td667SfieyZv645b8+eEE= -github.com/sagernet/sing-vmess v0.0.0-20220921140858-b6a1bdee672f/go.mod h1:bwhAdSNET1X+j9DOXGj9NIQR39xgcWIk1rOQ9lLD+gM= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sagernet/sing v0.1.0 h1:FGmaP2BVPYO2IyC/3R1DaQa/zr+kOKHRgWqrmOF+Gu8= +github.com/sagernet/sing v0.1.0/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= +github.com/sagernet/sing-vmess v0.1.0 h1:x0tYBJRbVi7zVXpMEW45eApGpXIDs9ub3raglouAKMo= +github.com/sagernet/sing-vmess v0.1.0/go.mod h1:4lwj6EHrUlgRnKhbmtboGbt+wtl5+tHMv96Ez8LZArw= +github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c h1:qP3ZOHnjZalvqbjundbXiv/YrNlo3HOgrKc+S1QGs0U= +github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI= +github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= +github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tobyxdd/quic-go v0.28.1-0.20220706211558-7780039ad599 h1:We+z04jRpTGxFggeGWf+GbinhlIk1I1kMMEgujhUfiA= -github.com/tobyxdd/quic-go v0.28.1-0.20220706211558-7780039ad599/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME= -github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.0-20220913150850-18c4f4234207 h1:nn7SOQy8xCu3iXNv7oiBhhEQtbWdnEOMnuKBlHvrqIM= -github.com/vishvananda/netns v0.0.0-20220913150850-18c4f4234207/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 h1:AHhUwwFJGl27E46OpdJHplZkK09m7aETNBNzhT6t15M= github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837/go.mod h1:YJTRELIWrGxR1s8xcEBgxcxBfwQfMGjdvNLTjN9XFgY= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a h1:diz9pEYuTIuLMJLs3rGDkeaTsNyRs6duYdFyPAxzE/U= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -291,95 +177,54 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= -golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e h1:IVOjWZQH/57UDcpX19vSmMz8w3ohroOMWohn8qWpRkg= +golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM= -golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669 h1:pvmSpBoSG0gD2LLPAX15QHPig8xsbU0tu1sSAmResqk= +golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab h1:eHo2TTVBaAPw9lDGK2Gb9GyPMXT6g7O63W6sx3ylbzU= -golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= @@ -388,56 +233,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= -gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index e5bb1ad773..eb4436fb38 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -2,14 +2,13 @@ package executor import ( "fmt" - "github.com/Dreamacro/clash/component/tls" - "github.com/Dreamacro/clash/listener/inner" "net/netip" "os" "runtime" "sync" "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/dialer" @@ -19,13 +18,16 @@ import ( "github.com/Dreamacro/clash/component/profile/cachefile" "github.com/Dreamacro/clash/component/resolver" SNI "github.com/Dreamacro/clash/component/sniffer" + "github.com/Dreamacro/clash/component/tls" "github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/dns" - P "github.com/Dreamacro/clash/listener" + "github.com/Dreamacro/clash/listener" authStore "github.com/Dreamacro/clash/listener/auth" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/inner" "github.com/Dreamacro/clash/listener/tproxy" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" @@ -76,7 +78,7 @@ func ApplyConfig(cfg *config.Config, force bool) { preUpdateExperimental(cfg) updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) - updateRules(cfg.Rules, cfg.RuleProviders) + updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders) updateSniffer(cfg.Sniffer) updateHosts(cfg.Hosts) initInnerTcp() @@ -85,9 +87,11 @@ func ApplyConfig(cfg *config.Config, force bool) { updateProfile(cfg) loadRuleProvider(cfg.RuleProviders) updateGeneral(cfg.General, force) + updateListeners(cfg.Listeners) updateIPTables(cfg) - updateTun(cfg.Tun) + updateTun(cfg.General) updateExperimental(cfg) + updateTunnels(cfg.Tunnels) log.SetLevel(cfg.General.LogLevel) } @@ -97,7 +101,7 @@ func initInnerTcp() { } func GetGeneral() *config.General { - ports := P.GetPorts() + ports := listener.GetPorts() var authenticator []string if auth := authStore.Authenticator(); auth != nil { authenticator = auth.Users() @@ -105,20 +109,23 @@ func GetGeneral() *config.General { general := &config.General{ Inbound: config.Inbound{ - Port: ports.Port, - SocksPort: ports.SocksPort, - RedirPort: ports.RedirPort, - TProxyPort: ports.TProxyPort, - MixedPort: ports.MixedPort, - Authentication: authenticator, - AllowLan: P.AllowLan(), - BindAddress: P.BindAddress(), + Port: ports.Port, + SocksPort: ports.SocksPort, + RedirPort: ports.RedirPort, + TProxyPort: ports.TProxyPort, + MixedPort: ports.MixedPort, + Tun: listener.GetTunConf(), + TuicServer: listener.GetTuicConf(), + ShadowSocksConfig: ports.ShadowSocksConfig, + VmessConfig: ports.VmessConfig, + Authentication: authenticator, + AllowLan: listener.AllowLan(), + BindAddress: listener.BindAddress(), }, Mode: tunnel.Mode(), LogLevel: log.Level(), IPv6: !resolver.DisableIPv6, GeodataLoader: G.LoaderName(), - Tun: P.GetTunConf(), Interface: dialer.DefaultInterface.Load(), Sniffing: tunnel.IsSniffing(), TCPConcurrent: dialer.GetDial(), @@ -127,6 +134,13 @@ func GetGeneral() *config.General { return general } +func updateListeners(listeners map[string]C.InboundListener) { + tcpIn := tunnel.TCPIn() + udpIn := tunnel.UDPIn() + + listener.PatchInboundListeners(listeners, tcpIn, udpIn, true) +} + func updateExperimental(c *config.Config) { runtime.GC() } @@ -198,8 +212,8 @@ func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.Pro tunnel.UpdateProxies(proxies, providers) } -func updateRules(rules []C.Rule, ruleProviders map[string]provider.RuleProvider) { - tunnel.UpdateRules(rules, ruleProviders) +func updateRules(rules []C.Rule, subRules map[string][]C.Rule, ruleProviders map[string]provider.RuleProvider) { + tunnel.UpdateRules(rules, subRules, ruleProviders) } func loadProvider(pv provider.Provider) { @@ -258,9 +272,12 @@ func loadProxyProvider(proxyProviders map[string]provider.ProxyProvider) { wg.Wait() } -func updateTun(tun *config.Tun) { - P.ReCreateTun(tun, tunnel.TCPIn(), tunnel.UDPIn()) - P.ReCreateRedirToTun(tun.RedirectToTun) +func updateTun(general *config.General) { + if general == nil { + return + } + listener.ReCreateTun(LC.Tun(general.Tun), tunnel.TCPIn(), tunnel.UDPIn()) + listener.ReCreateRedirToTun(general.Tun.RedirectToTun) } func updateSniffer(sniffer *config.Sniffer) { @@ -286,6 +303,10 @@ func updateSniffer(sniffer *config.Sniffer) { } } +func updateTunnels(tunnels []LC.Tunnel) { + listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn()) +} + func updateGeneral(general *config.General, force bool) { tunnel.SetMode(general.Mode) tunnel.SetAlwaysFindProcess(general.EnableProcess) @@ -323,22 +344,25 @@ func updateGeneral(general *config.General, force bool) { G.SetLoader(geodataLoader) allowLan := general.AllowLan - P.SetAllowLan(allowLan) + listener.SetAllowLan(allowLan) bindAddress := general.BindAddress - P.SetBindAddress(bindAddress) + listener.SetBindAddress(bindAddress) - P.SetInboundTfo(general.InboundTfo) + inbound.SetTfo(general.InboundTfo) tcpIn := tunnel.TCPIn() udpIn := tunnel.UDPIn() - P.ReCreateHTTP(general.Port, tcpIn) - P.ReCreateSocks(general.SocksPort, tcpIn, udpIn) - P.ReCreateRedir(general.RedirPort, tcpIn, udpIn) - P.ReCreateAutoRedir(general.EBpf.AutoRedir, tcpIn, udpIn) - P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) - P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) + listener.ReCreateHTTP(general.Port, tcpIn) + listener.ReCreateSocks(general.SocksPort, tcpIn, udpIn) + listener.ReCreateRedir(general.RedirPort, tcpIn, udpIn) + listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tcpIn, udpIn) + listener.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) + listener.ReCreateMixed(general.MixedPort, tcpIn, udpIn) + listener.ReCreateShadowSocks(general.ShadowSocksConfig, tcpIn, udpIn) + listener.ReCreateVmess(general.VmessConfig, tcpIn, udpIn) + listener.ReCreateTuic(LC.TuicServer(general.TuicServer), tcpIn, udpIn) } func updateUsers(users []auth.AuthUser) { @@ -400,7 +424,7 @@ func updateIPTables(cfg *config.Config) { } }() - if cfg.Tun.Enable { + if cfg.General.Tun.Enable { err = fmt.Errorf("when tun is enabled, iptables cannot be set automatically") return } @@ -445,7 +469,7 @@ func updateIPTables(cfg *config.Config) { } func Shutdown() { - P.Cleanup(false) + listener.Cleanup(false) tproxy.CleanupTProxyIPTables() resolver.StoreFakePoolState() diff --git a/hub/hub.go b/hub/hub.go index 471fdb5e17..ee18e70a4e 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -42,7 +42,8 @@ func Parse(options ...Option) error { } if cfg.General.ExternalController != "" { - go route.Start(cfg.General.ExternalController, cfg.General.Secret) + go route.Start(cfg.General.ExternalController,cfg.General.ExternalControllerTLS, + cfg.General.Secret,cfg.TLS.Certificate,cfg.TLS.PrivateKey) } executor.ApplyConfig(cfg, true) diff --git a/hub/route/configs.go b/hub/route/configs.go index 82f6528424..37acac5ec6 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -1,16 +1,18 @@ package route import ( - "github.com/Dreamacro/clash/component/dialer" "net/http" "path/filepath" "sync" + "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" + C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/hub/executor" P "github.com/Dreamacro/clash/listener" + LC "github.com/Dreamacro/clash/listener/config" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" @@ -33,20 +35,64 @@ func configRouter() http.Handler { } type configSchema struct { - Port *int `json:"port"` - SocksPort *int `json:"socks-port"` - RedirPort *int `json:"redir-port"` - TProxyPort *int `json:"tproxy-port"` - MixedPort *int `json:"mixed-port"` - Tun *config.Tun `json:"tun"` - AllowLan *bool `json:"allow-lan"` - BindAddress *string `json:"bind-address"` - Mode *tunnel.TunnelMode `json:"mode"` - LogLevel *log.LogLevel `json:"log-level"` - IPv6 *bool `json:"ipv6"` - Sniffing *bool `json:"sniffing"` - TcpConcurrent *bool `json:"tcp-concurrent"` - InterfaceName *string `json:"interface-name"` + Port *int `json:"port"` + SocksPort *int `json:"socks-port"` + RedirPort *int `json:"redir-port"` + TProxyPort *int `json:"tproxy-port"` + MixedPort *int `json:"mixed-port"` + Tun *tunSchema `json:"tun"` + TuicServer *tuicServerSchema `json:"tuic-server"` + ShadowSocksConfig *string `json:"ss-config"` + VmessConfig *string `json:"vmess-config"` + TcptunConfig *string `json:"tcptun-config"` + UdptunConfig *string `json:"udptun-config"` + AllowLan *bool `json:"allow-lan"` + BindAddress *string `json:"bind-address"` + Mode *tunnel.TunnelMode `json:"mode"` + LogLevel *log.LogLevel `json:"log-level"` + IPv6 *bool `json:"ipv6"` + Sniffing *bool `json:"sniffing"` + TcpConcurrent *bool `json:"tcp-concurrent"` + InterfaceName *string `json:"interface-name"` +} + +type tunSchema struct { + Enable bool `yaml:"enable" json:"enable"` + Device *string `yaml:"device" json:"device"` + Stack *C.TUNStack `yaml:"stack" json:"stack"` + DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"` + AutoRoute *bool `yaml:"auto-route" json:"auto-route"` + AutoDetectInterface *bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` + //RedirectToTun []string `yaml:"-" json:"-"` + + MTU *uint32 `yaml:"mtu" json:"mtu,omitempty"` + //Inet4Address *[]config.ListenPrefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address *[]LC.ListenPrefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` + Inet4RouteAddress *[]LC.ListenPrefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` + Inet6RouteAddress *[]LC.ListenPrefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` + IncludeUID *[]uint32 `yaml:"include-uid" json:"include-uid,omitempty"` + IncludeUIDRange *[]string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` + ExcludeUID *[]uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` + ExcludeUIDRange *[]string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` + IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"` + IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"` + ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` + EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` + UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` +} + +type tuicServerSchema struct { + Enable bool `yaml:"enable" json:"enable"` + Listen *string `yaml:"listen" json:"listen"` + Token *[]string `yaml:"token" json:"token"` + Certificate *string `yaml:"certificate" json:"certificate"` + PrivateKey *string `yaml:"private-key" json:"private-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"` + ALPN *[]string `yaml:"alpn" json:"alpn,omitempty"` + MaxUdpRelayPacketSize *int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` } func getConfigs(w http.ResponseWriter, r *http.Request) { @@ -62,6 +108,106 @@ func pointerOrDefault(p *int, def int) int { return def } +func pointerOrDefaultString(p *string, def string) string { + if p != nil { + return *p + } + + return def +} + +func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { + if p != nil { + def.Enable = p.Enable + if p.Device != nil { + def.Device = *p.Device + } + if p.Stack != nil { + def.Stack = *p.Stack + } + if p.DNSHijack != nil { + def.DNSHijack = *p.DNSHijack + } + if p.AutoRoute != nil { + def.AutoRoute = *p.AutoRoute + } + if p.AutoDetectInterface != nil { + def.AutoDetectInterface = *p.AutoDetectInterface + } + if p.MTU != nil { + def.MTU = *p.MTU + } + //if p.Inet4Address != nil { + // def.Inet4Address = *p.Inet4Address + //} + if p.Inet6Address != nil { + def.Inet6Address = *p.Inet6Address + } + if p.IncludeUID != nil { + def.IncludeUID = *p.IncludeUID + } + if p.IncludeUIDRange != nil { + def.IncludeUIDRange = *p.IncludeUIDRange + } + if p.ExcludeUID != nil { + def.ExcludeUID = *p.ExcludeUID + } + if p.ExcludeUIDRange != nil { + def.ExcludeUIDRange = *p.ExcludeUIDRange + } + if p.IncludeAndroidUser != nil { + def.IncludeAndroidUser = *p.IncludeAndroidUser + } + if p.IncludePackage != nil { + def.IncludePackage = *p.IncludePackage + } + if p.ExcludePackage != nil { + def.ExcludePackage = *p.ExcludePackage + } + if p.EndpointIndependentNat != nil { + def.EndpointIndependentNat = *p.EndpointIndependentNat + } + if p.UDPTimeout != nil { + def.UDPTimeout = *p.UDPTimeout + } + } + return def +} + +func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicServer { + if p != nil { + def.Enable = p.Enable + if p.Listen != nil { + def.Listen = *p.Listen + } + if p.Token != nil { + def.Token = *p.Token + } + if p.Certificate != nil { + def.Certificate = *p.Certificate + } + if p.PrivateKey != nil { + def.PrivateKey = *p.PrivateKey + } + if p.CongestionController != nil { + def.CongestionController = *p.CongestionController + } + if p.MaxIdleTime != nil { + def.MaxIdleTime = *p.MaxIdleTime + } + if p.AuthenticationTimeout != nil { + def.AuthenticationTimeout = *p.AuthenticationTimeout + } + if p.ALPN != nil { + def.ALPN = *p.ALPN + } + if p.MaxUdpRelayPacketSize != nil { + def.MaxUdpRelayPacketSize = *p.MaxUdpRelayPacketSize + } + } + return def +} + func patchConfigs(w http.ResponseWriter, r *http.Request) { general := &configSchema{} if err := render.DecodeJSON(r.Body, general); err != nil { @@ -100,6 +246,10 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn) P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn) P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn) + P.ReCreateTun(pointerOrDefaultTun(general.Tun, P.LastTunConf), tcpIn, udpIn) + P.ReCreateShadowSocks(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tcpIn, udpIn) + P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn) + P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tcpIn, udpIn) if general.Mode != nil { tunnel.SetMode(*general.Mode) diff --git a/hub/route/server.go b/hub/route/server.go index 7ebc4ceb0f..2bef92c162 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -2,14 +2,15 @@ package route import ( "bytes" + "crypto/tls" "encoding/json" - "net" "net/http" "strings" "time" + "github.com/Dreamacro/clash/adapter/inbound" + CN "github.com/Dreamacro/clash/common/net" C "github.com/Dreamacro/clash/constant" - _ "github.com/Dreamacro/clash/constant/mime" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel/statistic" @@ -41,7 +42,8 @@ func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } -func Start(addr string, secret string) { +func Start(addr string, tlsAddr string, secret string, + certificat, privateKey string) { if serverAddr != "" { return } @@ -50,18 +52,15 @@ func Start(addr string, secret string) { serverSecret = secret r := chi.NewRouter() - corsM := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, AllowedHeaders: []string{"Content-Type", "Authorization"}, MaxAge: 300, }) - r.Use(corsM.Handler) r.Group(func(r chi.Router) { r.Use(authentication) - r.Get("/", hello) r.Get("/logs", getLogs) r.Get("/traffic", traffic) @@ -86,16 +85,46 @@ func Start(addr string, secret string) { }) } - l, err := net.Listen("tcp", addr) + if len(tlsAddr) > 0 { + go func() { + c, err := CN.ParseCert(certificat, privateKey) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + + l, err := inbound.Listen("tcp", tlsAddr) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + + serverAddr = l.Addr().String() + log.Infoln("RESTful API tls listening at: %s", serverAddr) + tlsServe := &http.Server{ + Handler: r, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{c}, + }, + } + if err = tlsServe.ServeTLS(l, "", ""); err != nil { + log.Errorln("External controller tls serve error: %s", err) + } + }() + } + + l, err := inbound.Listen("tcp", addr) if err != nil { log.Errorln("External controller listen error: %s", err) return } serverAddr = l.Addr().String() log.Infoln("RESTful API listening at: %s", serverAddr) + if err = http.Serve(l, r); err != nil { log.Errorln("External controller serve error: %s", err) } + } func authentication(next http.Handler) http.Handler { @@ -211,16 +240,26 @@ func getLogs(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) } + ch := make(chan log.Event, 1024) sub := log.Subscribe() defer log.UnSubscribe(sub) buf := &bytes.Buffer{} - var err error - for elm := range sub { - buf.Reset() - logM := elm + + go func() { + for logM := range sub { + select { + case ch <- logM: + default: + } + } + close(ch) + }() + + for logM := range ch { if logM.LogLevel < level { continue } + buf.Reset() if err := json.NewEncoder(buf).Encode(Log{ Type: logM.Type(), @@ -229,6 +268,7 @@ func getLogs(w http.ResponseWriter, r *http.Request) { break } + var err error if wsConn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() diff --git a/listener/autoredir/tcp.go b/listener/autoredir/tcp.go index efcd668bc1..854d31d613 100644 --- a/listener/autoredir/tcp.go +++ b/listener/autoredir/tcp.go @@ -14,6 +14,7 @@ type Listener struct { listener net.Listener addr string closed bool + additions []inbound.Addition lookupFunc func(netip.AddrPort) (socks5.Addr, error) } @@ -56,17 +57,24 @@ func (l *Listener) handleRedir(conn net.Conn, in chan<- C.ConnContext) { _ = conn.(*net.TCPConn).SetKeepAlive(true) - in <- inbound.NewSocket(target, conn, C.REDIR) + in <- inbound.NewSocket(target, conn, C.REDIR, l.additions...) } -func New(addr string, in chan<- C.ConnContext) (*Listener, error) { +func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-REDIR"), + inbound.WithSpecialRules(""), + } + } l, err := net.Listen("tcp", addr) if err != nil { return nil, err } rl := &Listener{ - listener: l, - addr: addr, + listener: l, + addr: addr, + additions: additions, } go func() { diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go new file mode 100644 index 0000000000..cfe31f622f --- /dev/null +++ b/listener/config/shadowsocks.go @@ -0,0 +1,17 @@ +package config + +import ( + "encoding/json" +) + +type ShadowsocksServer struct { + Enable bool + Listen string + Password string + Cipher string +} + +func (t ShadowsocksServer) String() string { + b, _ := json.Marshal(t) + return string(b) +} diff --git a/listener/config/tuic.go b/listener/config/tuic.go new file mode 100644 index 0000000000..c584bbf5ea --- /dev/null +++ b/listener/config/tuic.go @@ -0,0 +1,23 @@ +package config + +import ( + "encoding/json" +) + +type TuicServer struct { + Enable bool `yaml:"enable" json:"enable"` + Listen string `yaml:"listen" json:"listen"` + Token []string `yaml:"token" json:"token"` + Certificate string `yaml:"certificate" json:"certificate"` + PrivateKey string `yaml:"private-key" json:"private-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"` + ALPN []string `yaml:"alpn" json:"alpn,omitempty"` + MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` +} + +func (t TuicServer) String() string { + b, _ := json.Marshal(t) + return string(b) +} diff --git a/listener/config/tun.go b/listener/config/tun.go new file mode 100644 index 0000000000..2e1d1a7175 --- /dev/null +++ b/listener/config/tun.go @@ -0,0 +1,98 @@ +package config + +import ( + "encoding/json" + "net/netip" + + C "github.com/Dreamacro/clash/constant" + + "gopkg.in/yaml.v3" +) + +type ListenPrefix netip.Prefix + +func (p ListenPrefix) MarshalJSON() ([]byte, error) { + prefix := netip.Prefix(p) + if !prefix.IsValid() { + return json.Marshal(nil) + } + return json.Marshal(prefix.String()) +} + +func (p ListenPrefix) MarshalYAML() (interface{}, error) { + prefix := netip.Prefix(p) + if !prefix.IsValid() { + return nil, nil + } + return prefix.String(), nil +} + +func (p *ListenPrefix) UnmarshalJSON(bytes []byte) error { + var value string + err := json.Unmarshal(bytes, &value) + if err != nil { + return err + } + prefix, err := netip.ParsePrefix(value) + if err != nil { + return err + } + *p = ListenPrefix(prefix) + return nil +} + +func (p *ListenPrefix) UnmarshalYAML(node *yaml.Node) error { + var value string + err := node.Decode(&value) + if err != nil { + return err + } + prefix, err := netip.ParsePrefix(value) + if err != nil { + return err + } + *p = ListenPrefix(prefix) + return nil +} + +func (p ListenPrefix) Build() netip.Prefix { + return netip.Prefix(p) +} + +func StringSliceToListenPrefixSlice(ss []string) ([]ListenPrefix, error) { + lps := make([]ListenPrefix, 0, len(ss)) + for _, s := range ss { + prefix, err := netip.ParsePrefix(s) + if err != nil { + return nil, err + } + lps = append(lps, ListenPrefix(prefix)) + } + return lps, nil +} + +type Tun struct { + Enable bool `yaml:"enable" json:"enable"` + Device string `yaml:"device" json:"device"` + Stack C.TUNStack `yaml:"stack" json:"stack"` + DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` + AutoRoute bool `yaml:"auto-route" json:"auto-route"` + AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` + RedirectToTun []string `yaml:"-" json:"-"` + + MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` + Inet4Address []ListenPrefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address []ListenPrefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` + Inet4RouteAddress []ListenPrefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` + Inet6RouteAddress []ListenPrefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` + IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` + IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` + ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` + ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` + IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` + IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` + ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` + EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` + UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` +} diff --git a/listener/config/tunnel.go b/listener/config/tunnel.go new file mode 100644 index 0000000000..eadee4b176 --- /dev/null +++ b/listener/config/tunnel.go @@ -0,0 +1,69 @@ +package config + +import ( + "fmt" + "net" + "strings" + + "github.com/samber/lo" +) + +type tunnel struct { + Network []string `yaml:"network"` + Address string `yaml:"address"` + Target string `yaml:"target"` + Proxy string `yaml:"proxy"` +} + +type Tunnel tunnel + +// UnmarshalYAML implements yaml.Unmarshaler +func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + if err := unmarshal(&tp); err != nil { + var inner tunnel + if err := unmarshal(&inner); err != nil { + return err + } + + *t = Tunnel(inner) + return nil + } + + // parse udp/tcp,address,target,proxy + parts := lo.Map(strings.Split(tp, ","), func(s string, _ int) string { + return strings.TrimSpace(s) + }) + if len(parts) != 3 && len(parts) != 4 { + return fmt.Errorf("invalid tunnel config %s", tp) + } + network := strings.Split(parts[0], "/") + + // validate network + for _, n := range network { + switch n { + case "tcp", "udp": + default: + return fmt.Errorf("invalid tunnel network %s", n) + } + } + + // validate address and target + address := parts[1] + target := parts[2] + for _, addr := range []string{address, target} { + if _, _, err := net.SplitHostPort(addr); err != nil { + return fmt.Errorf("invalid tunnel target or address %s", addr) + } + } + + *t = Tunnel(tunnel{ + Network: network, + Address: address, + Target: target, + }) + if len(parts) == 4 { + t.Proxy = parts[3] + } + return nil +} diff --git a/listener/config/vmess.go b/listener/config/vmess.go new file mode 100644 index 0000000000..cc49433e65 --- /dev/null +++ b/listener/config/vmess.go @@ -0,0 +1,22 @@ +package config + +import ( + "encoding/json" +) + +type VmessUser struct { + Username string + UUID string + AlterID int +} + +type VmessServer struct { + Enable bool + Listen string + Users []VmessUser +} + +func (t VmessServer) String() string { + b, _ := json.Marshal(t) + return string(b) +} diff --git a/listener/http/client.go b/listener/http/client.go index 873a9a3c9b..15c21f9185 100644 --- a/listener/http/client.go +++ b/listener/http/client.go @@ -12,7 +12,7 @@ import ( "github.com/Dreamacro/clash/transport/socks5" ) -func newClient(source net.Addr, in chan<- C.ConnContext) *http.Client { +func newClient(source net.Addr, in chan<- C.ConnContext, additions ...inbound.Addition) *http.Client { return &http.Client{ Transport: &http.Transport{ // from http.DefaultTransport @@ -32,7 +32,7 @@ func newClient(source net.Addr, in chan<- C.ConnContext) *http.Client { left, right := net.Pipe() - in <- inbound.NewHTTP(dstAddr, source, right) + in <- inbound.NewHTTP(dstAddr, source, right, additions...) return left, nil }, diff --git a/listener/http/proxy.go b/listener/http/proxy.go index b57ff4f3f9..a95f719576 100644 --- a/listener/http/proxy.go +++ b/listener/http/proxy.go @@ -5,7 +5,6 @@ import ( "net" "net/http" "strings" - "time" "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/common/cache" @@ -15,8 +14,8 @@ import ( "github.com/Dreamacro/clash/log" ) -func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) { - client := newClient(c.RemoteAddr(), in) +func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[string, bool], additions ...inbound.Addition) { + client := newClient(c.RemoteAddr(), in, additions...) defer client.CloseIdleConnections() conn := N.NewBufferedConn(c) @@ -49,7 +48,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, break // close connection } - in <- inbound.NewHTTPS(request, conn) + in <- inbound.NewHTTPS(request, conn, additions...) return // hijack connection } @@ -62,9 +61,9 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, request.RequestURI = "" if isUpgradeRequest(request) { - if resp = handleUpgrade(conn, conn.RemoteAddr(), request, in); resp == nil { - return // hijack connection - } + handleUpgrade(conn, request, in, additions...) + + return // hijack connection } removeHopByHopHeaders(request.Header) @@ -99,7 +98,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, _ = conn.Close() } -func authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http.Response { +func authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *http.Response { authenticator := authStore.Authenticator() if authenticator != nil { credential := parseBasicProxyAuthorization(request) @@ -109,11 +108,11 @@ func authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http return resp } - var authed bool - if authed = cache.Get(credential); !authed { + authed, exist := cache.Get(credential) + if !exist { user, pass, err := decodeBasicProxyAuthorization(credential) authed = err == nil && authenticator.Verify(user, pass) - cache.Put(credential, authed, time.Minute) + cache.Set(credential, authed) } if !authed { log.Infoln("Auth failed from %s", request.RemoteAddr) diff --git a/listener/http/server.go b/listener/http/server.go index edbca1b2cc..8819af11cf 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -1,11 +1,9 @@ package http import ( - "context" - "github.com/database64128/tfo-go" "net" - "time" + "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/common/cache" C "github.com/Dreamacro/clash/constant" ) @@ -32,23 +30,26 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, inboundTfo bool, in chan<- C.ConnContext) (*Listener, error) { - return NewWithAuthenticate(addr, in, true, inboundTfo) +func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + return NewWithAuthenticate(addr, in, true, additions...) } -func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool, inboundTfo bool) (*Listener, error) { - lc := tfo.ListenConfig{ - DisableTFO: !inboundTfo, +func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-HTTP"), + inbound.WithSpecialRules(""), + } } - l, err := lc.Listen(context.Background(), "tcp", addr) + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } - var c *cache.Cache[string, bool] + var c *cache.LruCache[string, bool] if authenticate { - c = cache.New[string, bool](time.Second * 30) + c = cache.New[string, bool](cache.WithAge[string, bool](30)) } hl := &Listener{ @@ -64,7 +65,7 @@ func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool } continue } - go HandleConn(conn, in, c) + go HandleConn(conn, in, c, additions...) } }() diff --git a/listener/http/upgrade.go b/listener/http/upgrade.go index 251c842ad7..90e28f0a4d 100644 --- a/listener/http/upgrade.go +++ b/listener/http/upgrade.go @@ -6,7 +6,6 @@ import ( "net" "net/http" "strings" - "time" "github.com/Dreamacro/clash/adapter/inbound" N "github.com/Dreamacro/clash/common/net" @@ -26,17 +25,15 @@ func isUpgradeRequest(req *http.Request) bool { return false } -func handleUpgrade(localConn net.Conn, source net.Addr, request *http.Request, in chan<- C.ConnContext) (resp *http.Response) { +func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext, additions ...inbound.Addition) { + defer conn.Close() + removeProxyHeaders(request.Header) removeExtraHTTPHostPort(request) address := request.Host if _, _, err := net.SplitHostPort(address); err != nil { - port := "80" - if request.TLS != nil { - port = "443" - } - address = net.JoinHostPort(address, port) + address = net.JoinHostPort(address, "80") } dstAddr := socks5.ParseAddr(address) @@ -46,9 +43,9 @@ func handleUpgrade(localConn net.Conn, source net.Addr, request *http.Request, i left, right := net.Pipe() - in <- inbound.NewHTTP(dstAddr, source, right) + in <- inbound.NewHTTP(dstAddr, conn.RemoteAddr(), right, additions...) - var remoteServer *N.BufferedConn + var bufferedLeft *N.BufferedConn if request.TLS != nil { tlsConn := tls.Client(left, &tls.Config{ ServerName: request.URL.Hostname(), @@ -57,47 +54,36 @@ func handleUpgrade(localConn net.Conn, source net.Addr, request *http.Request, i ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() if tlsConn.HandshakeContext(ctx) != nil { - _ = localConn.Close() _ = left.Close() return } - remoteServer = N.NewBufferedConn(tlsConn) + bufferedLeft = N.NewBufferedConn(tlsConn) } else { - remoteServer = N.NewBufferedConn(left) + bufferedLeft = N.NewBufferedConn(left) } defer func() { - _ = remoteServer.Close() + _ = bufferedLeft.Close() }() - err := request.Write(remoteServer) + err := request.Write(bufferedLeft) if err != nil { - _ = localConn.Close() return } - resp, err = http.ReadResponse(remoteServer.Reader(), request) + resp, err := http.ReadResponse(bufferedLeft.Reader(), request) if err != nil { - _ = localConn.Close() return } - if resp.StatusCode == http.StatusSwitchingProtocols { - removeProxyHeaders(resp.Header) - - err = localConn.SetReadDeadline(time.Time{}) // set to not time out - if err != nil { - return - } + removeProxyHeaders(resp.Header) - err = resp.Write(localConn) - if err != nil { - return - } + err = resp.Write(conn) + if err != nil { + return + } - N.Relay(remoteServer, localConn) // blocking here - _ = localConn.Close() - resp = nil + if resp.StatusCode == http.StatusSwitchingProtocols { + N.Relay(bufferedLeft, conn) } - return } diff --git a/listener/inbound/base.go b/listener/inbound/base.go new file mode 100644 index 0000000000..41be5b10d3 --- /dev/null +++ b/listener/inbound/base.go @@ -0,0 +1,103 @@ +package inbound + +import ( + "encoding/json" + "net" + "net/netip" + "strconv" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" +) + +type Base struct { + config *BaseOption + name string + specialRules string + listenAddr netip.Addr + port int +} + +func NewBase(options *BaseOption) (*Base, error) { + if options.Listen == "" { + options.Listen = "0.0.0.0" + } + addr, err := netip.ParseAddr(options.Listen) + if err != nil { + return nil, err + } + return &Base{ + name: options.Name(), + listenAddr: addr, + specialRules: options.SpecialRules, + port: options.Port, + config: options, + }, nil +} + +// Config implements constant.InboundListener +func (b *Base) Config() C.InboundConfig { + return b.config +} + +// Address implements constant.InboundListener +func (b *Base) Address() string { + return b.RawAddress() +} + +// Close implements constant.InboundListener +func (*Base) Close() error { + return nil +} + +// Name implements constant.InboundListener +func (b *Base) Name() string { + return b.name +} + +// RawAddress implements constant.InboundListener +func (b *Base) RawAddress() string { + return net.JoinHostPort(b.listenAddr.String(), strconv.Itoa(int(b.port))) +} + +// Listen implements constant.InboundListener +func (*Base) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + return nil +} + +func (b *Base) Additions() []inbound.Addition { + return b.config.Additions() +} + +var _ C.InboundListener = (*Base)(nil) + +type BaseOption struct { + NameStr string `inbound:"name"` + Listen string `inbound:"listen,omitempty"` + Port int `inbound:"port,omitempty"` + SpecialRules string `inbound:"rule,omitempty"` + SpecialProxy string `inbound:"proxy,omitempty"` +} + +func (o BaseOption) Name() string { + return o.NameStr +} + +func (o BaseOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +func (o BaseOption) Additions() []inbound.Addition { + return []inbound.Addition{ + inbound.WithInName(o.NameStr), + inbound.WithSpecialRules(o.SpecialRules), + inbound.WithSpecialProxy(o.SpecialProxy), + } +} + +var _ C.InboundConfig = (*BaseOption)(nil) + +func optionToString(option any) string { + str, _ := json.Marshal(option) + return string(str) +} diff --git a/listener/inbound/http.go b/listener/inbound/http.go new file mode 100644 index 0000000000..b19f015497 --- /dev/null +++ b/listener/inbound/http.go @@ -0,0 +1,63 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/http" + "github.com/Dreamacro/clash/log" +) + +type HTTPOption struct { + BaseOption +} + +func (o HTTPOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type HTTP struct { + *Base + config *HTTPOption + l *http.Listener +} + +func NewHTTP(options *HTTPOption) (*HTTP, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &HTTP{ + Base: base, + config: options, + }, nil +} + +// Config implements constant.InboundListener +func (h *HTTP) Config() C.InboundConfig { + return h.config +} + +// Address implements constant.InboundListener +func (h *HTTP) Address() string { + return h.l.Address() +} + +// Listen implements constant.InboundListener +func (h *HTTP) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + h.l, err = http.New(h.RawAddress(), tcpIn, h.Additions()...) + if err != nil { + return err + } + log.Infoln("HTTP[%s] proxy listening at: %s", h.Name(), h.Address()) + return nil +} + +// Close implements constant.InboundListener +func (h *HTTP) Close() error { + if h.l != nil { + return h.l.Close() + } + return nil +} + +var _ C.InboundListener = (*HTTP)(nil) diff --git a/listener/inbound/mixed.go b/listener/inbound/mixed.go new file mode 100644 index 0000000000..a2920c698d --- /dev/null +++ b/listener/inbound/mixed.go @@ -0,0 +1,89 @@ +package inbound + +import ( + "fmt" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + + "github.com/Dreamacro/clash/listener/mixed" + "github.com/Dreamacro/clash/listener/socks" +) + +type MixedOption struct { + BaseOption + UDP bool `inbound:"udp,omitempty"` +} + +func (o MixedOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Mixed struct { + *Base + config *MixedOption + l *mixed.Listener + lUDP *socks.UDPListener + udp bool +} + +func NewMixed(options *MixedOption) (*Mixed, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &Mixed{ + Base: base, + config: options, + udp: options.UDP, + }, nil +} + +// Config implements constant.InboundListener +func (m *Mixed) Config() C.InboundConfig { + return m.config +} + +// Address implements constant.InboundListener +func (m *Mixed) Address() string { + return m.l.Address() +} + +// Listen implements constant.InboundListener +func (m *Mixed) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + m.l, err = mixed.New(m.RawAddress(), tcpIn, m.Additions()...) + if err != nil { + return err + } + if m.udp { + m.lUDP, err = socks.NewUDP(m.RawAddress(), udpIn, m.Additions()...) + if err != nil { + return err + } + } + log.Infoln("Mixed(http+socks)[%s] proxy listening at: %s", m.Name(), m.Address()) + return nil +} + +// Close implements constant.InboundListener +func (m *Mixed) Close() error { + var err error + if m.l != nil { + if tcpErr := m.l.Close(); tcpErr != nil { + err = tcpErr + } + } + if m.udp && m.lUDP != nil { + if udpErr := m.lUDP.Close(); udpErr != nil { + if err == nil { + err = udpErr + } else { + return fmt.Errorf("close tcp err: %s, close udp err: %s", err.Error(), udpErr.Error()) + } + } + } + return err +} + +var _ C.InboundListener = (*Mixed)(nil) diff --git a/listener/inbound/redir.go b/listener/inbound/redir.go new file mode 100644 index 0000000000..7a1685ba22 --- /dev/null +++ b/listener/inbound/redir.go @@ -0,0 +1,63 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/redir" + "github.com/Dreamacro/clash/log" +) + +type RedirOption struct { + BaseOption +} + +func (o RedirOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Redir struct { + *Base + config *RedirOption + l *redir.Listener +} + +func NewRedir(options *RedirOption) (*Redir, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &Redir{ + Base: base, + config: options, + }, nil +} + +// Config implements constant.InboundListener +func (r *Redir) Config() C.InboundConfig { + return r.config +} + +// Address implements constant.InboundListener +func (r *Redir) Address() string { + return r.l.Address() +} + +// Listen implements constant.InboundListener +func (r *Redir) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + r.l, err = redir.New(r.RawAddress(), tcpIn, r.Additions()...) + if err != nil { + return err + } + log.Infoln("Redir[%s] proxy listening at: %s", r.Name(), r.Address()) + return nil +} + +// Close implements constant.InboundListener +func (r *Redir) Close() error { + if r.l != nil { + r.l.Close() + } + return nil +} + +var _ C.InboundListener = (*Redir)(nil) diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go new file mode 100644 index 0000000000..e6baa80ccf --- /dev/null +++ b/listener/inbound/shadowsocks.go @@ -0,0 +1,75 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing_shadowsocks" + "github.com/Dreamacro/clash/log" +) + +type ShadowSocksOption struct { + BaseOption + Password string `inbound:"password"` + Cipher string `inbound:"cipher"` +} + +func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type ShadowSocks struct { + *Base + config *ShadowSocksOption + l C.MultiAddrListener + ss LC.ShadowsocksServer +} + +func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &ShadowSocks{ + Base: base, + config: options, + ss: LC.ShadowsocksServer{ + Enable: true, + Listen: base.RawAddress(), + Password: options.Password, + Cipher: options.Cipher, + }, + }, nil +} + +// Config implements constant.InboundListener +func (s *ShadowSocks) Config() C.InboundConfig { + return s.config +} + +// Address implements constant.InboundListener +func (s *ShadowSocks) Address() string { + if s.l != nil { + for _, addr := range s.l.AddrList() { + return addr.String() + } + } + return "" +} + +// Listen implements constant.InboundListener +func (s *ShadowSocks) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + s.l, err = sing_shadowsocks.New(s.ss, tcpIn, udpIn, s.Additions()...) + if err != nil { + return err + } + log.Infoln("ShadowSocks[%s] proxy listening at: %s", s.Name(), s.Address()) + return nil +} + +// Close implements constant.InboundListener +func (s *ShadowSocks) Close() error { + return s.l.Close() +} + +var _ C.InboundListener = (*ShadowSocks)(nil) diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go new file mode 100644 index 0000000000..010d08f90a --- /dev/null +++ b/listener/inbound/socks.go @@ -0,0 +1,86 @@ +package inbound + +import ( + "fmt" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/socks" + "github.com/Dreamacro/clash/log" +) + +type SocksOption struct { + BaseOption + UDP bool `inbound:"udp,omitempty"` +} + +func (o SocksOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Socks struct { + *Base + config *SocksOption + udp bool + stl *socks.Listener + sul *socks.UDPListener +} + +func NewSocks(options *SocksOption) (*Socks, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &Socks{ + Base: base, + config: options, + udp: options.UDP, + }, nil +} + +// Config implements constant.InboundListener +func (s *Socks) Config() C.InboundConfig { + return s.config +} + +// Close implements constant.InboundListener +func (s *Socks) Close() error { + var err error + if s.stl != nil { + if tcpErr := s.stl.Close(); tcpErr != nil { + err = tcpErr + } + } + if s.udp && s.sul != nil { + if udpErr := s.sul.Close(); udpErr != nil { + if err == nil { + err = udpErr + } else { + return fmt.Errorf("close tcp err: %s, close udp err: %s", err.Error(), udpErr.Error()) + } + } + } + + return err +} + +// Address implements constant.InboundListener +func (s *Socks) Address() string { + return s.stl.Address() +} + +// Listen implements constant.InboundListener +func (s *Socks) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + if s.stl, err = socks.New(s.RawAddress(), tcpIn, s.Additions()...); err != nil { + return err + } + if s.udp { + if s.sul, err = socks.NewUDP(s.RawAddress(), udpIn, s.Additions()...); err != nil { + return err + } + } + + log.Infoln("SOCKS[%s] proxy listening at: %s", s.Name(), s.Address()) + return nil +} + +var _ C.InboundListener = (*Socks)(nil) diff --git a/listener/inbound/tproxy.go b/listener/inbound/tproxy.go new file mode 100644 index 0000000000..7aa8af8d23 --- /dev/null +++ b/listener/inbound/tproxy.go @@ -0,0 +1,94 @@ +package inbound + +import ( + "fmt" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/tproxy" + "github.com/Dreamacro/clash/log" +) + +type TProxyOption struct { + BaseOption + UDP bool `inbound:"udp,omitempty"` +} + +func (o TProxyOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type TProxy struct { + *Base + config *TProxyOption + lUDP *tproxy.UDPListener + lTCP *tproxy.Listener + udp bool +} + +func NewTProxy(options *TProxyOption) (*TProxy, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &TProxy{ + Base: base, + config: options, + udp: options.UDP, + }, nil + +} + +// Config implements constant.InboundListener +func (t *TProxy) Config() C.InboundConfig { + return t.config +} + +// Address implements constant.InboundListener +func (t *TProxy) Address() string { + return t.lTCP.Address() +} + +// Listen implements constant.InboundListener +func (t *TProxy) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + t.lTCP, err = tproxy.New(t.RawAddress(), tcpIn, t.Additions()...) + if err != nil { + return err + } + if t.udp { + if t.lUDP != nil { + t.lUDP, err = tproxy.NewUDP(t.RawAddress(), udpIn, t.Additions()...) + if err != nil { + return err + } + } + + } + log.Infoln("TProxy[%s] proxy listening at: %s", t.Name(), t.Address()) + return nil +} + +// Close implements constant.InboundListener +func (t *TProxy) Close() error { + var tcpErr error + var udpErr error + if t.lTCP != nil { + tcpErr = t.lTCP.Close() + } + if t.lUDP != nil { + udpErr = t.lUDP.Close() + } + + if tcpErr != nil && udpErr != nil { + return fmt.Errorf("tcp close err: %s and udp close err: %s", tcpErr, udpErr) + } + if tcpErr != nil { + return tcpErr + } + if udpErr != nil { + return udpErr + } + return nil +} + +var _ C.InboundListener = (*TProxy)(nil) diff --git a/listener/inbound/tuic.go b/listener/inbound/tuic.go new file mode 100644 index 0000000000..f3b8a5f2c7 --- /dev/null +++ b/listener/inbound/tuic.go @@ -0,0 +1,87 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/tuic" + "github.com/Dreamacro/clash/log" +) + +type TuicOption struct { + BaseOption + Token []string `inbound:"token"` + Certificate string `inbound:"certificate"` + PrivateKey string `inbound:"private-key"` + CongestionController string `inbound:"congestion-controllerr,omitempty"` + MaxIdleTime int `inbound:"max-idle-timer,omitempty"` + AuthenticationTimeout int `inbound:"authentication-timeoutr,omitempty"` + ALPN []string `inbound:"alpnr,omitempty"` + MaxUdpRelayPacketSize int `inbound:"max-udp-relay-packet-sizer,omitempty"` +} + +func (o TuicOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Tuic struct { + *Base + config *TuicOption + l *tuic.Listener + ts LC.TuicServer +} + +func NewTuic(options *TuicOption) (*Tuic, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &Tuic{ + Base: base, + config: options, + ts: LC.TuicServer{ + Enable: true, + Listen: base.RawAddress(), + Token: options.Token, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + CongestionController: options.CongestionController, + MaxIdleTime: options.MaxIdleTime, + AuthenticationTimeout: options.AuthenticationTimeout, + ALPN: options.ALPN, + MaxUdpRelayPacketSize: options.MaxUdpRelayPacketSize, + }, + }, nil +} + +// Config implements constant.InboundListener +func (t *Tuic) Config() C.InboundConfig { + return t.config +} + +// Address implements constant.InboundListener +func (t *Tuic) Address() string { + if t.l != nil { + for _, addr := range t.l.AddrList() { + return addr.String() + } + } + return "" +} + +// Listen implements constant.InboundListener +func (t *Tuic) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + t.l, err = tuic.New(t.ts, tcpIn, udpIn, t.Additions()...) + if err != nil { + return err + } + log.Infoln("Tuic[%s] proxy listening at: %s", t.Name(), t.Address()) + return nil +} + +// Close implements constant.InboundListener +func (t *Tuic) Close() error { + return t.l.Close() +} + +var _ C.InboundListener = (*Tuic)(nil) diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go new file mode 100644 index 0000000000..997164c2af --- /dev/null +++ b/listener/inbound/tun.go @@ -0,0 +1,129 @@ +package inbound + +import ( + "errors" + "strings" + + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing_tun" + "github.com/Dreamacro/clash/log" +) + +type TunOption struct { + BaseOption + Device string `inbound:"device,omitempty"` + Stack string `inbound:"stack,omitempty"` + DNSHijack []string `inbound:"dns-hijack,omitempty"` + AutoRoute bool `inbound:"auto-route,omitempty"` + AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"` + + MTU uint32 `inbound:"mtu,omitempty"` + Inet4Address []string `inbound:"inet4_address,omitempty"` + Inet6Address []string `inbound:"inet6_address,omitempty"` + StrictRoute bool `inbound:"strict_route,omitempty"` + Inet4RouteAddress []string `inbound:"inet4_route_address,omitempty"` + Inet6RouteAddress []string `inbound:"inet6_route_address,omitempty"` + IncludeUID []uint32 `inbound:"include_uid,omitempty"` + IncludeUIDRange []string `inbound:"include_uid_range,omitempty"` + ExcludeUID []uint32 `inbound:"exclude_uid,omitempty"` + ExcludeUIDRange []string `inbound:"exclude_uid_range,omitempty"` + IncludeAndroidUser []int `inbound:"include_android_user,omitempty"` + IncludePackage []string `inbound:"include_package,omitempty"` + ExcludePackage []string `inbound:"exclude_package,omitempty"` + EndpointIndependentNat bool `inbound:"endpoint_independent_nat,omitempty"` + UDPTimeout int64 `inbound:"udp_timeout,omitempty"` +} + +func (o TunOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Tun struct { + *Base + config *TunOption + l *sing_tun.Listener + tun LC.Tun +} + +func NewTun(options *TunOption) (*Tun, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + stack, exist := C.StackTypeMapping[strings.ToLower(options.Stack)] + if !exist { + return nil, errors.New("invalid tun stack") + } + inet4Address, err := LC.StringSliceToListenPrefixSlice(options.Inet4Address) + if err != nil { + return nil, err + } + inet6Address, err := LC.StringSliceToListenPrefixSlice(options.Inet6Address) + if err != nil { + return nil, err + } + inet4RouteAddress, err := LC.StringSliceToListenPrefixSlice(options.Inet4RouteAddress) + if err != nil { + return nil, err + } + inet6RouteAddress, err := LC.StringSliceToListenPrefixSlice(options.Inet6RouteAddress) + if err != nil { + return nil, err + } + return &Tun{ + Base: base, + config: options, + tun: LC.Tun{ + Enable: true, + Device: options.Device, + Stack: stack, + DNSHijack: options.DNSHijack, + AutoRoute: options.AutoRoute, + AutoDetectInterface: options.AutoDetectInterface, + MTU: options.MTU, + Inet4Address: inet4Address, + Inet6Address: inet6Address, + StrictRoute: options.StrictRoute, + Inet4RouteAddress: inet4RouteAddress, + Inet6RouteAddress: inet6RouteAddress, + IncludeUID: options.IncludeUID, + IncludeUIDRange: options.IncludeUIDRange, + ExcludeUID: options.ExcludeUID, + ExcludeUIDRange: options.ExcludeUIDRange, + IncludeAndroidUser: options.IncludeAndroidUser, + IncludePackage: options.IncludePackage, + ExcludePackage: options.ExcludePackage, + EndpointIndependentNat: options.EndpointIndependentNat, + UDPTimeout: options.UDPTimeout, + }, + }, nil +} + +// Config implements constant.InboundListener +func (t *Tun) Config() C.InboundConfig { + return t.config +} + +// Address implements constant.InboundListener +func (t *Tun) Address() string { + return t.l.Address() +} + +// Listen implements constant.InboundListener +func (t *Tun) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + t.l, err = sing_tun.New(t.tun, tcpIn, udpIn, t.Additions()...) + if err != nil { + return err + } + log.Infoln("Tun[%s] proxy listening at: %s", t.Name(), t.Address()) + return nil +} + +// Close implements constant.InboundListener +func (t *Tun) Close() error { + return t.l.Close() +} + +var _ C.InboundListener = (*Tun)(nil) diff --git a/listener/inbound/tunnel.go b/listener/inbound/tunnel.go new file mode 100644 index 0000000000..221f4cd6f1 --- /dev/null +++ b/listener/inbound/tunnel.go @@ -0,0 +1,98 @@ +package inbound + +import ( + "fmt" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/tunnel" + "github.com/Dreamacro/clash/log" +) + +type TunnelOption struct { + BaseOption + Network []string `inbound:"network"` + Target string `inbound:"target"` +} + +func (o TunnelOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Tunnel struct { + *Base + config *TunnelOption + ttl *tunnel.Listener + tul *tunnel.PacketConn +} + +func NewTunnel(options *TunnelOption) (*Tunnel, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &Tunnel{ + Base: base, + config: options, + }, nil +} + +// Config implements constant.InboundListener +func (t *Tunnel) Config() C.InboundConfig { + return t.config +} + +// Close implements constant.InboundListener +func (t *Tunnel) Close() error { + var err error + if t.ttl != nil { + if tcpErr := t.ttl.Close(); tcpErr != nil { + err = tcpErr + } + } + if t.tul != nil { + if udpErr := t.tul.Close(); udpErr != nil { + if err == nil { + err = udpErr + } else { + return fmt.Errorf("close tcp err: %s, close udp err: %s", err.Error(), udpErr.Error()) + } + } + } + + return err +} + +// Address implements constant.InboundListener +func (t *Tunnel) Address() string { + if t.ttl != nil { + return t.ttl.Address() + } + if t.tul != nil { + return t.tul.Address() + } + return "" +} + +// Listen implements constant.InboundListener +func (t *Tunnel) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + for _, network := range t.config.Network { + switch network { + case "tcp": + if t.ttl, err = tunnel.New(t.RawAddress(), t.config.Target, t.config.SpecialProxy, tcpIn, t.Additions()...); err != nil { + return err + } + case "udp": + if t.tul, err = tunnel.NewUDP(t.RawAddress(), t.config.Target, t.config.SpecialProxy, udpIn, t.Additions()...); err != nil { + return err + } + default: + log.Warnln("unknown network type: %s, passed", network) + continue + } + log.Infoln("Tunnel[%s](%s/%s)proxy listening at: %s", t.Name(), network, t.config.Target, t.Address()) + } + return nil +} + +var _ C.InboundListener = (*Tunnel)(nil) diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go new file mode 100644 index 0000000000..130e17c5a1 --- /dev/null +++ b/listener/inbound/vmess.go @@ -0,0 +1,95 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing_vmess" + "github.com/Dreamacro/clash/log" +) + +type VmessOption struct { + BaseOption + Users []VmessUser `inbound:"users"` +} + +type VmessUser struct { + Username string `inbound:"username,omitempty"` + UUID string `inbound:"uuid"` + AlterID int `inbound:"alterId"` +} + +func (o VmessOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Vmess struct { + *Base + config *VmessOption + l C.MultiAddrListener + vs LC.VmessServer +} + +func NewVmess(options *VmessOption) (*Vmess, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + users := make([]LC.VmessUser, len(options.Users)) + for i, v := range options.Users { + users[i] = LC.VmessUser{ + Username: v.Username, + UUID: v.UUID, + AlterID: v.AlterID, + } + } + return &Vmess{ + Base: base, + config: options, + vs: LC.VmessServer{ + Enable: true, + Listen: base.RawAddress(), + Users: users, + }, + }, nil +} + +// Config implements constant.InboundListener +func (v *Vmess) Config() C.InboundConfig { + return v.config +} + +// Address implements constant.InboundListener +func (v *Vmess) Address() string { + if v.l != nil { + for _, addr := range v.l.AddrList() { + return addr.String() + } + } + return "" +} + +// Listen implements constant.InboundListener +func (v *Vmess) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) error { + var err error + users := make([]LC.VmessUser, len(v.config.Users)) + for i, v := range v.config.Users { + users[i] = LC.VmessUser{ + Username: v.Username, + UUID: v.UUID, + AlterID: v.AlterID, + } + } + v.l, err = sing_vmess.New(v.vs, tcpIn, udpIn, v.Additions()...) + if err != nil { + return err + } + log.Infoln("Vmess[%s] proxy listening at: %s", v.Name(), v.Address()) + return nil +} + +// Close implements constant.InboundListener +func (v *Vmess) Close() error { + return v.l.Close() +} + +var _ C.InboundListener = (*Vmess)(nil) diff --git a/listener/listener.go b/listener/listener.go index 8512c0b1a8..d747d5f5fa 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -1,47 +1,57 @@ -package proxy +package listener import ( "fmt" - "github.com/Dreamacro/clash/listener/sing_tun" "golang.org/x/exp/slices" "net" "sort" "strconv" + "strings" "sync" - "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/component/ebpf" - "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/listener/autoredir" + LC "github.com/Dreamacro/clash/listener/config" "github.com/Dreamacro/clash/listener/http" - "github.com/Dreamacro/clash/listener/inner" "github.com/Dreamacro/clash/listener/mixed" "github.com/Dreamacro/clash/listener/redir" + embedSS "github.com/Dreamacro/clash/listener/shadowsocks" + "github.com/Dreamacro/clash/listener/sing_shadowsocks" + "github.com/Dreamacro/clash/listener/sing_tun" + "github.com/Dreamacro/clash/listener/sing_vmess" "github.com/Dreamacro/clash/listener/socks" "github.com/Dreamacro/clash/listener/tproxy" + "github.com/Dreamacro/clash/listener/tuic" + "github.com/Dreamacro/clash/listener/tunnel" "github.com/Dreamacro/clash/log" + + "github.com/samber/lo" ) var ( allowLan = false bindAddress = "*" - lastTunConf *config.Tun - inboundTfo = false - - socksListener *socks.Listener - socksUDPListener *socks.UDPListener - httpListener *http.Listener - redirListener *redir.Listener - redirUDPListener *tproxy.UDPListener - tproxyListener *tproxy.Listener - tproxyUDPListener *tproxy.UDPListener - mixedListener *mixed.Listener - mixedUDPLister *socks.UDPListener - tunLister *sing_tun.Listener - autoRedirListener *autoredir.Listener - autoRedirProgram *ebpf.TcEBpfProgram - tcProgram *ebpf.TcEBpfProgram + + socksListener *socks.Listener + socksUDPListener *socks.UDPListener + httpListener *http.Listener + redirListener *redir.Listener + redirUDPListener *tproxy.UDPListener + tproxyListener *tproxy.Listener + tproxyUDPListener *tproxy.UDPListener + mixedListener *mixed.Listener + mixedUDPLister *socks.UDPListener + tunnelTCPListeners = map[string]*tunnel.Listener{} + tunnelUDPListeners = map[string]*tunnel.PacketConn{} + inboundListeners = map[string]C.InboundListener{} + tunLister *sing_tun.Listener + shadowSocksListener C.MultiAddrListener + vmessListener *sing_vmess.Listener + tuicListener *tuic.Listener + autoRedirListener *autoredir.Listener + autoRedirProgram *ebpf.TcEBpfProgram + tcProgram *ebpf.TcEBpfProgram // lock for recreate function socksMux sync.Mutex @@ -49,26 +59,43 @@ var ( redirMux sync.Mutex tproxyMux sync.Mutex mixedMux sync.Mutex + tunnelMux sync.Mutex + inboundMux sync.Mutex tunMux sync.Mutex + ssMux sync.Mutex + vmessMux sync.Mutex + tuicMux sync.Mutex autoRedirMux sync.Mutex tcMux sync.Mutex + + LastTunConf LC.Tun + LastTuicConf LC.TuicServer ) type Ports struct { - Port int `json:"port"` - SocksPort int `json:"socks-port"` - RedirPort int `json:"redir-port"` - TProxyPort int `json:"tproxy-port"` - MixedPort int `json:"mixed-port"` + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` + ShadowSocksConfig string `json:"ss-config"` + VmessConfig string `json:"vmess-config"` } -func GetTunConf() config.Tun { - if lastTunConf == nil { - return config.Tun{ +func GetTunConf() LC.Tun { + if tunLister == nil { + return LC.Tun{ Enable: false, } } - return *lastTunConf + return tunLister.Config() +} + +func GetTuicConf() LC.TuicServer { + if tuicListener == nil { + return LC.TuicServer{Enable: false} + } + return tuicListener.Config() } func AllowLan() bool { @@ -87,14 +114,6 @@ func SetBindAddress(host string) { bindAddress = host } -func SetInboundTfo(itfo bool) { - inboundTfo = itfo -} - -func NewInner(tcpIn chan<- C.ConnContext) { - inner.New(tcpIn) -} - func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { httpMux.Lock() defer httpMux.Unlock() @@ -120,7 +139,7 @@ func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { return } - httpListener, err = http.New(addr, inboundTfo, tcpIn) + httpListener, err = http.New(addr, tcpIn) if err != nil { log.Errorln("Start HTTP server error: %s", err.Error()) return @@ -129,7 +148,7 @@ func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { log.Infoln("HTTP proxy listening at: %s", httpListener.Address()) } -func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { +func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { socksMux.Lock() defer socksMux.Unlock() @@ -171,7 +190,7 @@ func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P return } - tcpListener, err := socks.New(addr, inboundTfo, tcpIn) + tcpListener, err := socks.New(addr, tcpIn) if err != nil { return } @@ -188,7 +207,7 @@ func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P log.Infoln("SOCKS proxy listening at: %s", socksListener.Address()) } -func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { +func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { redirMux.Lock() defer redirMux.Unlock() @@ -234,7 +253,157 @@ func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P log.Infoln("Redirect proxy listening at: %s", redirListener.Address()) } -func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { +func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { + ssMux.Lock() + defer ssMux.Unlock() + + var err error + defer func() { + if err != nil { + log.Errorln("Start ShadowSocks server error: %s", err.Error()) + } + }() + + var ssConfig LC.ShadowsocksServer + if addr, cipher, password, err := embedSS.ParseSSURL(shadowSocksConfig); err == nil { + ssConfig = LC.ShadowsocksServer{ + Enable: len(shadowSocksConfig) > 0, + Listen: addr, + Password: password, + Cipher: cipher, + } + } + + shouldIgnore := false + + if shadowSocksListener != nil { + if shadowSocksListener.Config() != ssConfig.String() { + shadowSocksListener.Close() + shadowSocksListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + if !ssConfig.Enable { + return + } + + listener, err := sing_shadowsocks.New(ssConfig, tcpIn, udpIn) + if err != nil { + return + } + + shadowSocksListener = listener + + for _, addr := range shadowSocksListener.AddrList() { + log.Infoln("ShadowSocks proxy listening at: %s", addr.String()) + } + return +} + +func ReCreateVmess(vmessConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { + vmessMux.Lock() + defer vmessMux.Unlock() + + var err error + defer func() { + if err != nil { + log.Errorln("Start Vmess server error: %s", err.Error()) + } + }() + + var vsConfig LC.VmessServer + if addr, username, password, err := sing_vmess.ParseVmessURL(vmessConfig); err == nil { + vsConfig = LC.VmessServer{ + Enable: len(vmessConfig) > 0, + Listen: addr, + Users: []LC.VmessUser{{Username: username, UUID: password, AlterID: 1}}, + } + } + + shouldIgnore := false + + if vmessListener != nil { + if vmessListener.Config() != vsConfig.String() { + vmessListener.Close() + vmessListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + if !vsConfig.Enable { + return + } + + listener, err := sing_vmess.New(vsConfig, tcpIn, udpIn) + if err != nil { + return + } + + vmessListener = listener + + for _, addr := range vmessListener.AddrList() { + log.Infoln("Vmess proxy listening at: %s", addr.String()) + } + return +} + +func ReCreateTuic(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { + tuicMux.Lock() + defer func() { + LastTuicConf = config + tuicMux.Unlock() + }() + shouldIgnore := false + + var err error + defer func() { + if err != nil { + log.Errorln("Start Tuic server error: %s", err.Error()) + } + }() + + if tuicListener != nil { + if tuicListener.Config().String() != config.String() { + tuicListener.Close() + tuicListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + if !config.Enable { + return + } + + listener, err := tuic.New(config, tcpIn, udpIn) + if err != nil { + return + } + + tuicListener = listener + + for _, addr := range tuicListener.AddrList() { + log.Infoln("Tuic proxy listening at: %s", addr.String()) + } + return +} + +func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { tproxyMux.Lock() defer tproxyMux.Unlock() @@ -280,7 +449,7 @@ func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound. log.Infoln("TProxy server listening at: %s", tproxyListener.Address()) } -func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { +func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { mixedMux.Lock() defer mixedMux.Unlock() @@ -321,7 +490,7 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P return } - mixedListener, err = mixed.New(addr, inboundTfo, tcpIn) + mixedListener, err = mixed.New(addr, tcpIn) if err != nil { return } @@ -335,9 +504,12 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) } -func ReCreateTun(tunConf *config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { +func ReCreateTun(tunConf LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { tunMux.Lock() - defer tunMux.Unlock() + defer func() { + LastTunConf = tunConf + tunMux.Unlock() + }() var err error defer func() { @@ -347,7 +519,7 @@ func ReCreateTun(tunConf *config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- * } }() - if !hasTunConfigChange(tunConf) { + if !hasTunConfigChange(&tunConf) { if tunLister != nil { tunLister.FlushDefaultInterface() } @@ -360,9 +532,13 @@ func ReCreateTun(tunConf *config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- * return } - tunLister, err = sing_tun.New(*tunConf, tcpIn, udpIn) + lister, err := sing_tun.New(tunConf, tcpIn, udpIn) + if err != nil { + return + } + tunLister = lister - lastTunConf = tunConf + log.Infoln("[TUN] Tun adapter listening at: %s", tunLister.Address()) } func ReCreateRedirToTun(ifaceNames []string) { @@ -382,11 +558,13 @@ func ReCreateRedirToTun(ifaceNames []string) { return } - if lastTunConf == nil || !lastTunConf.Enable { + tunConf := GetTunConf() + + if !tunConf.Enable { return } - program, err := ebpf.NewTcEBpfProgram(nicArr, lastTunConf.Device) + program, err := ebpf.NewTcEBpfProgram(nicArr, tunConf.Device) if err != nil { log.Errorln("Attached tc ebpf program error: %v", err) return @@ -396,7 +574,7 @@ func ReCreateRedirToTun(ifaceNames []string) { log.Infoln("Attached tc ebpf program to interfaces %v", tcProgram.RawNICs()) } -func ReCreateAutoRedir(ifaceNames []string, tcpIn chan<- C.ConnContext, _ chan<- *inbound.PacketAdapter) { +func ReCreateAutoRedir(ifaceNames []string, tcpIn chan<- C.ConnContext, _ chan<- C.PacketAdapter) { autoRedirMux.Lock() defer autoRedirMux.Unlock() @@ -452,6 +630,124 @@ func ReCreateAutoRedir(ifaceNames []string, tcpIn chan<- C.ConnContext, _ chan<- log.Infoln("Auto redirect proxy listening at: %s, attached tc ebpf program to interfaces %v", autoRedirListener.Address(), autoRedirProgram.RawNICs()) } +func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { + tunnelMux.Lock() + defer tunnelMux.Unlock() + + type addrProxy struct { + network string + addr string + target string + proxy string + } + + tcpOld := lo.Map( + lo.Keys(tunnelTCPListeners), + func(key string, _ int) addrProxy { + parts := strings.Split(key, "/") + return addrProxy{ + network: "tcp", + addr: parts[0], + target: parts[1], + proxy: parts[2], + } + }, + ) + udpOld := lo.Map( + lo.Keys(tunnelUDPListeners), + func(key string, _ int) addrProxy { + parts := strings.Split(key, "/") + return addrProxy{ + network: "udp", + addr: parts[0], + target: parts[1], + proxy: parts[2], + } + }, + ) + oldElm := lo.Union(tcpOld, udpOld) + + newElm := lo.FlatMap( + tunnels, + func(tunnel LC.Tunnel, _ int) []addrProxy { + return lo.Map( + tunnel.Network, + func(network string, _ int) addrProxy { + return addrProxy{ + network: network, + addr: tunnel.Address, + target: tunnel.Target, + proxy: tunnel.Proxy, + } + }, + ) + }, + ) + + needClose, needCreate := lo.Difference(oldElm, newElm) + + for _, elm := range needClose { + key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) + if elm.network == "tcp" { + tunnelTCPListeners[key].Close() + delete(tunnelTCPListeners, key) + } else { + tunnelUDPListeners[key].Close() + delete(tunnelUDPListeners, key) + } + } + + for _, elm := range needCreate { + key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) + if elm.network == "tcp" { + l, err := tunnel.New(elm.addr, elm.target, elm.proxy, tcpIn) + if err != nil { + log.Errorln("Start tunnel %s error: %s", elm.target, err.Error()) + continue + } + tunnelTCPListeners[key] = l + log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address()) + } else { + l, err := tunnel.NewUDP(elm.addr, elm.target, elm.proxy, udpIn) + if err != nil { + log.Errorln("Start tunnel %s error: %s", elm.target, err.Error()) + continue + } + tunnelUDPListeners[key] = l + log.Infoln("Tunnel(udp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelUDPListeners[key].Address()) + } + } +} + +func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, dropOld bool) { + inboundMux.Lock() + defer inboundMux.Unlock() + + for name, newListener := range newListenerMap { + if oldListener, ok := inboundListeners[name]; ok { + if !oldListener.Config().Equal(newListener.Config()) { + _ = oldListener.Close() + } else { + continue + } + } + if err := newListener.Listen(tcpIn, udpIn); err != nil { + log.Errorln("Listener %s listen err: %s", name, err.Error()) + continue + } + inboundListeners[name] = newListener + } + + if dropOld { + for name, oldListener := range inboundListeners { + if _, ok := newListenerMap[name]; !ok { + _ = oldListener.Close() + delete(inboundListeners, name) + } + } + } +} + // GetPorts return the ports of proxy servers func GetPorts() *Ports { ports := &Ports{} @@ -486,6 +782,14 @@ func GetPorts() *Ports { ports.MixedPort = port } + if shadowSocksListener != nil { + ports.ShadowSocksConfig = shadowSocksListener.Config() + } + + if vmessListener != nil { + ports.VmessConfig = vmessListener.Config() + } + return ports } @@ -508,38 +812,83 @@ func genAddr(host string, port int, allowLan bool) string { return fmt.Sprintf("127.0.0.1:%d", port) } -func hasTunConfigChange(tunConf *config.Tun) bool { - if lastTunConf == nil { +func hasTunConfigChange(tunConf *LC.Tun) bool { + if LastTunConf.Enable != tunConf.Enable || + LastTunConf.Device != tunConf.Device || + LastTunConf.Stack != tunConf.Stack || + LastTunConf.AutoRoute != tunConf.AutoRoute || + LastTunConf.AutoDetectInterface != tunConf.AutoDetectInterface || + LastTunConf.MTU != tunConf.MTU || + LastTunConf.StrictRoute != tunConf.StrictRoute || + LastTunConf.EndpointIndependentNat != tunConf.EndpointIndependentNat || + LastTunConf.UDPTimeout != tunConf.UDPTimeout { return true } - if len(lastTunConf.DNSHijack) != len(tunConf.DNSHijack) { + if len(LastTunConf.DNSHijack) != len(tunConf.DNSHijack) { return true } - sort.Slice(lastTunConf.DNSHijack, func(i, j int) bool { - return lastTunConf.DNSHijack[i].Addr().Less(lastTunConf.DNSHijack[j].Addr()) + sort.Slice(tunConf.DNSHijack, func(i, j int) bool { + return tunConf.DNSHijack[i] < tunConf.DNSHijack[j] }) - sort.Slice(tunConf.DNSHijack, func(i, j int) bool { - return tunConf.DNSHijack[i].Addr().Less(tunConf.DNSHijack[j].Addr()) + sort.Slice(tunConf.Inet4Address, func(i, j int) bool { + return tunConf.Inet4Address[i].Build().String() < tunConf.Inet4Address[j].Build().String() }) - for i, dns := range tunConf.DNSHijack { - if dns != lastTunConf.DNSHijack[i] { - return true - } - } + sort.Slice(tunConf.Inet6Address, func(i, j int) bool { + return tunConf.Inet6Address[i].Build().String() < tunConf.Inet6Address[j].Build().String() + }) - if lastTunConf.Enable != tunConf.Enable || - lastTunConf.Device != tunConf.Device || - lastTunConf.Stack != tunConf.Stack || - lastTunConf.AutoRoute != tunConf.AutoRoute || - lastTunConf.AutoDetectInterface != tunConf.AutoDetectInterface { - return true - } + sort.Slice(tunConf.Inet4RouteAddress, func(i, j int) bool { + return tunConf.Inet4RouteAddress[i].Build().String() < tunConf.Inet4RouteAddress[j].Build().String() + }) + + sort.Slice(tunConf.Inet6RouteAddress, func(i, j int) bool { + return tunConf.Inet6RouteAddress[i].Build().String() < tunConf.Inet6RouteAddress[j].Build().String() + }) + + sort.Slice(tunConf.IncludeUID, func(i, j int) bool { + return tunConf.IncludeUID[i] < tunConf.IncludeUID[j] + }) + + sort.Slice(tunConf.IncludeUIDRange, func(i, j int) bool { + return tunConf.IncludeUIDRange[i] < tunConf.IncludeUIDRange[j] + }) + + sort.Slice(tunConf.ExcludeUID, func(i, j int) bool { + return tunConf.ExcludeUID[i] < tunConf.ExcludeUID[j] + }) + + sort.Slice(tunConf.ExcludeUIDRange, func(i, j int) bool { + return tunConf.ExcludeUIDRange[i] < tunConf.ExcludeUIDRange[j] + }) + + sort.Slice(tunConf.IncludeAndroidUser, func(i, j int) bool { + return tunConf.IncludeAndroidUser[i] < tunConf.IncludeAndroidUser[j] + }) + + sort.Slice(tunConf.IncludePackage, func(i, j int) bool { + return tunConf.IncludePackage[i] < tunConf.IncludePackage[j] + }) + + sort.Slice(tunConf.ExcludePackage, func(i, j int) bool { + return tunConf.ExcludePackage[i] < tunConf.ExcludePackage[j] + }) - if slices.Equal(tunConf.Inet4Address, lastTunConf.Inet4Address) && slices.Equal(tunConf.Inet6Address, lastTunConf.Inet6Address) { + if !slices.Equal(tunConf.DNSHijack, LastTunConf.DNSHijack) || + !slices.Equal(tunConf.Inet4Address, LastTunConf.Inet4Address) || + !slices.Equal(tunConf.Inet6Address, LastTunConf.Inet6Address) || + !slices.Equal(tunConf.Inet4RouteAddress, LastTunConf.Inet4RouteAddress) || + !slices.Equal(tunConf.Inet6RouteAddress, LastTunConf.Inet6RouteAddress) || + !slices.Equal(tunConf.IncludeUID, LastTunConf.IncludeUID) || + !slices.Equal(tunConf.IncludeUIDRange, LastTunConf.IncludeUIDRange) || + !slices.Equal(tunConf.ExcludeUID, LastTunConf.ExcludeUID) || + !slices.Equal(tunConf.ExcludeUIDRange, LastTunConf.ExcludeUIDRange) || + !slices.Equal(tunConf.IncludeAndroidUser, LastTunConf.IncludeAndroidUser) || + !slices.Equal(tunConf.IncludePackage, LastTunConf.IncludePackage) || + !slices.Equal(tunConf.ExcludePackage, LastTunConf.ExcludePackage) { return true } @@ -551,5 +900,5 @@ func Cleanup(wait bool) { tunLister.Close() tunLister = nil } - lastTunConf = nil + LastTunConf = LC.Tun{} } diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index feaf73aad7..e838587397 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -1,10 +1,8 @@ package mixed import ( - "context" - "github.com/database64128/tfo-go" + "github.com/Dreamacro/clash/adapter/inbound" "net" - "time" "github.com/Dreamacro/clash/common/cache" N "github.com/Dreamacro/clash/common/net" @@ -18,7 +16,7 @@ import ( type Listener struct { listener net.Listener addr string - cache *cache.Cache[string, bool] + cache *cache.LruCache[string, bool] closed bool } @@ -38,11 +36,14 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, inboundTfo bool, in chan<- C.ConnContext) (*Listener, error) { - lc := tfo.ListenConfig{ - DisableTFO: !inboundTfo, +func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-MIXED"), + inbound.WithSpecialRules(""), + } } - l, err := lc.Listen(context.Background(), "tcp", addr) + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } @@ -50,7 +51,7 @@ func New(addr string, inboundTfo bool, in chan<- C.ConnContext) (*Listener, erro ml := &Listener{ listener: l, addr: addr, - cache: cache.New[string, bool](30 * time.Second), + cache: cache.New[string, bool](cache.WithAge[string, bool](30)), } go func() { for { @@ -61,14 +62,14 @@ func New(addr string, inboundTfo bool, in chan<- C.ConnContext) (*Listener, erro } continue } - go handleConn(c, in, ml.cache) + go handleConn(c, in, ml.cache, additions...) } }() return ml, nil } -func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) { +func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[string, bool], additions ...inbound.Addition) { conn.(*net.TCPConn).SetKeepAlive(true) bufConn := N.NewBufferedConn(conn) @@ -79,10 +80,10 @@ func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache[strin switch head[0] { case socks4.Version: - socks.HandleSocks4(bufConn, in) + socks.HandleSocks4(bufConn, in, additions...) case socks5.Version: - socks.HandleSocks5(bufConn, in) + socks.HandleSocks5(bufConn, in, additions...) default: - http.HandleConn(bufConn, in, cache) + http.HandleConn(bufConn, in, cache, additions...) } } diff --git a/listener/parse.go b/listener/parse.go new file mode 100644 index 0000000000..9459b9e1ba --- /dev/null +++ b/listener/parse.go @@ -0,0 +1,105 @@ +package listener + +import ( + "fmt" + + "github.com/Dreamacro/clash/common/structure" + C "github.com/Dreamacro/clash/constant" + IN "github.com/Dreamacro/clash/listener/inbound" +) + +func ParseListener(mapping map[string]any) (C.InboundListener, error) { + decoder := structure.NewDecoder(structure.Option{TagName: "inbound", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) + proxyType, existType := mapping["type"].(string) + if !existType { + return nil, fmt.Errorf("missing type") + } + + var ( + listener C.InboundListener + err error + ) + switch proxyType { + case "socks": + socksOption := &IN.SocksOption{UDP: true} + err = decoder.Decode(mapping, socksOption) + if err != nil { + return nil, err + } + listener, err = IN.NewSocks(socksOption) + case "http": + httpOption := &IN.HTTPOption{} + err = decoder.Decode(mapping, httpOption) + if err != nil { + return nil, err + } + listener, err = IN.NewHTTP(httpOption) + case "tproxy": + tproxyOption := &IN.TProxyOption{UDP: true} + err = decoder.Decode(mapping, tproxyOption) + if err != nil { + return nil, err + } + listener, err = IN.NewTProxy(tproxyOption) + case "redir": + redirOption := &IN.RedirOption{} + err = decoder.Decode(mapping, redirOption) + if err != nil { + return nil, err + } + listener, err = IN.NewRedir(redirOption) + case "mixed": + mixedOption := &IN.MixedOption{UDP: true} + err = decoder.Decode(mapping, mixedOption) + if err != nil { + return nil, err + } + listener, err = IN.NewMixed(mixedOption) + case "tunnel": + tunnelOption := &IN.TunnelOption{} + err = decoder.Decode(mapping, tunnelOption) + if err != nil { + return nil, err + } + listener, err = IN.NewTunnel(tunnelOption) + case "tun": + tunOption := &IN.TunOption{ + Stack: C.TunGvisor.String(), + DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query + } + err = decoder.Decode(mapping, tunOption) + if err != nil { + return nil, err + } + listener, err = IN.NewTun(tunOption) + case "shadowsocks": + shadowsocksOption := &IN.ShadowSocksOption{} + err = decoder.Decode(mapping, shadowsocksOption) + if err != nil { + return nil, err + } + listener, err = IN.NewShadowSocks(shadowsocksOption) + case "vmess": + vmessOption := &IN.VmessOption{} + err = decoder.Decode(mapping, vmessOption) + if err != nil { + return nil, err + } + listener, err = IN.NewVmess(vmessOption) + case "tuic": + tuicOption := &IN.TuicOption{ + MaxIdleTime: 15000, + AuthenticationTimeout: 1000, + ALPN: []string{"h3"}, + MaxUdpRelayPacketSize: 1500, + } + err = decoder.Decode(mapping, tuicOption) + if err != nil { + return nil, err + } + listener, err = IN.NewTuic(tuicOption) + default: + return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) + } + return listener, err +} diff --git a/listener/redir/tcp.go b/listener/redir/tcp.go index 15c98a8f3a..ad4a91bc06 100644 --- a/listener/redir/tcp.go +++ b/listener/redir/tcp.go @@ -29,7 +29,13 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, in chan<- C.ConnContext) (*Listener, error) { +func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-REDIR"), + inbound.WithSpecialRules(""), + } + } l, err := net.Listen("tcp", addr) if err != nil { return nil, err @@ -48,19 +54,18 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) { } continue } - go handleRedir(c, in) + go handleRedir(c, in, additions...) } }() return rl, nil } - -func handleRedir(conn net.Conn, in chan<- C.ConnContext) { +func handleRedir(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { target, err := parserPacket(conn) if err != nil { conn.Close() return } conn.(*net.TCPConn).SetKeepAlive(true) - in <- inbound.NewSocket(target, conn, C.REDIR) + in <- inbound.NewSocket(target, conn, C.REDIR, additions...) } diff --git a/listener/redir/tcp_linux.go b/listener/redir/tcp_linux.go index c4a47d8e9b..b65c34ee4f 100644 --- a/listener/redir/tcp_linux.go +++ b/listener/redir/tcp_linux.go @@ -1,12 +1,16 @@ package redir import ( + "encoding/binary" "errors" "net" + "net/netip" "syscall" "unsafe" "github.com/Dreamacro/clash/transport/socks5" + + "golang.org/x/sys/unix" ) const ( @@ -25,27 +29,36 @@ func parserPacket(conn net.Conn) (socks5.Addr, error) { return nil, err } - var addr socks5.Addr + var addr netip.AddrPort rc.Control(func(fd uintptr) { - addr, err = getorigdst(fd) + if ip4 := c.LocalAddr().(*net.TCPAddr).IP.To4(); ip4 != nil { + addr, err = getorigdst(fd) + } else { + addr, err = getorigdst6(fd) + } }) - return addr, err + return socks5.AddrFromStdAddrPort(addr), err } // Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c -func getorigdst(fd uintptr) (socks5.Addr, error) { - raw := syscall.RawSockaddrInet4{} - siz := uint32(unsafe.Sizeof(raw)) - if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil { - return nil, err +func getorigdst(fd uintptr) (netip.AddrPort, error) { + addr := unix.RawSockaddrInet4{} + size := uint32(unsafe.Sizeof(addr)) + if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0); err != nil { + return netip.AddrPort{}, err } + port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) + return netip.AddrPortFrom(netip.AddrFrom4(addr.Addr), port), nil +} - addr := make([]byte, 1+net.IPv4len+2) - addr[0] = socks5.AtypIPv4 - copy(addr[1:1+net.IPv4len], raw.Addr[:]) - port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian - addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1] - return addr, nil +func getorigdst6(fd uintptr) (netip.AddrPort, error) { + addr := unix.RawSockaddrInet6{} + size := uint32(unsafe.Sizeof(addr)) + if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0); err != nil { + return netip.AddrPort{}, err + } + port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) + return netip.AddrPortFrom(netip.AddrFrom16(addr.Addr), port), nil } diff --git a/listener/shadowsocks/tcp.go b/listener/shadowsocks/tcp.go new file mode 100644 index 0000000000..21db5b6330 --- /dev/null +++ b/listener/shadowsocks/tcp.go @@ -0,0 +1,116 @@ +package shadowsocks + +import ( + "net" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/transport/shadowsocks/core" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + closed bool + config LC.ShadowsocksServer + listeners []net.Listener + udpListeners []*UDPListener + pickCipher core.Cipher +} + +var _listener *Listener + +func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) (*Listener, error) { + pickCipher, err := core.PickCipher(config.Cipher, nil, config.Password) + if err != nil { + return nil, err + } + + sl := &Listener{false, config, nil, nil, pickCipher} + _listener = sl + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + + //UDP + ul, err := NewUDP(addr, pickCipher, udpIn) + if err != nil { + return nil, err + } + sl.udpListeners = append(sl.udpListeners, ul) + + //TCP + l, err := inbound.Listen("tcp", addr) + if err != nil { + return nil, err + } + sl.listeners = append(sl.listeners, l) + + go func() { + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + _ = c.(*net.TCPConn).SetKeepAlive(true) + go sl.HandleConn(c, tcpIn) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + var retErr error + for _, lis := range l.listeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + for _, lis := range l.udpListeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + return retErr +} + +func (l *Listener) Config() string { + return l.config.String() +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.listeners { + addrList = append(addrList, lis.Addr()) + } + for _, lis := range l.udpListeners { + addrList = append(addrList, lis.LocalAddr()) + } + return +} + +func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { + conn = l.pickCipher.StreamConn(conn) + + target, err := socks5.ReadAddr(conn, make([]byte, socks5.MaxAddrLen)) + if err != nil { + _ = conn.Close() + return + } + in <- inbound.NewSocket(target, conn, C.SHADOWSOCKS, additions...) +} + +func HandleShadowSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) bool { + if _listener != nil && _listener.pickCipher != nil { + go _listener.HandleConn(conn, in, additions...) + return true + } + return false +} diff --git a/listener/shadowsocks/udp.go b/listener/shadowsocks/udp.go new file mode 100644 index 0000000000..3f05840651 --- /dev/null +++ b/listener/shadowsocks/udp.go @@ -0,0 +1,80 @@ +package shadowsocks + +import ( + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/common/sockopt" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/shadowsocks/core" + "github.com/Dreamacro/clash/transport/socks5" +) + +type UDPListener struct { + packetConn net.PacketConn + closed bool +} + +func NewUDP(addr string, pickCipher core.Cipher, in chan<- C.PacketAdapter) (*UDPListener, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + err = sockopt.UDPReuseaddr(l.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl := &UDPListener{l, false} + conn := pickCipher.PacketConn(l) + go func() { + for { + buf := pool.Get(pool.RelayBufferSize) + n, remoteAddr, err := conn.ReadFrom(buf) + if err != nil { + pool.Put(buf) + if sl.closed { + break + } + continue + } + handleSocksUDP(conn, in, buf[:n], remoteAddr) + } + }() + + return sl, nil +} + +func (l *UDPListener) Close() error { + l.closed = true + return l.packetConn.Close() +} + +func (l *UDPListener) LocalAddr() net.Addr { + return l.packetConn.LocalAddr() +} + +func handleSocksUDP(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, addr net.Addr) { + tgtAddr := socks5.SplitAddr(buf) + if tgtAddr == nil { + // Unresolved UDP packet, return buffer to the pool + pool.Put(buf) + return + } + target := socks5.ParseAddr(tgtAddr.String()) + payload := buf[len(tgtAddr):] + + packet := &packet{ + pc: pc, + rAddr: addr, + payload: payload, + bufRef: buf, + } + select { + case in <- inbound.NewPacket(target, packet, C.SHADOWSOCKS): + default: + } +} diff --git a/listener/shadowsocks/utils.go b/listener/shadowsocks/utils.go new file mode 100644 index 0000000000..2e9fd00339 --- /dev/null +++ b/listener/shadowsocks/utils.go @@ -0,0 +1,59 @@ +package shadowsocks + +import ( + "bytes" + "errors" + "net" + "net/url" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/socks5" +) + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte + bufRef []byte +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack wirtes UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + if addr == nil { + err = errors.New("address is invalid") + return + } + packet := bytes.Join([][]byte{socks5.ParseAddrToSocksAddr(addr), b}, []byte{}) + return c.pc.WriteTo(packet, c.rAddr) +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *packet) Drop() { + pool.Put(c.bufRef) +} + +func (c *packet) InAddr() net.Addr { + return c.pc.LocalAddr() +} + +func ParseSSURL(s string) (addr, cipher, password string, err error) { + u, err := url.Parse(s) + if err != nil { + return + } + + addr = u.Host + if u.User != nil { + cipher = u.User.Username() + password, _ = u.User.Password() + } + return +} diff --git a/listener/sing/context.go b/listener/sing/context.go new file mode 100644 index 0000000000..f7aed85191 --- /dev/null +++ b/listener/sing/context.go @@ -0,0 +1,24 @@ +package sing + +import ( + "context" + + "github.com/Dreamacro/clash/adapter/inbound" +) + +type contextKey string + +var ctxKeyAdditions = contextKey("Additions") + +func WithAdditions(ctx context.Context, additions ...inbound.Addition) context.Context { + return context.WithValue(ctx, ctxKeyAdditions, additions) +} + +func getAdditions(ctx context.Context) []inbound.Addition { + if v := ctx.Value(ctxKeyAdditions); v != nil { + if a, ok := v.([]inbound.Addition); ok { + return a + } + } + return nil +} diff --git a/listener/sing/sing.go b/listener/sing/sing.go index df55f1000b..e8aafa3919 100644 --- a/listener/sing/sing.go +++ b/listener/sing/sing.go @@ -3,6 +3,7 @@ package sing import ( "context" "errors" + "golang.org/x/exp/slices" "net" "sync" "time" @@ -23,9 +24,10 @@ import ( const UDPTimeout = 5 * time.Minute type ListenerHandler struct { - TcpIn chan<- C.ConnContext - UdpIn chan<- *inbound.PacketAdapter - Type C.Type + TcpIn chan<- C.ConnContext + UdpIn chan<- C.PacketAdapter + Type C.Type + Additions []inbound.Addition } type waitCloseConn struct { @@ -47,6 +49,11 @@ func (c *waitCloseConn) RemoteAddr() net.Addr { } func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + additions := h.Additions + if ctxAdditions := getAdditions(ctx); len(ctxAdditions) > 0 { + additions = slices.Clone(additions) + additions = append(additions, ctxAdditions...) + } switch metadata.Destination.Fqdn { case vmess.MuxDestination.Fqdn: return vmess.HandleMuxConnection(ctx, conn, h) @@ -58,11 +65,17 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta wg := &sync.WaitGroup{} defer wg.Wait() // this goroutine must exit after conn.Close() wg.Add(1) - h.TcpIn <- inbound.NewSocket(target, &waitCloseConn{Conn: conn, wg: wg, rAddr: metadata.Source.TCPAddr()}, h.Type) + + h.TcpIn <- inbound.NewSocket(target, &waitCloseConn{Conn: conn, wg: wg, rAddr: metadata.Source.TCPAddr()}, h.Type, additions...) return nil } func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { + additions := h.Additions + if ctxAdditions := getAdditions(ctx); len(ctxAdditions) > 0 { + additions = slices.Clone(additions) + additions = append(additions, ctxAdditions...) + } defer func() { _ = conn.Close() }() mutex := sync.Mutex{} conn2 := conn // a new interface to set nil in defer @@ -90,7 +103,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. buff: buff, } select { - case h.UdpIn <- inbound.NewPacket(target, packet, h.Type): + case h.UdpIn <- inbound.NewPacket(target, packet, h.Type, additions...): default: } } diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go new file mode 100644 index 0000000000..305a9496cf --- /dev/null +++ b/listener/sing_shadowsocks/server.go @@ -0,0 +1,186 @@ +package sing_shadowsocks + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/sockopt" + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + embedSS "github.com/Dreamacro/clash/listener/shadowsocks" + "github.com/Dreamacro/clash/listener/sing" + "github.com/Dreamacro/clash/log" + + shadowsocks "github.com/metacubex/sing-shadowsocks" + "github.com/metacubex/sing-shadowsocks/shadowaead" + "github.com/metacubex/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/metadata" +) + +type Listener struct { + closed bool + config LC.ShadowsocksServer + listeners []net.Listener + udpListeners []net.PacketConn + service shadowsocks.Service +} + +var _listener *Listener + +func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (C.MultiAddrListener, error) { + var sl *Listener + var err error + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-SHADOWSOCKS"), + inbound.WithSpecialRules(""), + } + defer func() { + _listener = sl + }() + } + + udpTimeout := int64(sing.UDPTimeout.Seconds()) + + h := &sing.ListenerHandler{ + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.SHADOWSOCKS, + Additions: additions, + } + + sl = &Listener{false, config, nil, nil, nil} + + switch { + case config.Cipher == shadowsocks.MethodNone: + sl.service = shadowsocks.NewNoneService(udpTimeout, h) + case common.Contains(shadowaead.List, config.Cipher): + sl.service, err = shadowaead.NewService(config.Cipher, nil, config.Password, udpTimeout, h) + case common.Contains(shadowaead_2022.List, config.Cipher): + sl.service, err = shadowaead_2022.NewServiceWithPassword(config.Cipher, config.Password, udpTimeout, h) + default: + err = fmt.Errorf("shadowsocks: unsupported method: %s", config.Cipher) + return embedSS.New(config, tcpIn, udpIn) + } + if err != nil { + return nil, err + } + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + + //UDP + ul, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl.udpListeners = append(sl.udpListeners, ul) + + go func() { + conn := bufio.NewPacketConn(ul) + for { + buff := buf.NewPacket() + remoteAddr, err := conn.ReadPacket(buff) + if err != nil { + buff.Release() + if sl.closed { + break + } + continue + } + _ = sl.service.NewPacket(context.TODO(), conn, buff, metadata.Metadata{ + Protocol: "shadowsocks", + Source: remoteAddr, + }) + } + }() + + //TCP + l, err := inbound.Listen("tcp", addr) + if err != nil { + return nil, err + } + sl.listeners = append(sl.listeners, l) + + go func() { + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + _ = c.(*net.TCPConn).SetKeepAlive(true) + + go sl.HandleConn(c, tcpIn) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + l.closed = true + var retErr error + for _, lis := range l.listeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + for _, lis := range l.udpListeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + return retErr +} + +func (l *Listener) Config() string { + return l.config.String() +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.listeners { + addrList = append(addrList, lis.Addr()) + } + for _, lis := range l.udpListeners { + addrList = append(addrList, lis.LocalAddr()) + } + return +} + +func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { + ctx := sing.WithAdditions(context.TODO(), additions...) + err := l.service.NewConnection(ctx, conn, metadata.Metadata{ + Protocol: "shadowsocks", + Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), + }) + if err != nil { + _ = conn.Close() + return + } +} + +func HandleShadowSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) bool { + if _listener != nil && _listener.service != nil { + go _listener.HandleConn(conn, in, additions...) + return true + } + return embedSS.HandleShadowSocks(conn, in, additions...) +} diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 39e2b1e5a2..21dee43c6a 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -23,6 +23,7 @@ import ( ) const DefaultDnsReadTimeout = time.Second * 10 +const DefaultDnsRelayTimeout = time.Second * 5 type ListenerHandler struct { sing.ListenerHandler @@ -69,8 +70,10 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta } err = func() error { + ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) + defer cancel() inData := buff[:n] - msg, err := RelayDnsPacket(inData) + msg, err := RelayDnsPacket(ctx, inData) if err != nil { return err } @@ -117,8 +120,10 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. return err } go func() { + ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) + defer cancel() inData := buff.Bytes() - msg, err := RelayDnsPacket(inData) + msg, err := RelayDnsPacket(ctx, inData) if err != nil { buff.Release() return @@ -146,13 +151,13 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata) } -func RelayDnsPacket(payload []byte) ([]byte, error) { +func RelayDnsPacket(ctx context.Context, payload []byte) ([]byte, error) { msg := &D.Msg{} if err := msg.Unpack(payload); err != nil { return nil, err } - r, err := resolver.ServeMsg(msg) + r, err := resolver.ServeMsg(ctx, msg) if err != nil { m := new(D.Msg) m.SetRcode(msg, D.RcodeServerFailure) diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index 44824b75ba..5c387a8d30 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -2,6 +2,7 @@ package sing_tun import ( "context" + "fmt" "net" "net/netip" "runtime" @@ -11,12 +12,12 @@ import ( "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/iface" - "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" "github.com/Dreamacro/clash/listener/sing" "github.com/Dreamacro/clash/log" - tun "github.com/sagernet/sing-tun" + tun "github.com/metacubex/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -27,9 +28,10 @@ var InterfaceName = "Meta" type Listener struct { closed bool - options config.Tun + options LC.Tun handler *ListenerHandler tunName string + addrStr string tunIf tun.Tun tunStack tun.Stack @@ -65,10 +67,17 @@ func CalculateInterfaceName(name string) (tunName string) { return } -func New(options config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (l *Listener, err error) { +func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (l *Listener, err error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-TUN"), + inbound.WithSpecialRules(""), + } + } tunName := options.Device if tunName == "" { tunName = CalculateInterfaceName(InterfaceName) + options.Device = tunName } tunMTU := options.MTU if tunMTU == 0 { @@ -100,7 +109,16 @@ func New(options config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P var dnsAdds []netip.AddrPort for _, d := range options.DNSHijack { - dnsAdds = append(dnsAdds, d) + if _, after, ok := strings.Cut(d, "://"); ok { + d = after + } + d = strings.Replace(d, "any", "0.0.0.0", 1) + addrPort, err := netip.ParseAddrPort(d) + if err != nil { + return nil, fmt.Errorf("parse dns-hijack url error: %w", err) + } + + dnsAdds = append(dnsAdds, addrPort) } for _, a := range options.Inet4Address { addrPort := netip.AddrPortFrom(a.Build().Addr().Next(), 53) @@ -113,9 +131,10 @@ func New(options config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P handler := &ListenerHandler{ ListenerHandler: sing.ListenerHandler{ - TcpIn: tcpIn, - UdpIn: udpIn, - Type: C.TUN, + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.TUN, + Additions: additions, }, DnsAdds: dnsAdds, } @@ -162,12 +181,12 @@ func New(options config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P tunOptions := tun.Options{ Name: tunName, MTU: tunMTU, - Inet4Address: common.Map(options.Inet4Address, config.ListenPrefix.Build), - Inet6Address: common.Map(options.Inet6Address, config.ListenPrefix.Build), + Inet4Address: common.Map(options.Inet4Address, LC.ListenPrefix.Build), + Inet6Address: common.Map(options.Inet6Address, LC.ListenPrefix.Build), AutoRoute: options.AutoRoute, StrictRoute: options.StrictRoute, - Inet4RouteAddress: common.Map(options.Inet4RouteAddress, config.ListenPrefix.Build), - Inet6RouteAddress: common.Map(options.Inet6RouteAddress, config.ListenPrefix.Build), + Inet4RouteAddress: common.Map(options.Inet4RouteAddress, LC.ListenPrefix.Build), + Inet6RouteAddress: common.Map(options.Inet6RouteAddress, LC.ListenPrefix.Build), IncludeUID: includeUID, ExcludeUID: excludeUID, IncludeAndroidUser: options.IncludeAndroidUser, @@ -203,11 +222,15 @@ func New(options config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P if err != nil { return } + err = l.tunStack.Start() if err != nil { return } - log.Infoln("Tun adapter listening at: %s(%s,%s), mtu: %d, auto route: %v, ip stack: %s", + + //l.openAndroidHotspot(tunOptions) + + l.addrStr = fmt.Sprintf("%s(%s,%s), mtu: %d, auto route: %v, ip stack: %s", tunName, tunOptions.Inet4Address, tunOptions.Inet6Address, tunMTU, options.AutoRoute, options.Stack) return } @@ -218,9 +241,9 @@ func (l *Listener) FlushDefaultInterface() { for _, destination := range []netip.Addr{netip.IPv4Unspecified(), netip.IPv6Unspecified(), netip.MustParseAddr("1.1.1.1")} { autoDetectInterfaceName := l.defaultInterfaceMonitor.DefaultInterfaceName(destination) if autoDetectInterfaceName == l.tunName { - log.Warnln("Auto detect interface by %s get same name with tun", destination.String()) + log.Warnln("[TUN] Auto detect interface by %s get same name with tun", destination.String()) } else if autoDetectInterfaceName == "" || autoDetectInterfaceName == "" { - log.Warnln("Auto detect interface by %s get empty name.", destination.String()) + log.Warnln("[TUN] Auto detect interface by %s get empty name.", destination.String()) } else { targetInterface = autoDetectInterfaceName if old := dialer.DefaultInterface.Load(); old != targetInterface { @@ -268,9 +291,9 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. return uidRanges, nil } -func (l *Listener) Close() { +func (l *Listener) Close() error { l.closed = true - _ = common.Close( + return common.Close( l.tunStack, l.tunIf, l.defaultInterfaceMonitor, @@ -279,6 +302,10 @@ func (l *Listener) Close() { ) } -func (l *Listener) Config() config.Tun { +func (l *Listener) Config() LC.Tun { return l.options } + +func (l *Listener) Address() string { + return l.addrStr +} diff --git a/listener/sing_tun/server_android.go b/listener/sing_tun/server_android.go index e9d36f663d..4f85c4188b 100644 --- a/listener/sing_tun/server_android.go +++ b/listener/sing_tun/server_android.go @@ -1,7 +1,11 @@ package sing_tun import ( - tun "github.com/sagernet/sing-tun" + "github.com/Dreamacro/clash/log" + tun "github.com/metacubex/sing-tun" + "github.com/sagernet/netlink" + "golang.org/x/sys/unix" + "runtime" ) func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { @@ -21,3 +25,25 @@ func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { func (h *ListenerHandler) OnPackagesUpdated(packages int, sharedUsers int) { return } + +func (l *Listener) openAndroidHotspot(tunOptions tun.Options) { + if runtime.GOOS == "android" && tunOptions.AutoRoute { + priority := 9000 + if len(tunOptions.ExcludedRanges()) > 0 { + priority++ + } + if tunOptions.InterfaceMonitor.AndroidVPNEnabled() { + priority++ + } + it := netlink.NewRule() + it.Priority = priority + it.IifName = tunOptions.Name + it.Table = 254 //main + it.Family = unix.AF_INET + it.SuppressPrefixlen = 0 + err := netlink.RuleAdd(it) + if err != nil { + log.Warnln("[TUN] add AndroidHotspot rule error") + } + } +} diff --git a/listener/sing_tun/server_notandroid.go b/listener/sing_tun/server_notandroid.go index f621c71ef0..6b30ee03b2 100644 --- a/listener/sing_tun/server_notandroid.go +++ b/listener/sing_tun/server_notandroid.go @@ -3,9 +3,10 @@ package sing_tun import ( - tun "github.com/sagernet/sing-tun" + tun "github.com/metacubex/sing-tun" ) func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { return nil } +func (l *Listener) openAndroidHotspot(tunOptions tun.Options) {} diff --git a/listener/sing_tun/server_notwindows.go b/listener/sing_tun/server_notwindows.go index 27872d8e99..d3280c5c72 100644 --- a/listener/sing_tun/server_notwindows.go +++ b/listener/sing_tun/server_notwindows.go @@ -3,7 +3,7 @@ package sing_tun import ( - tun "github.com/sagernet/sing-tun" + tun "github.com/metacubex/sing-tun" ) func tunOpen(options tun.Options) (tun.Tun, error) { diff --git a/listener/sing_tun/server_windows.go b/listener/sing_tun/server_windows.go index 20fc703110..7b745cacb6 100644 --- a/listener/sing_tun/server_windows.go +++ b/listener/sing_tun/server_windows.go @@ -5,7 +5,7 @@ import ( "github.com/Dreamacro/clash/log" - tun "github.com/sagernet/sing-tun" + tun "github.com/metacubex/sing-tun" ) func tunOpen(options tun.Options) (tunIf tun.Tun, err error) { diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go new file mode 100644 index 0000000000..859317f0cc --- /dev/null +++ b/listener/sing_vmess/server.go @@ -0,0 +1,155 @@ +package sing_vmess + +import ( + "context" + "net" + "net/url" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing" + + vmess "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/metadata" +) + +type Listener struct { + closed bool + config LC.VmessServer + listeners []net.Listener + service *vmess.Service[string] +} + +var _listener *Listener + +func New(config LC.VmessServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (sl *Listener, err error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-VMESS"), + inbound.WithSpecialRules(""), + } + defer func() { + _listener = sl + }() + } + h := &sing.ListenerHandler{ + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.VMESS, + Additions: additions, + } + + service := vmess.NewService[string](h) + err = service.UpdateUsers( + common.Map(config.Users, func(it LC.VmessUser) string { + return it.Username + }), + common.Map(config.Users, func(it LC.VmessUser) string { + return it.UUID + }), + common.Map(config.Users, func(it LC.VmessUser) int { + return it.AlterID + })) + if err != nil { + return nil, err + } + + err = service.Start() + if err != nil { + return nil, err + } + + sl = &Listener{false, config, nil, service} + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + + //TCP + l, err := inbound.Listen("tcp", addr) + if err != nil { + return nil, err + } + sl.listeners = append(sl.listeners, l) + + go func() { + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + _ = c.(*net.TCPConn).SetKeepAlive(true) + + go sl.HandleConn(c, tcpIn) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + l.closed = true + var retErr error + for _, lis := range l.listeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + err := l.service.Close() + if err != nil { + retErr = err + } + return retErr +} + +func (l *Listener) Config() string { + return l.config.String() +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.listeners { + addrList = append(addrList, lis.Addr()) + } + return +} + +func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { + ctx := sing.WithAdditions(context.TODO(), additions...) + err := l.service.NewConnection(ctx, conn, metadata.Metadata{ + Protocol: "vmess", + Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), + }) + if err != nil { + _ = conn.Close() + return + } +} + +func HandleVmess(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) bool { + if _listener != nil && _listener.service != nil { + go _listener.HandleConn(conn, in, additions...) + return true + } + return false +} + +func ParseVmessURL(s string) (addr, username, password string, err error) { + u, err := url.Parse(s) + if err != nil { + return + } + + addr = u.Host + if u.User != nil { + username = u.User.Username() + password, _ = u.User.Password() + } + return +} diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index 8b505e744d..cbaac98726 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -1,8 +1,6 @@ package socks import ( - "context" - "github.com/database64128/tfo-go" "io" "net" @@ -36,11 +34,14 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, inboundTfo bool, in chan<- C.ConnContext) (*Listener, error) { - lc := tfo.ListenConfig{ - DisableTFO: !inboundTfo, +func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-SOCKS"), + inbound.WithSpecialRules(""), + } } - l, err := lc.Listen(context.Background(), "tcp", addr) + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } @@ -58,14 +59,14 @@ func New(addr string, inboundTfo bool, in chan<- C.ConnContext) (*Listener, erro } continue } - go handleSocks(c, in) + go handleSocks(c, in, additions...) } }() return sl, nil } -func handleSocks(conn net.Conn, in chan<- C.ConnContext) { +func handleSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { conn.(*net.TCPConn).SetKeepAlive(true) bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) @@ -76,24 +77,24 @@ func handleSocks(conn net.Conn, in chan<- C.ConnContext) { switch head[0] { case socks4.Version: - HandleSocks4(bufConn, in) + HandleSocks4(bufConn, in, additions...) case socks5.Version: - HandleSocks5(bufConn, in) + HandleSocks5(bufConn, in, additions...) default: conn.Close() } } -func HandleSocks4(conn net.Conn, in chan<- C.ConnContext) { +func HandleSocks4(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { addr, _, err := socks4.ServerHandshake(conn, authStore.Authenticator()) if err != nil { conn.Close() return } - in <- inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4) + in <- inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...) } -func HandleSocks5(conn net.Conn, in chan<- C.ConnContext) { +func HandleSocks5(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator()) if err != nil { conn.Close() @@ -104,5 +105,5 @@ func HandleSocks5(conn net.Conn, in chan<- C.ConnContext) { io.Copy(io.Discard, conn) return } - in <- inbound.NewSocket(target, conn, C.SOCKS5) + in <- inbound.NewSocket(target, conn, C.SOCKS5, additions...) } diff --git a/listener/socks/udp.go b/listener/socks/udp.go index 8bc439fb4c..f375dade34 100644 --- a/listener/socks/udp.go +++ b/listener/socks/udp.go @@ -33,7 +33,13 @@ func (l *UDPListener) Close() error { return l.packetConn.Close() } -func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) { +func NewUDP(addr string, in chan<- C.PacketAdapter, additions ...inbound.Addition) (*UDPListener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-SOCKS"), + inbound.WithSpecialRules(""), + } + } l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err @@ -58,14 +64,14 @@ func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) } continue } - handleSocksUDP(l, in, buf[:n], remoteAddr) + handleSocksUDP(l, in, buf[:n], remoteAddr, additions...) } }() return sl, nil } -func handleSocksUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, addr net.Addr) { +func handleSocksUDP(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, addr net.Addr, additions ...inbound.Addition) { target, payload, err := socks5.DecodeUDPPacket(buf) if err != nil { // Unresolved UDP packet, return buffer to the pool @@ -79,7 +85,7 @@ func handleSocksUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []b bufRef: buf, } select { - case in <- inbound.NewPacket(target, packet, C.SOCKS5): + case in <- inbound.NewPacket(target, packet, C.SOCKS5, additions...): default: } } diff --git a/listener/socks/utils.go b/listener/socks/utils.go index 28dfef7230..4c53b9e5c0 100644 --- a/listener/socks/utils.go +++ b/listener/socks/utils.go @@ -35,3 +35,7 @@ func (c *packet) LocalAddr() net.Addr { func (c *packet) Drop() { pool.Put(c.bufRef) } + +func (c *packet) InAddr() net.Addr { + return c.pc.LocalAddr() +} diff --git a/listener/tproxy/packet.go b/listener/tproxy/packet.go index 8aa3e9bf4b..e86a11ca7f 100644 --- a/listener/tproxy/packet.go +++ b/listener/tproxy/packet.go @@ -2,12 +2,14 @@ package tproxy import ( "net" + "net/netip" "github.com/Dreamacro/clash/common/pool" ) type packet struct { - lAddr *net.UDPAddr + pc net.PacketConn + lAddr netip.AddrPort buf []byte } @@ -17,7 +19,7 @@ func (c *packet) Data() []byte { // WriteBack opens a new socket binding `addr` to write UDP packet back func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { - tc, err := dialUDP("udp", addr.(*net.UDPAddr), c.lAddr) + tc, err := dialUDP("udp", addr.(*net.UDPAddr).AddrPort(), c.lAddr) if err != nil { n = 0 return @@ -29,9 +31,13 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { // LocalAddr returns the source IP/Port of UDP Packet func (c *packet) LocalAddr() net.Addr { - return c.lAddr + return &net.UDPAddr{IP: c.lAddr.Addr().AsSlice(), Port: int(c.lAddr.Port()), Zone: c.lAddr.Addr().Zone()} } func (c *packet) Drop() { pool.Put(c.buf) } + +func (c *packet) InAddr() net.Addr { + return c.pc.LocalAddr() +} diff --git a/listener/tproxy/tproxy.go b/listener/tproxy/tproxy.go index 1a09f36632..198481f78d 100644 --- a/listener/tproxy/tproxy.go +++ b/listener/tproxy/tproxy.go @@ -30,13 +30,19 @@ func (l *Listener) Close() error { return l.listener.Close() } -func (l *Listener) handleTProxy(conn net.Conn, in chan<- C.ConnContext) { +func (l *Listener) handleTProxy(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { target := socks5.ParseAddrToSocksAddr(conn.LocalAddr()) conn.(*net.TCPConn).SetKeepAlive(true) - in <- inbound.NewSocket(target, conn, C.TPROXY) + in <- inbound.NewSocket(target, conn, C.TPROXY, additions...) } -func New(addr string, in chan<- C.ConnContext) (*Listener, error) { +func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-TPROXY"), + inbound.WithSpecialRules(""), + } + } l, err := net.Listen("tcp", addr) if err != nil { return nil, err @@ -67,7 +73,7 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) { } continue } - go rl.handleTProxy(c, in) + go rl.handleTProxy(c, in, additions...) } }() diff --git a/listener/tproxy/udp.go b/listener/tproxy/udp.go index c7e6d99e3f..f85c9ea979 100644 --- a/listener/tproxy/udp.go +++ b/listener/tproxy/udp.go @@ -2,6 +2,7 @@ package tproxy import ( "net" + "net/netip" "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/common/pool" @@ -31,7 +32,13 @@ func (l *UDPListener) Close() error { return l.packetConn.Close() } -func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) { +func NewUDP(addr string, in chan<- C.PacketAdapter, additions ...inbound.Addition) (*UDPListener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-TPROXY"), + inbound.WithSpecialRules(""), + } + } l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err @@ -58,7 +65,7 @@ func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) oob := make([]byte, 1024) for { buf := pool.Get(pool.UDPBufferSize) - n, oobn, _, lAddr, err := c.ReadMsgUDP(buf, oob) + n, oobn, _, lAddr, err := c.ReadMsgUDPAddrPort(buf, oob) if err != nil { pool.Put(buf) if rl.closed { @@ -67,25 +74,31 @@ func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) continue } - rAddr, err := getOrigDst(oob, oobn) + rAddr, err := getOrigDst(oob[:oobn]) if err != nil { continue } - handlePacketConn(l, in, buf[:n], lAddr, rAddr) + + if rAddr.Addr().Is4() { + // try to unmap 4in6 address + lAddr = netip.AddrPortFrom(lAddr.Addr().Unmap(), lAddr.Port()) + } + handlePacketConn(l, in, buf[:n], lAddr, rAddr, additions...) } }() return rl, nil } -func handlePacketConn(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, lAddr *net.UDPAddr, rAddr *net.UDPAddr) { - target := socks5.ParseAddrToSocksAddr(rAddr) +func handlePacketConn(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, lAddr, rAddr netip.AddrPort, additions ...inbound.Addition) { + target := socks5.AddrFromStdAddrPort(rAddr) pkt := &packet{ + pc: pc, lAddr: lAddr, buf: buf, } select { - case in <- inbound.NewPacket(target, pkt, C.TPROXY): + case in <- inbound.NewPacket(target, pkt, C.TPROXY, additions...): default: } } diff --git a/listener/tproxy/udp_linux.go b/listener/tproxy/udp_linux.go index 8cda96fd53..472a23d3aa 100644 --- a/listener/tproxy/udp_linux.go +++ b/listener/tproxy/udp_linux.go @@ -3,13 +3,14 @@ package tproxy import ( - "encoding/binary" - "errors" "fmt" "net" + "net/netip" "os" "strconv" "syscall" + + "golang.org/x/sys/unix" ) const ( @@ -19,7 +20,7 @@ const ( // dialUDP acts like net.DialUDP for transparent proxy. // It binds to a non-local address(`lAddr`). -func dialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) { +func dialUDP(network string, lAddr, rAddr netip.AddrPort) (uc *net.UDPConn, err error) { rSockAddr, err := udpAddrToSockAddr(rAddr) if err != nil { return nil, err @@ -35,23 +36,25 @@ func dialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPCo return nil, err } + defer func() { + if err != nil { + syscall.Close(fd) + } + }() + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { - syscall.Close(fd) return nil, err } if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { - syscall.Close(fd) return nil, err } if err = syscall.Bind(fd, lSockAddr); err != nil { - syscall.Close(fd) return nil, err } if err = syscall.Connect(fd, rSockAddr); err != nil { - syscall.Close(fd) return nil, err } @@ -60,35 +63,26 @@ func dialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPCo c, err := net.FileConn(fdFile) if err != nil { - syscall.Close(fd) return nil, err } return c.(*net.UDPConn), nil } -func udpAddrToSockAddr(addr *net.UDPAddr) (syscall.Sockaddr, error) { - switch { - case addr.IP.To4() != nil: - ip := [4]byte{} - copy(ip[:], addr.IP.To4()) - - return &syscall.SockaddrInet4{Addr: ip, Port: addr.Port}, nil - - default: - ip := [16]byte{} - copy(ip[:], addr.IP.To16()) - - zoneID, err := strconv.ParseUint(addr.Zone, 10, 32) - if err != nil { - zoneID = 0 - } +func udpAddrToSockAddr(addr netip.AddrPort) (syscall.Sockaddr, error) { + if addr.Addr().Is4() { + return &syscall.SockaddrInet4{Addr: addr.Addr().As4(), Port: int(addr.Port())}, nil + } - return &syscall.SockaddrInet6{Addr: ip, Port: addr.Port, ZoneId: uint32(zoneID)}, nil + zoneID, err := strconv.ParseUint(addr.Addr().Zone(), 10, 32) + if err != nil { + zoneID = 0 } + + return &syscall.SockaddrInet6{Addr: addr.Addr().As16(), Port: int(addr.Port()), ZoneId: uint32(zoneID)}, nil } -func udpAddrFamily(net string, lAddr, rAddr *net.UDPAddr) int { +func udpAddrFamily(net string, lAddr, rAddr netip.AddrPort) int { switch net[len(net)-1] { case '4': return syscall.AF_INET @@ -96,29 +90,35 @@ func udpAddrFamily(net string, lAddr, rAddr *net.UDPAddr) int { return syscall.AF_INET6 } - if (lAddr == nil || lAddr.IP.To4() != nil) && (rAddr == nil || lAddr.IP.To4() != nil) { + if lAddr.Addr().Is4() && rAddr.Addr().Is4() { return syscall.AF_INET } return syscall.AF_INET6 } -func getOrigDst(oob []byte, oobn int) (*net.UDPAddr, error) { - msgs, err := syscall.ParseSocketControlMessage(oob[:oobn]) +func getOrigDst(oob []byte) (netip.AddrPort, error) { + // oob contains socket control messages which we need to parse. + scms, err := unix.ParseSocketControlMessage(oob) if err != nil { - return nil, err + return netip.AddrPort{}, fmt.Errorf("parse control message: %w", err) } - for _, msg := range msgs { - if msg.Header.Level == syscall.SOL_IP && msg.Header.Type == syscall.IP_RECVORIGDSTADDR { - ip := net.IP(msg.Data[4:8]) - port := binary.BigEndian.Uint16(msg.Data[2:4]) - return &net.UDPAddr{IP: ip, Port: int(port)}, nil - } else if msg.Header.Level == syscall.SOL_IPV6 && msg.Header.Type == IPV6_RECVORIGDSTADDR { - ip := net.IP(msg.Data[8:24]) - port := binary.BigEndian.Uint16(msg.Data[2:4]) - return &net.UDPAddr{IP: ip, Port: int(port)}, nil - } + // retrieve the destination address from the SCM. + sa, err := unix.ParseOrigDstAddr(&scms[0]) + if err != nil { + return netip.AddrPort{}, fmt.Errorf("retrieve destination: %w", err) + } + + // encode the destination address into a cmsg. + var rAddr netip.AddrPort + switch v := sa.(type) { + case *unix.SockaddrInet4: + rAddr = netip.AddrPortFrom(netip.AddrFrom4(v.Addr), uint16(v.Port)) + case *unix.SockaddrInet6: + rAddr = netip.AddrPortFrom(netip.AddrFrom16(v.Addr), uint16(v.Port)) + default: + return netip.AddrPort{}, fmt.Errorf("unsupported address type: %T", v) } - return nil, errors.New("cannot find origDst") + return rAddr, nil } diff --git a/listener/tproxy/udp_other.go b/listener/tproxy/udp_other.go index db4a1409d4..b35b07dd5e 100644 --- a/listener/tproxy/udp_other.go +++ b/listener/tproxy/udp_other.go @@ -5,12 +5,13 @@ package tproxy import ( "errors" "net" + "net/netip" ) -func getOrigDst(oob []byte, oobn int) (*net.UDPAddr, error) { - return nil, errors.New("UDP redir not supported on current platform") +func getOrigDst(oob []byte) (netip.AddrPort, error) { + return netip.AddrPort{}, errors.New("UDP redir not supported on current platform") } -func dialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) { +func dialUDP(network string, lAddr, rAddr netip.AddrPort) (*net.UDPConn, error) { return nil, errors.New("UDP redir not supported on current platform") } diff --git a/listener/tuic/server.go b/listener/tuic/server.go new file mode 100644 index 0000000000..8b928637cb --- /dev/null +++ b/listener/tuic/server.go @@ -0,0 +1,148 @@ +package tuic + +import ( + "crypto/tls" + "net" + "strings" + "time" + + "github.com/metacubex/quic-go" + + "github.com/Dreamacro/clash/adapter/inbound" + CN "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/sockopt" + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/socks5" + "github.com/Dreamacro/clash/transport/tuic" +) + +type Listener struct { + closed bool + config LC.TuicServer + udpListeners []net.PacketConn + servers []*tuic.Server +} + +func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-TUIC"), + inbound.WithSpecialRules(""), + } + } + cert, err := CN.ParseCert(config.Certificate, config.PrivateKey) + if err != nil { + return nil, err + } + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{cert}, + } + if len(config.ALPN) > 0 { + tlsConfig.NextProtos = config.ALPN + } else { + tlsConfig.NextProtos = []string{"h3"} + } + quicConfig := &quic.Config{ + MaxIdleTimeout: time.Duration(config.MaxIdleTime) * time.Millisecond, + MaxIncomingStreams: 1 >> 32, + MaxIncomingUniStreams: 1 >> 32, + EnableDatagrams: true, + } + quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 + quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow + quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 + quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow + + tokens := make([][32]byte, len(config.Token)) + for i, token := range config.Token { + tokens[i] = tuic.GenTKN(token) + } + + option := &tuic.ServerOption{ + HandleTcpFn: func(conn net.Conn, addr socks5.Addr) error { + tcpIn <- inbound.NewSocket(addr, conn, C.TUIC, additions...) + return nil + }, + HandleUdpFn: func(addr socks5.Addr, packet C.UDPPacket) error { + select { + case udpIn <- inbound.NewPacket(addr, packet, C.TUIC, additions...): + default: + } + return nil + }, + TlsConfig: tlsConfig, + QuicConfig: quicConfig, + Tokens: tokens, + CongestionController: config.CongestionController, + AuthenticationTimeout: time.Duration(config.AuthenticationTimeout) * time.Millisecond, + MaxUdpRelayPacketSize: config.MaxUdpRelayPacketSize, + } + + sl := &Listener{false, config, nil, nil} + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + + ul, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl.udpListeners = append(sl.udpListeners, ul) + + server, err := tuic.NewServer(option, ul) + if err != nil { + return nil, err + } + + sl.servers = append(sl.servers, server) + + go func() { + err := server.Serve() + if err != nil { + if sl.closed { + return + } + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + l.closed = true + var retErr error + for _, lis := range l.servers { + err := lis.Close() + if err != nil { + retErr = err + } + } + for _, lis := range l.udpListeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + return retErr +} + +func (l *Listener) Config() LC.TuicServer { + return l.config +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.udpListeners { + addrList = append(addrList, lis.LocalAddr()) + } + return +} diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go new file mode 100644 index 0000000000..602f76758d --- /dev/null +++ b/listener/tunnel/packet.go @@ -0,0 +1,35 @@ +package tunnel + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack write UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + return c.pc.WriteTo(b, c.rAddr) +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *packet) Drop() { + pool.Put(c.payload) +} + +func (c *packet) InAddr() net.Addr { + return c.pc.LocalAddr() +} diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go new file mode 100644 index 0000000000..bf278c1c51 --- /dev/null +++ b/listener/tunnel/tcp.go @@ -0,0 +1,75 @@ +package tunnel + +import ( + "fmt" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + target socks5.Addr + proxy string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func (l *Listener) handleTCP(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { + conn.(*net.TCPConn).SetKeepAlive(true) + ctx := inbound.NewSocket(l.target, conn, C.TUNNEL, additions...) + ctx.Metadata().SpecialProxy = l.proxy + in <- ctx +} + +func New(addr, target, proxy string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + return nil, fmt.Errorf("invalid target address %s", target) + } + + rl := &Listener{ + listener: l, + target: targetAddr, + proxy: proxy, + addr: addr, + } + + go func() { + for { + c, err := l.Accept() + if err != nil { + if rl.closed { + break + } + continue + } + go rl.handleTCP(c, in, additions...) + } + }() + + return rl, nil +} diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go new file mode 100644 index 0000000000..0795084c5f --- /dev/null +++ b/listener/tunnel/udp.go @@ -0,0 +1,85 @@ +package tunnel + +import ( + "fmt" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type PacketConn struct { + conn net.PacketConn + addr string + target socks5.Addr + proxy string + closed bool +} + +// RawAddress implements C.Listener +func (l *PacketConn) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *PacketConn) Address() string { + return l.conn.LocalAddr().String() +} + +// Close implements C.Listener +func (l *PacketConn) Close() error { + l.closed = true + return l.conn.Close() +} + +func NewUDP(addr, target, proxy string, in chan<- C.PacketAdapter, additions ...inbound.Addition) (*PacketConn, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + return nil, fmt.Errorf("invalid target address %s", target) + } + + sl := &PacketConn{ + conn: l, + target: targetAddr, + proxy: proxy, + addr: addr, + } + go func() { + for { + buf := pool.Get(pool.UDPBufferSize) + n, remoteAddr, err := l.ReadFrom(buf) + if err != nil { + pool.Put(buf) + if sl.closed { + break + } + continue + } + sl.handleUDP(l, in, buf[:n], remoteAddr, additions...) + } + }() + + return sl, nil +} + +func (l *PacketConn) handleUDP(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, addr net.Addr, additions ...inbound.Addition) { + packet := &packet{ + pc: pc, + rAddr: addr, + payload: buf, + } + + ctx := inbound.NewPacket(l.target, packet, C.TUNNEL, additions...) + ctx.Metadata().SpecialProxy = l.proxy + select { + case in <- ctx: + default: + } +} diff --git a/rules/common/domain.go b/rules/common/domain.go index 2f0e4f2f70..1dc6b25092 100644 --- a/rules/common/domain.go +++ b/rules/common/domain.go @@ -19,9 +19,6 @@ func (d *Domain) RuleType() C.RuleType { } func (d *Domain) Match(metadata *C.Metadata) (bool, string) { - if metadata.AddrType != C.AtypDomainName { - return false, "" - } return metadata.Host == d.domain, d.adapter } diff --git a/rules/common/domain_keyword.go b/rules/common/domain_keyword.go index 58257544b4..4fff673e1c 100644 --- a/rules/common/domain_keyword.go +++ b/rules/common/domain_keyword.go @@ -19,9 +19,6 @@ func (dk *DomainKeyword) RuleType() C.RuleType { } func (dk *DomainKeyword) Match(metadata *C.Metadata) (bool, string) { - if metadata.AddrType != C.AtypDomainName { - return false, "" - } domain := metadata.Host return strings.Contains(domain, dk.keyword), dk.adapter } diff --git a/rules/common/domain_suffix.go b/rules/common/domain_suffix.go index c9016744eb..1e48a2362c 100644 --- a/rules/common/domain_suffix.go +++ b/rules/common/domain_suffix.go @@ -19,9 +19,6 @@ func (ds *DomainSuffix) RuleType() C.RuleType { } func (ds *DomainSuffix) Match(metadata *C.Metadata) (bool, string) { - if metadata.AddrType != C.AtypDomainName { - return false, "" - } domain := metadata.Host return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix, ds.adapter } diff --git a/rules/common/geoip.go b/rules/common/geoip.go index ada862d2b0..72e7704539 100644 --- a/rules/common/geoip.go +++ b/rules/common/geoip.go @@ -2,10 +2,10 @@ package common import ( "fmt" - "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/geodata/router" "strings" + "github.com/Dreamacro/clash/component/geodata" + "github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/mmdb" "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" diff --git a/rules/common/geosite.go b/rules/common/geosite.go index 9897f3496d..e5a0b9f7d3 100644 --- a/rules/common/geosite.go +++ b/rules/common/geosite.go @@ -2,13 +2,14 @@ package common import ( "fmt" + "github.com/Dreamacro/clash/component/geodata" _ "github.com/Dreamacro/clash/component/geodata/memconservative" "github.com/Dreamacro/clash/component/geodata/router" _ "github.com/Dreamacro/clash/component/geodata/standard" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" - _ "unsafe" + "github.com/Dreamacro/clash/transport/socks5" ) type GEOSITE struct { @@ -24,7 +25,7 @@ func (gs *GEOSITE) RuleType() C.RuleType { } func (gs *GEOSITE) Match(metadata *C.Metadata) (bool, string) { - if metadata.AddrType != C.AtypDomainName { + if metadata.AddrType() != socks5.AtypDomainName { return false, "" } diff --git a/rules/common/port.go b/rules/common/port.go index 270e5b2077..3b7ea1fcf1 100644 --- a/rules/common/port.go +++ b/rules/common/port.go @@ -13,22 +13,23 @@ type Port struct { *Base adapter string port string - isSource bool + ruleType C.RuleType portList []utils.Range[uint16] } func (p *Port) RuleType() C.RuleType { - if p.isSource { - return C.SrcPort - } - return C.DstPort + return p.ruleType } func (p *Port) Match(metadata *C.Metadata) (bool, string) { - if p.isSource { - return p.matchPortReal(metadata.SrcPort), p.adapter + targetPort := metadata.DstPort + switch p.ruleType { + case C.InPort: + targetPort = metadata.InPort + case C.SrcPort: + targetPort = metadata.SrcPort } - return p.matchPortReal(metadata.DstPort), p.adapter + return p.matchPortReal(targetPort), p.adapter } func (p *Port) Adapter() string { @@ -51,7 +52,7 @@ func (p *Port) matchPortReal(portRef string) bool { return false } -func NewPort(port string, adapter string, isSource bool) (*Port, error) { +func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) { ports := strings.Split(port, "/") if len(ports) > 28 { return nil, fmt.Errorf("%s, too many ports to use, maximum support 28 ports", errPayload.Error()) @@ -95,7 +96,7 @@ func NewPort(port string, adapter string, isSource bool) (*Port, error) { Base: &Base{}, adapter: adapter, port: port, - isSource: isSource, + ruleType: ruleType, portList: portRange, }, nil } diff --git a/rules/common/process.go b/rules/common/process.go index 9263c32d99..e972d2bc7e 100644 --- a/rules/common/process.go +++ b/rules/common/process.go @@ -14,7 +14,11 @@ type Process struct { } func (ps *Process) RuleType() C.RuleType { - return C.Process + if ps.nameOnly { + return C.Process + } + + return C.ProcessPath } func (ps *Process) Match(metadata *C.Metadata) (bool, string) { diff --git a/rules/common/uid.go b/rules/common/uid.go index 5a989f6772..1457a3d0dc 100644 --- a/rules/common/uid.go +++ b/rules/common/uid.go @@ -13,7 +13,7 @@ import ( type Uid struct { *Base - uids []utils.Range[int32] + uids []utils.Range[uint32] oUid string adapter string } @@ -26,7 +26,7 @@ func NewUid(oUid, adapter string) (*Uid, error) { return nil, fmt.Errorf("uid rule not support this platform") } - var uidRange []utils.Range[int32] + var uidRange []utils.Range[uint32] for _, u := range strings.Split(oUid, "/") { if u == "" { continue @@ -45,14 +45,14 @@ func NewUid(oUid, adapter string) (*Uid, error) { switch subUidsLen { case 1: - uidRange = append(uidRange, *utils.NewRange(int32(uidStart), int32(uidStart))) + uidRange = append(uidRange, *utils.NewRange(uint32(uidStart), uint32(uidStart))) case 2: uidEnd, err := strconv.ParseUint(strings.Trim(subUids[1], "[ ]"), 10, 32) if err != nil { return nil, errPayload } - uidRange = append(uidRange, *utils.NewRange(int32(uidStart), int32(uidEnd))) + uidRange = append(uidRange, *utils.NewRange(uint32(uidStart), uint32(uidEnd))) } } @@ -76,19 +76,21 @@ func (u *Uid) Match(metadata *C.Metadata) (bool, string) { if err != nil { return false, "" } - var uid int32 + var uid *uint32 if metadata.Uid != nil { - uid = *metadata.Uid + uid = metadata.Uid } else if uid, err = process.FindUid(metadata.NetWork.String(), metadata.SrcIP, int(srcPort)); err == nil { - metadata.Uid = &uid + metadata.Uid = uid } else { log.Warnln("[UID] could not get uid from %s", metadata.String()) return false, "" } - for _, _uid := range u.uids { - if _uid.Contains(uid) { - return true, u.adapter + if uid != nil { + for _, _uid := range u.uids { + if _uid.Contains(*uid) { + return true, u.adapter + } } } return false, "" diff --git a/rules/logic/and.go b/rules/logic/and.go deleted file mode 100644 index a8fc1badc1..0000000000 --- a/rules/logic/and.go +++ /dev/null @@ -1,68 +0,0 @@ -package logic - -import ( - "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/rules/common" - "strings" -) - -type AND struct { - *common.Base - rules []C.Rule - payload string - adapter string - needIP bool -} - -func (A *AND) ShouldFindProcess() bool { - return false -} - -func NewAND(payload string, adapter string, - parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) (*AND, error) { - and := &AND{Base: &common.Base{}, payload: payload, adapter: adapter} - rules, err := ParseRuleByPayload(payload, parse) - if err != nil { - return nil, err - } - - and.rules = rules - payloads := make([]string, 0, len(rules)) - for _, rule := range rules { - payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) - if rule.ShouldResolveIP() { - and.needIP = true - break - } - } - - and.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " && ")) - return and, nil -} - -func (A *AND) RuleType() C.RuleType { - return C.AND -} - -func (A *AND) Match(metadata *C.Metadata) (bool, string) { - for _, rule := range A.rules { - if m, _ := rule.Match(metadata); !m { - return false, "" - } - } - - return true, A.adapter -} - -func (A *AND) Adapter() string { - return A.adapter -} - -func (A *AND) Payload() string { - return A.payload -} - -func (A *AND) ShouldResolveIP() bool { - return A.needIP -} diff --git a/rules/logic/common.go b/rules/logic/common.go deleted file mode 100644 index 080771ba66..0000000000 --- a/rules/logic/common.go +++ /dev/null @@ -1,139 +0,0 @@ -package logic - -import ( - "fmt" - "github.com/Dreamacro/clash/common/collections" - C "github.com/Dreamacro/clash/constant" - "regexp" - "strings" - _ "unsafe" -) - -func ParseRuleByPayload(payload string, parseRule func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) ([]C.Rule, error) { - regex, err := regexp.Compile("\\(.*\\)") - if err != nil { - return nil, err - } - - if regex.MatchString(payload) { - subAllRanges, err := format(payload) - if err != nil { - return nil, err - } - rules := make([]C.Rule, 0, len(subAllRanges)) - - subRanges := findSubRuleRange(payload, subAllRanges) - for _, subRange := range subRanges { - subPayload := payload[subRange.start+1 : subRange.end] - - rule, err := payloadToRule(subPayload, parseLogicSubRule(parseRule)) - if err != nil { - return nil, err - } - - rules = append(rules, rule) - } - - return rules, nil - } - - return nil, fmt.Errorf("payload format error") -} - -func containRange(r Range, preStart, preEnd int) bool { - return preStart < r.start && preEnd > r.end -} - -func payloadToRule(subPayload string, parseRule func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error)) (C.Rule, error) { - splitStr := strings.SplitN(subPayload, ",", 2) - if len(splitStr) < 2 { - return nil, fmt.Errorf("[%s] format is error", subPayload) - } - - tp := splitStr[0] - payload := splitStr[1] - if tp == "NOT" || tp == "OR" || tp == "AND" { - return parseRule(tp, payload, "", nil) - } - param := strings.Split(payload, ",") - return parseRule(tp, param[0], "", param[1:]) -} - -func parseLogicSubRule(parseRule func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) { - return func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) { - switch tp { - case "MATCH", "SUB-RULE": - return nil, fmt.Errorf("unsupported rule type [%s] on logic rule", tp) - default: - return parseRule(tp, payload, target, params, nil) - } - } -} - -type Range struct { - start int - end int - index int -} - -func format(payload string) ([]Range, error) { - stack := collections.NewStack() - num := 0 - subRanges := make([]Range, 0) - for i, c := range payload { - if c == '(' { - sr := Range{ - start: i, - index: num, - } - - num++ - stack.Push(sr) - } else if c == ')' { - if stack.Len() == 0 { - return nil, fmt.Errorf("missing '('") - } - - sr := stack.Pop().(Range) - sr.end = i - subRanges = append(subRanges, sr) - } - } - - if stack.Len() != 0 { - return nil, fmt.Errorf("format error is missing )") - } - - sortResult := make([]Range, len(subRanges)) - for _, sr := range subRanges { - sortResult[sr.index] = sr - } - - return sortResult, nil -} - -func findSubRuleRange(payload string, ruleRanges []Range) []Range { - payloadLen := len(payload) - subRuleRange := make([]Range, 0) - for _, rr := range ruleRanges { - if rr.start == 0 && rr.end == payloadLen-1 { - // 最大范围跳过 - continue - } - - containInSub := false - for _, r := range subRuleRange { - if containRange(rr, r.start, r.end) { - // The subRuleRange contains a range of rr, which is the next level node of the tree - containInSub = true - break - } - } - - if !containInSub { - subRuleRange = append(subRuleRange, rr) - } - } - - return subRuleRange -} diff --git a/rules/logic/logic.go b/rules/logic/logic.go new file mode 100644 index 0000000000..4a0bd7e8d7 --- /dev/null +++ b/rules/logic/logic.go @@ -0,0 +1,289 @@ +package logic + +import ( + "fmt" + "regexp" + "strings" + + "github.com/Dreamacro/clash/common/collections" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/rules/common" +) + +type Logic struct { + *common.Base + payload string + adapter string + ruleType C.RuleType + rules []C.Rule + subRules map[string][]C.Rule + needIP bool + needProcess bool +} + +type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) + +func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule ParseRuleFunc) (*Logic, error) { + logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules} + err := logic.parsePayload(fmt.Sprintf("(%s)", payload), parseRule) + if err != nil { + return nil, err + } + + if len(logic.rules) != 1 { + return nil, fmt.Errorf("Sub-Rule rule must contain one rule") + } + for _, rule := range subRules[adapter] { + if rule.ShouldResolveIP() { + logic.needIP = true + } + if rule.ShouldFindProcess() { + logic.needProcess = true + } + } + logic.subRules = subRules + return logic, nil +} + +func NewNOT(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, error) { + logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.NOT} + err := logic.parsePayload(payload, parseRule) + if err != nil { + return nil, err + } + + if len(logic.rules) != 1 { + return nil, fmt.Errorf("not rule must contain one rule") + } + logic.needIP = logic.rules[0].ShouldResolveIP() + logic.needProcess = logic.rules[0].ShouldFindProcess() + logic.payload = fmt.Sprintf("(!(%s,%s))", logic.rules[0].RuleType(), logic.rules[0].Payload()) + return logic, nil +} + +func NewOR(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, error) { + logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.OR} + err := logic.parsePayload(payload, parseRule) + if err != nil { + return nil, err + } + + payloads := make([]string, 0, len(logic.rules)) + for _, rule := range logic.rules { + payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) + if rule.ShouldResolveIP() { + logic.needIP = true + } + if rule.ShouldFindProcess() { + logic.needProcess = true + } + } + logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " || ")) + + return logic, nil +} +func NewAND(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, error) { + logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.AND} + err := logic.parsePayload(payload, parseRule) + if err != nil { + return nil, err + } + + payloads := make([]string, 0, len(logic.rules)) + for _, rule := range logic.rules { + payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) + if rule.ShouldResolveIP() { + logic.needIP = true + } + if rule.ShouldFindProcess() { + logic.needProcess = true + } + } + logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " && ")) + + return logic, nil +} + +type Range struct { + start int + end int + index int +} + +func (r Range) containRange(preStart, preEnd int) bool { + return preStart < r.start && preEnd > r.end +} + +func (logic *Logic) payloadToRule(subPayload string, parseRule ParseRuleFunc) (C.Rule, error) { + splitStr := strings.SplitN(subPayload, ",", 2) + if len(splitStr) < 2 { + return nil, fmt.Errorf("[%s] format is error", subPayload) + } + + tp := splitStr[0] + payload := splitStr[1] + switch tp { + case "MATCH", "SUB-RULE": + return nil, fmt.Errorf("unsupported rule type [%s] on logic rule", tp) + case "NOT", "OR", "AND": + return parseRule(tp, payload, "", nil, nil) + } + param := strings.Split(payload, ",") + return parseRule(tp, param[0], "", param[1:], nil) +} + +func (logic *Logic) format(payload string) ([]Range, error) { + stack := collections.NewStack() + num := 0 + subRanges := make([]Range, 0) + for i, c := range payload { + if c == '(' { + sr := Range{ + start: i, + index: num, + } + + num++ + stack.Push(sr) + } else if c == ')' { + if stack.Len() == 0 { + return nil, fmt.Errorf("missing '('") + } + + sr := stack.Pop().(Range) + sr.end = i + subRanges = append(subRanges, sr) + } + } + + if stack.Len() != 0 { + return nil, fmt.Errorf("format error is missing )") + } + + sortResult := make([]Range, len(subRanges)) + for _, sr := range subRanges { + sortResult[sr.index] = sr + } + + return sortResult, nil +} + +func (logic *Logic) findSubRuleRange(payload string, ruleRanges []Range) []Range { + payloadLen := len(payload) + subRuleRange := make([]Range, 0) + for _, rr := range ruleRanges { + if rr.start == 0 && rr.end == payloadLen-1 { + // 最大范围跳过 + continue + } + + containInSub := false + for _, r := range subRuleRange { + if rr.containRange(r.start, r.end) { + // The subRuleRange contains a range of rr, which is the next level node of the tree + containInSub = true + break + } + } + + if !containInSub { + subRuleRange = append(subRuleRange, rr) + } + } + + return subRuleRange +} + +func (logic *Logic) parsePayload(payload string, parseRule ParseRuleFunc) error { + regex, err := regexp.Compile("\\(.*\\)") + if err != nil { + return err + } + + if regex.MatchString(payload) { + subAllRanges, err := logic.format(payload) + if err != nil { + return err + } + rules := make([]C.Rule, 0, len(subAllRanges)) + + subRanges := logic.findSubRuleRange(payload, subAllRanges) + for _, subRange := range subRanges { + subPayload := payload[subRange.start+1 : subRange.end] + + rule, err := logic.payloadToRule(subPayload, parseRule) + if err != nil { + return err + } + + rules = append(rules, rule) + } + + logic.rules = rules + + return nil + } + + return fmt.Errorf("payload format error") +} + +func (logic *Logic) RuleType() C.RuleType { + return logic.ruleType +} + +func matchSubRules(metadata *C.Metadata, name string, subRules map[string][]C.Rule) (bool, string) { + for _, rule := range subRules[name] { + if m, a := rule.Match(metadata); m { + if rule.RuleType() == C.SubRules { + matchSubRules(metadata, rule.Adapter(), subRules) + } else { + return m, a + } + } + } + return false, "" +} + +func (logic *Logic) Match(metadata *C.Metadata) (bool, string) { + switch logic.ruleType { + case C.SubRules: + return matchSubRules(metadata, logic.adapter, logic.subRules) + case C.NOT: + if m, _ := logic.rules[0].Match(metadata); !m { + return true, logic.adapter + } + return false, "" + case C.OR: + for _, rule := range logic.rules { + if m, _ := rule.Match(metadata); m { + return true, logic.adapter + } + } + return false, "" + case C.AND: + for _, rule := range logic.rules { + if m, _ := rule.Match(metadata); !m { + return false, logic.adapter + } + } + return true, logic.adapter + } + + return false, "" +} + +func (logic *Logic) Adapter() string { + return logic.adapter +} + +func (logic *Logic) Payload() string { + return logic.payload +} + +func (logic *Logic) ShouldResolveIP() bool { + return logic.needIP +} + +func (logic *Logic) ShouldFindProcess() bool { + return logic.needProcess +} diff --git a/rules/logic/logic_test.go b/rules/logic/logic_test.go deleted file mode 100644 index 4ec72cc4a7..0000000000 --- a/rules/logic/logic_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package logic - -import ( - "fmt" - C "github.com/Dreamacro/clash/constant" - RC "github.com/Dreamacro/clash/rules/common" - RP "github.com/Dreamacro/clash/rules/provider" - "github.com/stretchr/testify/assert" - "testing" -) - -func ParseRule(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error) { - switch tp { - case "DOMAIN": - parsed = RC.NewDomain(payload, target) - case "DOMAIN-SUFFIX": - parsed = RC.NewDomainSuffix(payload, target) - case "DOMAIN-KEYWORD": - parsed = RC.NewDomainKeyword(payload, target) - case "GEOSITE": - parsed, parseErr = RC.NewGEOSITE(payload, target) - case "GEOIP": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewGEOIP(payload, target, noResolve) - case "IP-CIDR", "IP-CIDR6": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRNoResolve(noResolve)) - case "SRC-IP-CIDR": - parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) - case "IP-SUFFIX": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewIPSuffix(payload, target, false, noResolve) - case "SRC-IP-SUFFIX": - parsed, parseErr = RC.NewIPSuffix(payload, target, true, true) - case "SRC-PORT": - parsed, parseErr = RC.NewPort(payload, target, true) - case "DST-PORT": - parsed, parseErr = RC.NewPort(payload, target, false) - case "PROCESS-NAME": - parsed, parseErr = RC.NewProcess(payload, target, true) - case "PROCESS-PATH": - parsed, parseErr = RC.NewProcess(payload, target, false) - case "NETWORK": - parsed, parseErr = RC.NewNetworkType(payload, target) - case "UID": - parsed, parseErr = RC.NewUid(payload, target) - case "IN-TYPE": - parsed, parseErr = RC.NewInType(payload, target) - case "SUB-RULE": - parsed, parseErr = NewSubRule(payload, target, subRules, ParseRule) - case "AND": - parsed, parseErr = NewAND(payload, target, ParseRule) - case "OR": - parsed, parseErr = NewOR(payload, target, ParseRule) - case "NOT": - parsed, parseErr = NewNOT(payload, target, ParseRule) - case "RULE-SET": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RP.NewRuleSet(payload, target, noResolve) - case "MATCH": - parsed = RC.NewMatch(target) - parseErr = nil - default: - parseErr = fmt.Errorf("unsupported rule type %s", tp) - } - - return -} - -func TestAND(t *testing.T) { - and, err := NewAND("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.Equal(t, nil, err) - assert.Equal(t, "DIRECT", and.adapter) - assert.Equal(t, false, and.ShouldResolveIP()) - m, _ := and.Match(&C.Metadata{ - Host: "baidu.com", - AddrType: C.AtypDomainName, - NetWork: C.TCP, - DstPort: "20000", - }) - assert.Equal(t, true, m) - - and, err = NewAND("(DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.NotEqual(t, nil, err) - - and, err = NewAND("((AND,(DOMAIN,baidu.com),(NETWORK,TCP)),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.Equal(t, nil, err) -} - -func TestNOT(t *testing.T) { - not, err := NewNOT("((DST-PORT,6000-6500))", "REJECT", ParseRule) - assert.Equal(t, nil, err) - m, _ := not.Match(&C.Metadata{ - DstPort: "6100", - }) - assert.Equal(t, false, m) - - _, err = NewNOT("((DST-PORT,5600-6666),(DOMAIN,baidu.com))", "DIRECT", ParseRule) - assert.NotEqual(t, nil, err) - - _, err = NewNOT("(())", "DIRECT", ParseRule) - assert.NotEqual(t, nil, err) -} - -func TestOR(t *testing.T) { - or, err := NewOR("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.Equal(t, nil, err) - m, _ := or.Match(&C.Metadata{ - NetWork: C.TCP, - }) - assert.Equal(t, true, m) - assert.Equal(t, false, or.ShouldResolveIP()) -} diff --git a/rules/logic/not.go b/rules/logic/not.go deleted file mode 100644 index 6a5b34d8fe..0000000000 --- a/rules/logic/not.go +++ /dev/null @@ -1,63 +0,0 @@ -package logic - -import ( - "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/rules/common" -) - -type NOT struct { - *common.Base - rule C.Rule - payload string - adapter string -} - -func (not *NOT) ShouldFindProcess() bool { - return false -} - -func NewNOT(payload string, adapter string, parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) (*NOT, error) { - not := &NOT{Base: &common.Base{}, adapter: adapter} - rule, err := ParseRuleByPayload(payload, parse) - if err != nil { - return nil, err - } - - if len(rule) != 1 { - return nil, fmt.Errorf("not rule must contain one rule") - } - - not.rule = rule[0] - not.payload = fmt.Sprintf("(!(%s,%s))", rule[0].RuleType(), rule[0].Payload()) - - return not, nil -} - -func (not *NOT) RuleType() C.RuleType { - return C.NOT -} - -func (not *NOT) Match(metadata *C.Metadata) (bool, string) { - if not.rule == nil { - return true, not.adapter - } - - if m, _ := not.rule.Match(metadata); !m { - return true, not.adapter - } - - return false, "" -} - -func (not *NOT) Adapter() string { - return not.adapter -} - -func (not *NOT) Payload() string { - return not.payload -} - -func (not *NOT) ShouldResolveIP() bool { - return not.rule != nil && not.rule.ShouldResolveIP() -} diff --git a/rules/logic/or.go b/rules/logic/or.go deleted file mode 100644 index d1aae9acd2..0000000000 --- a/rules/logic/or.go +++ /dev/null @@ -1,67 +0,0 @@ -package logic - -import ( - "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/rules/common" - "strings" -) - -type OR struct { - *common.Base - rules []C.Rule - payload string - adapter string - needIP bool -} - -func (or *OR) ShouldFindProcess() bool { - return false -} - -func (or *OR) RuleType() C.RuleType { - return C.OR -} - -func (or *OR) Match(metadata *C.Metadata) (bool, string) { - for _, rule := range or.rules { - if m, _ := rule.Match(metadata); m { - return true, or.adapter - } - } - - return false, "" -} - -func (or *OR) Adapter() string { - return or.adapter -} - -func (or *OR) Payload() string { - return or.payload -} - -func (or *OR) ShouldResolveIP() bool { - return or.needIP -} - -func NewOR(payload string, adapter string, parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) (*OR, error) { - or := &OR{Base: &common.Base{}, payload: payload, adapter: adapter} - rules, err := ParseRuleByPayload(payload, parse) - if err != nil { - return nil, err - } - - or.rules = rules - payloads := make([]string, 0, len(rules)) - for _, rule := range rules { - payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType(), rule.Payload())) - if rule.ShouldResolveIP() { - or.needIP = true - break - } - } - - or.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " || ")) - return or, nil -} diff --git a/rules/logic/sub_rules.go b/rules/logic/sub_rules.go deleted file mode 100644 index b4ad761372..0000000000 --- a/rules/logic/sub_rules.go +++ /dev/null @@ -1,91 +0,0 @@ -package logic - -import ( - "fmt" - - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/rules/common" -) - -type SubRule struct { - *common.Base - payload string - payloadRule C.Rule - subName string - subRules *map[string][]C.Rule - shouldFindProcess *bool - shouldResolveIP *bool -} - -func NewSubRule(payload, subName string, sub *map[string][]C.Rule, - parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) (*SubRule, error) { - payloadRule, err := ParseRuleByPayload(fmt.Sprintf("(%s)", payload), parse) - if err != nil { - return nil, err - } - if len(payloadRule) != 1 { - return nil, fmt.Errorf("Sub-Rule rule must contain one rule") - } - - return &SubRule{ - Base: &common.Base{}, - payload: payload, - payloadRule: payloadRule[0], - subName: subName, - subRules: sub, - }, nil -} - -func (r *SubRule) RuleType() C.RuleType { - return C.SubRules -} - -func (r *SubRule) Match(metadata *C.Metadata) (bool, string) { - - return match(metadata, r.subName, r.subRules) -} - -func match(metadata *C.Metadata, name string, subRules *map[string][]C.Rule) (bool, string) { - for _, rule := range (*subRules)[name] { - if m, a := rule.Match(metadata); m { - if rule.RuleType() == C.SubRules { - match(metadata, rule.Adapter(), subRules) - } else { - return m, a - } - } - } - return false, "" -} - -func (r *SubRule) ShouldResolveIP() bool { - if r.shouldResolveIP == nil { - s := false - for _, rule := range (*r.subRules)[r.subName] { - s = s || rule.ShouldResolveIP() - } - r.shouldResolveIP = &s - } - - return *r.shouldResolveIP -} - -func (r *SubRule) ShouldFindProcess() bool { - if r.shouldFindProcess == nil { - s := false - for _, rule := range (*r.subRules)[r.subName] { - s = s || rule.ShouldFindProcess() - } - r.shouldFindProcess = &s - } - - return *r.shouldFindProcess -} - -func (r *SubRule) Adapter() string { - return r.subName -} - -func (r *SubRule) Payload() string { - return r.payload -} diff --git a/rules/logic_test/logic_test.go b/rules/logic_test/logic_test.go new file mode 100644 index 0000000000..de5ae56914 --- /dev/null +++ b/rules/logic_test/logic_test.go @@ -0,0 +1,57 @@ +package logic_test + +import ( + // https://github.com/golang/go/wiki/CodeReviewComments#import-dot + . "github.com/Dreamacro/clash/rules/logic" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/rules" + "github.com/stretchr/testify/assert" + "testing" +) + +var ParseRule = rules.ParseRule + +func TestAND(t *testing.T) { + and, err := NewAND("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) + assert.Equal(t, nil, err) + assert.Equal(t, "DIRECT", and.Adapter()) + assert.Equal(t, false, and.ShouldResolveIP()) + m, _ := and.Match(&C.Metadata{ + Host: "baidu.com", + NetWork: C.TCP, + DstPort: "20000", + }) + assert.Equal(t, true, m) + + and, err = NewAND("(DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) + assert.NotEqual(t, nil, err) + + and, err = NewAND("((AND,(DOMAIN,baidu.com),(NETWORK,TCP)),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) + assert.Equal(t, nil, err) +} + +func TestNOT(t *testing.T) { + not, err := NewNOT("((DST-PORT,6000-6500))", "REJECT", ParseRule) + assert.Equal(t, nil, err) + m, _ := not.Match(&C.Metadata{ + DstPort: "6100", + }) + assert.Equal(t, false, m) + + _, err = NewNOT("((DST-PORT,5600-6666),(DOMAIN,baidu.com))", "DIRECT", ParseRule) + assert.NotEqual(t, nil, err) + + _, err = NewNOT("(())", "DIRECT", ParseRule) + assert.NotEqual(t, nil, err) +} + +func TestOR(t *testing.T) { + or, err := NewOR("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) + assert.Equal(t, nil, err) + m, _ := or.Match(&C.Metadata{ + NetWork: C.TCP, + }) + assert.Equal(t, true, m) + assert.Equal(t, false, or.ShouldResolveIP()) +} diff --git a/rules/parser.go b/rules/parser.go index 6f9e229f09..1a33622595 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -8,7 +8,7 @@ import ( RP "github.com/Dreamacro/clash/rules/provider" ) -func ParseRule(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error) { +func ParseRule(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error) { switch tp { case "DOMAIN": parsed = RC.NewDomain(payload, target) @@ -32,9 +32,11 @@ func ParseRule(tp, payload, target string, params []string, subRules *map[string case "SRC-IP-SUFFIX": parsed, parseErr = RC.NewIPSuffix(payload, target, true, true) case "SRC-PORT": - parsed, parseErr = RC.NewPort(payload, target, true) + parsed, parseErr = RC.NewPort(payload, target, C.SrcPort) case "DST-PORT": - parsed, parseErr = RC.NewPort(payload, target, false) + parsed, parseErr = RC.NewPort(payload, target, C.DstPort) + case "IN-PORT": + parsed, parseErr = RC.NewPort(payload, target, C.InPort) case "PROCESS-NAME": parsed, parseErr = RC.NewProcess(payload, target, true) case "PROCESS-PATH": diff --git a/rules/provider/classical_strategy.go b/rules/provider/classical_strategy.go index 727688fcfc..1262327125 100644 --- a/rules/provider/classical_strategy.go +++ b/rules/provider/classical_strategy.go @@ -66,7 +66,7 @@ func ruleParse(ruleRaw string) (string, string, []string) { return "", "", nil } -func NewClassicalStrategy(parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) *classicalStrategy { +func NewClassicalStrategy(parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) *classicalStrategy { return &classicalStrategy{rules: []C.Rule{}, parse: func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) { switch tp { case "MATCH", "SUB-RULE": diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go index a61453837c..61fe93a633 100644 --- a/rules/provider/domain_strategy.go +++ b/rules/provider/domain_strategy.go @@ -9,7 +9,7 @@ import ( type domainStrategy struct { count int - domainRules *trie.DomainTrie[bool] + domainRules *trie.DomainTrie[struct{}] } func (d *domainStrategy) Match(metadata *C.Metadata) bool { @@ -25,17 +25,18 @@ func (d *domainStrategy) ShouldResolveIP() bool { } func (d *domainStrategy) OnUpdate(rules []string) { - domainTrie := trie.New[bool]() + domainTrie := trie.New[struct{}]() count := 0 for _, rule := range rules { actualDomain, _ := idna.ToASCII(rule) - err := domainTrie.Insert(actualDomain, true) + err := domainTrie.Insert(actualDomain, struct{}{}) if err != nil { log.Warnln("invalid domain:[%s]", rule) } else { count++ } } + domainTrie.Optimize() d.domainRules = domainTrie d.count = count diff --git a/rules/provider/parse.go b/rules/provider/parse.go index 86e21a3032..206bef10a4 100644 --- a/rules/provider/parse.go +++ b/rules/provider/parse.go @@ -17,7 +17,7 @@ type ruleProviderSchema struct { Interval int `provider:"interval,omitempty"` } -func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) (P.RuleProvider, error) { +func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) (P.RuleProvider, error) { schema := &ruleProviderSchema{} decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) if err := decoder.Decode(mapping, schema); err != nil { diff --git a/rules/provider/provider.go b/rules/provider/provider.go index 9ae125fb94..347bebaa6e 100644 --- a/rules/provider/provider.go +++ b/rules/provider/provider.go @@ -103,7 +103,7 @@ func (rp *ruleSetProvider) MarshalJSON() ([]byte, error) { } func NewRuleSetProvider(name string, behavior P.RuleType, interval time.Duration, vehicle P.Vehicle, - parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) P.RuleProvider { + parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) P.RuleProvider { rp := &ruleSetProvider{ behavior: behavior, } @@ -126,7 +126,7 @@ func NewRuleSetProvider(name string, behavior P.RuleType, interval time.Duration return wrapper } -func newStrategy(behavior P.RuleType, parse func(tp, payload, target string, params []string, subRules *map[string][]C.Rule) (parsed C.Rule, parseErr error)) ruleStrategy { +func newStrategy(behavior P.RuleType, parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) ruleStrategy { switch behavior { case P.Domain: strategy := NewDomainStrategy() diff --git a/test/.golangci.yaml b/test/.golangci.yaml index cfaed7700d..b65afc5efa 100644 --- a/test/.golangci.yaml +++ b/test/.golangci.yaml @@ -13,4 +13,4 @@ linters-settings: - prefix(github.com/Dreamacro/clash) - default staticcheck: - go: '1.18' + go: '1.19' diff --git a/test/clash_test.go b/test/clash_test.go index 2cc5bd9a52..3fdca5d01b 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -30,7 +30,7 @@ import ( const ( ImageShadowsocks = "mritd/shadowsocks:latest" ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest" - ImageVmess = "v2fly/v2fly-core:latest" + ImageVmess = "v2fly/v2fly-core:v4.45.2" ImageVmessLatest = "sagernet/v2fly-core:latest" ImageVless = "teddysun/xray:latest" ImageTrojan = "trojangfw/trojan:latest" @@ -555,9 +555,8 @@ func testPacketConnTimeout(t *testing.T, pc net.PacketConn) error { func testSuit(t *testing.T, proxy C.ProxyAdapter) { assert.NoError(t, testPingPongWithConn(t, func() net.Conn { conn, err := proxy.DialContext(context.Background(), &C.Metadata{ - Host: localIP.String(), - DstPort: "10001", - AddrType: socks5.AtypDomainName, + Host: localIP.String(), + DstPort: "10001", }) require.NoError(t, err) return conn @@ -565,9 +564,8 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { assert.NoError(t, testLargeDataWithConn(t, func() net.Conn { conn, err := proxy.DialContext(context.Background(), &C.Metadata{ - Host: localIP.String(), - DstPort: "10001", - AddrType: socks5.AtypDomainName, + Host: localIP.String(), + DstPort: "10001", }) require.NoError(t, err) return conn @@ -578,10 +576,9 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { } pc, err := proxy.ListenPacketContext(context.Background(), &C.Metadata{ - NetWork: C.UDP, - DstIP: localIP, - DstPort: "10001", - AddrType: socks5.AtypIPv4, + NetWork: C.UDP, + DstIP: localIP, + DstPort: "10001", }) require.NoError(t, err) defer pc.Close() @@ -589,10 +586,9 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { assert.NoError(t, testPingPongWithPacketConn(t, pc)) pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ - NetWork: C.UDP, - DstIP: localIP, - DstPort: "10001", - AddrType: socks5.AtypIPv4, + NetWork: C.UDP, + DstIP: localIP, + DstPort: "10001", }) require.NoError(t, err) defer pc.Close() @@ -600,10 +596,9 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { assert.NoError(t, testLargeDataWithPacketConn(t, pc)) pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ - NetWork: C.UDP, - DstIP: localIP, - DstPort: "10001", - AddrType: socks5.AtypIPv4, + NetWork: C.UDP, + DstIP: localIP, + DstPort: "10001", }) require.NoError(t, err) defer pc.Close() @@ -639,9 +634,8 @@ func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) { }() conn, err := proxy.DialContext(context.Background(), &C.Metadata{ - Host: localIP.String(), - DstPort: "10001", - AddrType: socks5.AtypDomainName, + Host: localIP.String(), + DstPort: "10001", }) require.NoError(b, err) diff --git a/test/dns_test.go b/test/dns_test.go index 1e4c010b8d..8e30ba98ab 100644 --- a/test/dns_test.go +++ b/test/dns_test.go @@ -78,9 +78,9 @@ dns: } list := []domainPair{ - {"foo.org", "198.18.0.3"}, - {"bar.org", "198.18.0.4"}, - {"foo.org", "198.18.0.3"}, + {"foo.org", "198.18.0.4"}, + {"bar.org", "198.18.0.5"}, + {"foo.org", "198.18.0.4"}, {"foo.clash.dev", "1.1.1.1"}, } diff --git a/test/go.mod b/test/go.mod index d5f6c29ea7..6cbf50e836 100644 --- a/test/go.mod +++ b/test/go.mod @@ -4,81 +4,82 @@ go 1.19 require ( github.com/Dreamacro/clash v0.0.0 - github.com/docker/docker v20.10.16+incompatible + github.com/docker/docker v20.10.21+incompatible github.com/docker/go-connections v0.4.0 - github.com/miekg/dns v1.1.49 - github.com/stretchr/testify v1.7.2 - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e + github.com/miekg/dns v1.1.50 + github.com/stretchr/testify v1.8.1 + golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e ) replace github.com/Dreamacro/clash => ../ -replace github.com/vishvananda/netlink => github.com/MetaCubeX/netlink v1.2.0-beta.0.20220529072258-d6853f887820 - -replace github.com/lucas-clemente/quic-go => github.com/tobyxdd/quic-go v0.27.1-0.20220512040129-ed2a645d9218 - require ( github.com/Microsoft/go-winio v0.5.1 // indirect - github.com/cheekybits/genny v1.0.0 // indirect - github.com/cilium/ebpf v0.9.1 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/cilium/ebpf v0.9.3 // indirect github.com/coreos/go-iptables v0.6.0 // indirect - github.com/database64128/tfo-go v1.1.0 // indirect + github.com/database64128/tfo-go/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect + github.com/gofrs/uuid v4.3.1+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/google/btree v1.0.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect + github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c // indirect + github.com/josharian/native v1.0.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect - github.com/lucas-clemente/quic-go v0.27.2 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect - github.com/marten-seemann/qpack v0.2.1 // indirect - github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect - github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect - github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/marten-seemann/qpack v0.3.0 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect + github.com/mdlayher/netlink v1.7.0 // indirect + github.com/mdlayher/socket v0.4.0 // indirect + github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e // indirect + github.com/metacubex/sing-shadowsocks v0.1.0 // indirect + github.com/metacubex/sing-tun v0.1.0 // indirect + github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c // indirect + github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/oschwald/geoip2-golang v1.7.0 // indirect - github.com/oschwald/maxminddb-golang v1.9.0 // indirect + github.com/oschwald/geoip2-golang v1.8.0 // indirect + github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sagernet/sing v0.0.0-20220627234642-a817f7084d9c // indirect - github.com/sagernet/sing-shadowsocks v0.0.0-20220627234717-689e0165ef2c // indirect - github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect - github.com/vishvananda/netlink v1.2.1-beta.2 // indirect + github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect + github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect + github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect + github.com/sagernet/sing v0.1.0 // indirect + github.com/sagernet/sing-vmess v0.1.0 // indirect + github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c // indirect + github.com/samber/lo v1.35.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect - github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect + github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 // indirect go.etcd.io/bbolt v1.3.6 // indirect - go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/exp v0.0.0-20220608143224-64259d1afd70 // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect - golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect - golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect - golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect - golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect - golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect - golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 // indirect - golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + go.uber.org/atomic v1.10.0 // indirect + golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a // indirect + golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669 // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + golang.org/x/tools v0.1.12 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.1.0 // indirect - gvisor.dev/gvisor v0.0.0-20220810234332-45096a971e66 // indirect + gotest.tools/v3 v3.4.0 // indirect + gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/test/go.sum b/test/go.sum index adec0f5549..22e24a1670 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,85 +1,44 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/MetaCubeX/netlink v1.2.0-beta.0.20220529072258-d6853f887820 h1:fGKWZ25VApYnuPZoNeqdH/nZtHa2XMajwH6Yj/OgoVc= -github.com/MetaCubeX/netlink v1.2.0-beta.0.20220529072258-d6853f887820/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= -github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= -github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4= -github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc= +github.com/cilium/ebpf v0.9.3/go.mod h1:w27N4UjpaQ9X/DGrSugxUG+H+NhgntDuPb5lCzxCn8A= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/database64128/tfo-go v1.1.0 h1:VO0polyGNSAmr99nYw9GQeMz7ZOcQ/QbjlTwniHwfTQ= -github.com/database64128/tfo-go v1.1.0/go.mod h1:95pOT8bnV3P2Lmu9upHNWFHz6dYGJ9cr7pnb0tGQAG8= +github.com/database64128/tfo-go/v2 v2.0.2 h1:5rGgkJeLEKlNaqredfrPQNLnctn1b+1fq/8tdKdOzJg= +github.com/database64128/tfo-go/v2 v2.0.2/go.mod h1:FDdt4JaAsRU66wsYHxSVytYimPkKIHupVsxM+5DhvjY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405crbpxvWzKovETOQ= -github.com/docker/docker v20.10.16+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= +github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -88,208 +47,145 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= -github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f h1:l1QCwn715k8nYkj4Ql50rzEog3WnMdrd4YYMMwemxEo= -github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c h1:OCFM4+DXTWfNlyeoddrTwdup/ztkGSyAMR2UGcPckNQ= +github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= +github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= -github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= -github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= -github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= -github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ= -github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= -github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= -github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/marten-seemann/qpack v0.3.0 h1:UiWstOgT8+znlkDPOg2+3rIuYXJ2CnGDkGUXN6ki6hE= +github.com/marten-seemann/qpack v0.3.0/go.mod h1:cGfKPBiP4a9EQdxCwEwI/GEeWAsjSekBvx/X8mh58+g= +github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI= +github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE= +github.com/marten-seemann/qtls-go1-19 v0.1.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/netlink v1.7.0 h1:ZNGI4V7i1fJ94DPYtWhI/R85i/Q7ZxnuhUJQcJMoodI= +github.com/mdlayher/netlink v1.7.0/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= -github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= +github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= +github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e h1:RnfC6+sShJ3biU2Q2wuh4FxZ8/3fp1QG+1zAfswVehA= +github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e/go.mod h1:7NPWVTLiX2Ss9q9gBNZaNHsPqZ3Tg/ApyrXxxUYbl78= +github.com/metacubex/sing-shadowsocks v0.1.0 h1:uGBtNkpy4QFlofaNkJf+iFegeLU11VzTUlkC46FHF8A= +github.com/metacubex/sing-shadowsocks v0.1.0/go.mod h1:8pBSYDKVxTtqUtGZyEh4ZpFJXwP6wBVVKrs6oQiOwmQ= +github.com/metacubex/sing-tun v0.1.0 h1:iQj0+0WjJynSKAtfv87wOZlVKWl3w9RvkOSkVe9zuMg= +github.com/metacubex/sing-tun v0.1.0/go.mod h1:l4JyI6RTrlHLQz5vSakg+wxA+LwGVI0Mz5ZtlOv67dA= +github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c h1:VHtXDny/TNOF7YDT9d9Qkr+x6K1O4cejXLlyPUXDeXQ= +github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c/go.mod h1:fULJ451x1/XlpIhl+Oo+EPGKla9tFZaqT5dKLrZ+NvM= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/POIPtYD5tRAq0qMqCRjQNK+g= +github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/oschwald/geoip2-golang v1.7.0 h1:JW1r5AKi+vv2ujSxjKthySK3jo8w8oKWPyXsw+Qs/S8= -github.com/oschwald/geoip2-golang v1.7.0/go.mod h1:mdI/C7iK7NVMcIDDtf4bCKMJ7r0o7UwGeCo9eiitCMQ= -github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y= -github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= +github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= +github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= +github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sagernet/sing v0.0.0-20220627234642-a817f7084d9c h1:98QC0wtaD648MFPw82KaT1O9LloQgR4ZyIDtNtsno8Y= -github.com/sagernet/sing v0.0.0-20220627234642-a817f7084d9c/go.mod h1:I67R/q5f67xDExL2kL3RLIP7kGJBOPkYXkpRAykgC+E= -github.com/sagernet/sing-shadowsocks v0.0.0-20220627234717-689e0165ef2c h1:Jhgjyb2jXL4GtwJec6/kgeTqaQXsvMiNX2wAkGOSD3I= -github.com/sagernet/sing-shadowsocks v0.0.0-20220627234717-689e0165ef2c/go.mod h1:ng5pxdNnKZWlxzZTXRqWeY0ftzhScPZmjgJGJeRuPYY= -github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec h1:jUSfKmyL6K9O2TvIvcVacZ4eNXHYbNSfdph+DRPyVlU= -github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec/go.mod h1:jDZ8fJgOea7Y7MMHWgfqwLBVLnhtW2zuxS5wjtDaB84= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= +github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= +github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= +github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= +github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= +github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= +github.com/sagernet/sing v0.1.0 h1:FGmaP2BVPYO2IyC/3R1DaQa/zr+kOKHRgWqrmOF+Gu8= +github.com/sagernet/sing v0.1.0/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= +github.com/sagernet/sing-vmess v0.1.0 h1:x0tYBJRbVi7zVXpMEW45eApGpXIDs9ub3raglouAKMo= +github.com/sagernet/sing-vmess v0.1.0/go.mod h1:4lwj6EHrUlgRnKhbmtboGbt+wtl5+tHMv96Ez8LZArw= +github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c h1:qP3ZOHnjZalvqbjundbXiv/YrNlo3HOgrKc+S1QGs0U= +github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI= +github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= +github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tobyxdd/quic-go v0.27.1-0.20220512040129-ed2a645d9218 h1:0DEghzcIfYe+7HTuI+zEd/5M+5c/gcepjJWGdcPPIrc= -github.com/tobyxdd/quic-go v0.27.1-0.20220512040129-ed2a645d9218/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME= -github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 h1:4mkzGhKqt3JO1BWYjtD3iRFyAx4ow67hmSqOcGjuxqQ= -github.com/xtls/go v0.0.0-20210920065950-d4af136d3672/go.mod h1:YGGVbz9cOxyKFUmhW7LGaLZaMA0cPlHJinvAmVxEMSU= +github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 h1:AHhUwwFJGl27E46OpdJHplZkK09m7aETNBNzhT6t15M= +github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837/go.mod h1:YJTRELIWrGxR1s8xcEBgxcxBfwQfMGjdvNLTjN9XFgY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20220608143224-64259d1afd70 h1:8uGxpY2cLF9H/NSHUiEWUIBZqIcsMzMWIMPCCUkyYgc= -golang.org/x/exp v0.0.0-20220608143224-64259d1afd70/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a h1:diz9pEYuTIuLMJLs3rGDkeaTsNyRs6duYdFyPAxzE/U= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= +golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -298,166 +194,83 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e h1:IVOjWZQH/57UDcpX19vSmMz8w3ohroOMWohn8qWpRkg= +golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew= -golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669 h1:pvmSpBoSG0gD2LLPAX15QHPig8xsbU0tu1sSAmResqk= +golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab h1:eHo2TTVBaAPw9lDGK2Gb9GyPMXT6g7O63W6sx3ylbzU= -golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= -golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= -golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 h1:QlbNZ9SwDAepRQwgeWHLi3rfEMo/kVEU4SmgsNM7HmQ= -golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= -golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e h1:yV04h6Tx19uDR6LvuEbR19cDU+3QrB9LuGjtF7F5G0w= -golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e/go.mod h1:1CeiatTZwcwSFA3cAtMm8CQoroviTldnxd7DOgM/vI4= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= -gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -gvisor.dev/gvisor v0.0.0-20220810234332-45096a971e66 h1:GrHxpIMY0lHZ3Q8rp3m4iOb0pJsnCQ/5AHaN9SXE69E= -gvisor.dev/gvisor v0.0.0-20220810234332-45096a971e66/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= +gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/transport/gun/gun.go b/transport/gun/gun.go index f65a192964..66b95517ac 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -18,7 +18,6 @@ import ( "time" "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" "go.uber.org/atomic" "golang.org/x/net/http2" @@ -168,7 +167,7 @@ func (g *Conn) SetDeadline(t time.Time) error { func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *TransportWrap { wrap := TransportWrap{} - dialFunc := func(network, addr string, cfg *tls.Config) (net.Conn, error) { + dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { pconn, err := dialFn(network, addr) if err != nil { return nil, err @@ -176,10 +175,6 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *TransportWrap { wrap.remoteAddr = pconn.RemoteAddr() cn := tls.Client(pconn, cfg) - - // fix tls handshake not timeout - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() if err := cn.HandshakeContext(ctx); err != nil { pconn.Close() return nil, err @@ -193,7 +188,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *TransportWrap { } wrap.Transport = &http2.Transport{ - DialTLS: dialFunc, + DialTLSContext: dialFunc, TLSClientConfig: tlsConfig, AllowHTTP: false, DisableCompression: true, diff --git a/transport/hysteria/congestion/brutal.go b/transport/hysteria/congestion/brutal.go index d08ac3465a..8f02ef14fa 100644 --- a/transport/hysteria/congestion/brutal.go +++ b/transport/hysteria/congestion/brutal.go @@ -1,7 +1,7 @@ package congestion import ( - "github.com/lucas-clemente/quic-go/congestion" + "github.com/metacubex/quic-go/congestion" "time" ) diff --git a/transport/hysteria/congestion/pacer.go b/transport/hysteria/congestion/pacer.go index 43707108fb..2dff53008d 100644 --- a/transport/hysteria/congestion/pacer.go +++ b/transport/hysteria/congestion/pacer.go @@ -1,7 +1,7 @@ package congestion import ( - "github.com/lucas-clemente/quic-go/congestion" + "github.com/metacubex/quic-go/congestion" "math" "time" ) diff --git a/transport/hysteria/conns/udp/hop.go b/transport/hysteria/conns/udp/hop.go new file mode 100644 index 0000000000..c31f317f68 --- /dev/null +++ b/transport/hysteria/conns/udp/hop.go @@ -0,0 +1,360 @@ +package udp + +import ( + "errors" + "math/rand" + "net" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/Dreamacro/clash/transport/hysteria/obfs" + "github.com/Dreamacro/clash/transport/hysteria/utils" +) + +const ( + packetQueueSize = 1024 +) + +// ObfsUDPHopClientPacketConn is the UDP port-hopping packet connection for client side. +// It hops to a different local & server port every once in a while. +type ObfsUDPHopClientPacketConn struct { + serverAddr net.Addr // Combined udpHopAddr + serverAddrs []net.Addr + hopInterval time.Duration + + obfs obfs.Obfuscator + + connMutex sync.RWMutex + prevConn net.PacketConn + currentConn net.PacketConn + addrIndex int + + readBufferSize int + writeBufferSize int + + recvQueue chan *udpPacket + closeChan chan struct{} + closed bool + + bufPool sync.Pool +} + +type udpHopAddr string + +func (a *udpHopAddr) Network() string { + return "udp-hop" +} + +func (a *udpHopAddr) String() string { + return string(*a) +} + +type udpPacket struct { + buf []byte + n int + addr net.Addr +} + +func NewObfsUDPHopClientPacketConn(server string, serverPorts string, hopInterval time.Duration, obfs obfs.Obfuscator, dialer utils.PacketDialer) (net.PacketConn, error) { + ports, err := parsePorts(serverPorts) + if err != nil { + return nil, err + } + // Resolve the server IP address, then attach the ports to UDP addresses + rAddr, err := dialer.RemoteAddr(server) + if err != nil { + return nil, err + } + ip, _, err := net.SplitHostPort(rAddr.String()) + if err != nil { + return nil, err + } + serverAddrs := make([]net.Addr, len(ports)) + for i, port := range ports { + serverAddrs[i] = &net.UDPAddr{ + IP: net.ParseIP(ip), + Port: int(port), + } + } + hopAddr := udpHopAddr(server) + conn := &ObfsUDPHopClientPacketConn{ + serverAddr: &hopAddr, + serverAddrs: serverAddrs, + hopInterval: hopInterval, + obfs: obfs, + addrIndex: rand.Intn(len(serverAddrs)), + recvQueue: make(chan *udpPacket, packetQueueSize), + closeChan: make(chan struct{}), + bufPool: sync.Pool{ + New: func() interface{} { + return make([]byte, udpBufferSize) + }, + }, + } + curConn, err := dialer.ListenPacket(rAddr) + if err != nil { + return nil, err + } + if obfs != nil { + conn.currentConn = NewObfsUDPConn(curConn, obfs) + } else { + conn.currentConn = curConn + } + go conn.recvRoutine(conn.currentConn) + go conn.hopRoutine(dialer, rAddr) + if _, ok := conn.currentConn.(syscall.Conn); ok { + return &ObfsUDPHopClientPacketConnWithSyscall{conn}, nil + } + return conn, nil +} + +func (c *ObfsUDPHopClientPacketConn) recvRoutine(conn net.PacketConn) { + for { + buf := c.bufPool.Get().([]byte) + n, addr, err := conn.ReadFrom(buf) + if err != nil { + return + } + select { + case c.recvQueue <- &udpPacket{buf, n, addr}: + default: + // Drop the packet if the queue is full + c.bufPool.Put(buf) + } + } +} + +func (c *ObfsUDPHopClientPacketConn) hopRoutine(dialer utils.PacketDialer, rAddr net.Addr) { + ticker := time.NewTicker(c.hopInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + c.hop(dialer, rAddr) + case <-c.closeChan: + return + } + } +} + +func (c *ObfsUDPHopClientPacketConn) hop(dialer utils.PacketDialer, rAddr net.Addr) { + c.connMutex.Lock() + defer c.connMutex.Unlock() + if c.closed { + return + } + newConn, err := dialer.ListenPacket(rAddr) + if err != nil { + // Skip this hop if failed to listen + return + } + // Close prevConn, + // prevConn <- currentConn + // currentConn <- newConn + // update addrIndex + // + // We need to keep receiving packets from the previous connection, + // because otherwise there will be packet loss due to the time gap + // between we hop to a new port and the server acknowledges this change. + if c.prevConn != nil { + _ = c.prevConn.Close() // recvRoutine will exit on error + } + c.prevConn = c.currentConn + if c.obfs != nil { + c.currentConn = NewObfsUDPConn(newConn, c.obfs) + } else { + c.currentConn = newConn + } + // Set buffer sizes if previously set + if c.readBufferSize > 0 { + _ = trySetPacketConnReadBuffer(c.currentConn, c.readBufferSize) + } + if c.writeBufferSize > 0 { + _ = trySetPacketConnWriteBuffer(c.currentConn, c.writeBufferSize) + } + go c.recvRoutine(c.currentConn) + c.addrIndex = rand.Intn(len(c.serverAddrs)) +} + +func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + for { + select { + case p := <-c.recvQueue: + /* + // Check if the packet is from one of the server addresses + for _, addr := range c.serverAddrs { + if addr.String() == p.addr.String() { + // Copy the packet to the buffer + n := copy(b, p.buf[:p.n]) + c.bufPool.Put(p.buf) + return n, c.serverAddr, nil + } + } + // Drop the packet, continue + c.bufPool.Put(p.buf) + */ + // The above code was causing performance issues when the range is large, + // so we skip the check for now. Should probably still check by using a map + // or something in the future. + n := copy(b, p.buf[:p.n]) + c.bufPool.Put(p.buf) + return n, c.serverAddr, nil + case <-c.closeChan: + return 0, nil, net.ErrClosed + } + // Ignore packets from other addresses + } +} + +func (c *ObfsUDPHopClientPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + c.connMutex.RLock() + defer c.connMutex.RUnlock() + /* + // Check if the address is the server address + if addr.String() != c.serverAddr.String() { + return 0, net.ErrWriteToConnected + } + */ + // Skip the check for now, always write to the server + return c.currentConn.WriteTo(b, c.serverAddrs[c.addrIndex]) +} + +func (c *ObfsUDPHopClientPacketConn) Close() error { + c.connMutex.Lock() + defer c.connMutex.Unlock() + if c.closed { + return nil + } + // Close prevConn and currentConn + // Close closeChan to unblock ReadFrom & hopRoutine + // Set closed flag to true to prevent double close + if c.prevConn != nil { + _ = c.prevConn.Close() + } + err := c.currentConn.Close() + close(c.closeChan) + c.closed = true + return err +} + +func (c *ObfsUDPHopClientPacketConn) LocalAddr() net.Addr { + c.connMutex.RLock() + defer c.connMutex.RUnlock() + return c.currentConn.LocalAddr() +} + +func (c *ObfsUDPHopClientPacketConn) SetReadDeadline(t time.Time) error { + // Not supported + return nil +} + +func (c *ObfsUDPHopClientPacketConn) SetWriteDeadline(t time.Time) error { + // Not supported + return nil +} + +func (c *ObfsUDPHopClientPacketConn) SetDeadline(t time.Time) error { + err := c.SetReadDeadline(t) + if err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +func (c *ObfsUDPHopClientPacketConn) SetReadBuffer(bytes int) error { + c.connMutex.Lock() + defer c.connMutex.Unlock() + c.readBufferSize = bytes + if c.prevConn != nil { + _ = trySetPacketConnReadBuffer(c.prevConn, bytes) + } + return trySetPacketConnReadBuffer(c.currentConn, bytes) +} + +func (c *ObfsUDPHopClientPacketConn) SetWriteBuffer(bytes int) error { + c.connMutex.Lock() + defer c.connMutex.Unlock() + c.writeBufferSize = bytes + if c.prevConn != nil { + _ = trySetPacketConnWriteBuffer(c.prevConn, bytes) + } + return trySetPacketConnWriteBuffer(c.currentConn, bytes) +} + +func trySetPacketConnReadBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetReadBuffer(bytes int) error + }) + if ok { + return sc.SetReadBuffer(bytes) + } + return nil +} + +func trySetPacketConnWriteBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetWriteBuffer(bytes int) error + }) + if ok { + return sc.SetWriteBuffer(bytes) + } + return nil +} + +type ObfsUDPHopClientPacketConnWithSyscall struct { + *ObfsUDPHopClientPacketConn +} + +func (c *ObfsUDPHopClientPacketConnWithSyscall) SyscallConn() (syscall.RawConn, error) { + c.connMutex.RLock() + defer c.connMutex.RUnlock() + sc, ok := c.currentConn.(syscall.Conn) + if !ok { + return nil, errors.New("not supported") + } + return sc.SyscallConn() +} + +// parsePorts parses the multi-port server address and returns the host and ports. +// Supports both comma-separated single ports and dash-separated port ranges. +// Format: "host:port1,port2-port3,port4" +func parsePorts(serverPorts string) (ports []uint16, err error) { + portStrs := strings.Split(serverPorts, ",") + for _, portStr := range portStrs { + if strings.Contains(portStr, "-") { + // Port range + portRange := strings.Split(portStr, "-") + if len(portRange) != 2 { + return nil, net.InvalidAddrError("invalid port range") + } + start, err := strconv.ParseUint(portRange[0], 10, 16) + if err != nil { + return nil, net.InvalidAddrError("invalid port range") + } + end, err := strconv.ParseUint(portRange[1], 10, 16) + if err != nil { + return nil, net.InvalidAddrError("invalid port range") + } + if start > end { + start, end = end, start + } + for i := start; i <= end; i++ { + ports = append(ports, uint16(i)) + } + } else { + // Single port + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil, net.InvalidAddrError("invalid port") + } + ports = append(ports, uint16(port)) + } + } + if len(ports) == 0 { + return nil, net.InvalidAddrError("invalid port") + } + return ports, nil +} diff --git a/transport/hysteria/core/client.go b/transport/hysteria/core/client.go index bd91250dbb..5465e3d2bc 100644 --- a/transport/hysteria/core/client.go +++ b/transport/hysteria/core/client.go @@ -6,18 +6,20 @@ import ( "crypto/tls" "errors" "fmt" - "github.com/Dreamacro/clash/transport/hysteria/obfs" - "github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" - "github.com/Dreamacro/clash/transport/hysteria/transport" - "github.com/Dreamacro/clash/transport/hysteria/utils" - "github.com/lucas-clemente/quic-go" - "github.com/lucas-clemente/quic-go/congestion" - "github.com/lunixbochs/struc" "math/rand" "net" "strconv" "sync" "time" + + "github.com/lunixbochs/struc" + "github.com/metacubex/quic-go" + "github.com/metacubex/quic-go/congestion" + + "github.com/Dreamacro/clash/transport/hysteria/obfs" + "github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" + "github.com/Dreamacro/clash/transport/hysteria/transport" + "github.com/Dreamacro/clash/transport/hysteria/utils" ) var ( @@ -29,6 +31,7 @@ type CongestionFactory func(refBPS uint64) congestion.CongestionControl type Client struct { transport *transport.ClientTransport serverAddr string + serverPorts string protocol string sendBPS, recvBPS uint64 auth []byte @@ -45,15 +48,18 @@ type Client struct { udpSessionMutex sync.RWMutex udpSessionMap map[uint32]chan *udpMessage udpDefragger defragger + hopInterval time.Duration + fastOpen bool } -func NewClient(serverAddr string, protocol string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config, +func NewClient(serverAddr string, serverPorts string, protocol string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config, transport *transport.ClientTransport, sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, - obfuscator obfs.Obfuscator) (*Client, error) { + obfuscator obfs.Obfuscator, hopInterval time.Duration, fastOpen bool) (*Client, error) { quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud_fix.DisablePathMTUDiscovery c := &Client{ transport: transport, serverAddr: serverAddr, + serverPorts: serverPorts, protocol: protocol, sendBPS: sendBPS, recvBPS: recvBPS, @@ -62,12 +68,14 @@ func NewClient(serverAddr string, protocol string, auth []byte, tlsConfig *tls.C obfuscator: obfuscator, tlsConfig: tlsConfig, quicConfig: quicConfig, + hopInterval: hopInterval, + fastOpen: fastOpen, } return c, nil } -func (c *Client) connectToServer(dialer transport.PacketDialer) error { - qs, err := c.transport.QUICDial(c.protocol, c.serverAddr, c.tlsConfig, c.quicConfig, c.obfuscator, dialer) +func (c *Client) connectToServer(dialer utils.PacketDialer) error { + qs, err := c.transport.QUICDial(c.protocol, c.serverAddr, c.serverPorts, c.tlsConfig, c.quicConfig, c.obfuscator, c.hopInterval, dialer) if err != nil { return err } @@ -154,7 +162,7 @@ func (c *Client) handleMessage(qs quic.Connection) { } } -func (c *Client) openStreamWithReconnect(dialer transport.PacketDialer) (quic.Connection, quic.Stream, error) { +func (c *Client) openStreamWithReconnect(dialer utils.PacketDialer) (quic.Connection, quic.Stream, error) { c.reconnectMutex.Lock() defer c.reconnectMutex.Unlock() if c.closed { @@ -186,7 +194,7 @@ func (c *Client) openStreamWithReconnect(dialer transport.PacketDialer) (quic.Co return c.quicSession, &wrappedQUICStream{stream}, err } -func (c *Client) DialTCP(addr string, dialer transport.PacketDialer) (net.Conn, error) { +func (c *Client) DialTCP(addr string, dialer utils.PacketDialer) (net.Conn, error) { host, port, err := utils.SplitHostPort(addr) if err != nil { return nil, err @@ -205,25 +213,31 @@ func (c *Client) DialTCP(addr string, dialer transport.PacketDialer) (net.Conn, _ = stream.Close() return nil, err } - // Read response - var sr serverResponse - err = struc.Unpack(stream, &sr) - if err != nil { - _ = stream.Close() - return nil, err - } - if !sr.OK { - _ = stream.Close() - return nil, fmt.Errorf("connection rejected: %s", sr.Message) + // If fast open is enabled, we return the stream immediately + // and defer the response handling to the first Read() call + if !c.fastOpen { + // Read response + var sr serverResponse + err = struc.Unpack(stream, &sr) + if err != nil { + _ = stream.Close() + return nil, err + } + if !sr.OK { + _ = stream.Close() + return nil, fmt.Errorf("connection rejected: %s", sr.Message) + } } + return &quicConn{ Orig: stream, PseudoLocalAddr: session.LocalAddr(), PseudoRemoteAddr: session.RemoteAddr(), + Established: !c.fastOpen, }, nil } -func (c *Client) DialUDP(dialer transport.PacketDialer) (UDPConn, error) { +func (c *Client) DialUDP(dialer utils.PacketDialer) (UDPConn, error) { session, stream, err := c.openStreamWithReconnect(dialer) if err != nil { return nil, err @@ -288,9 +302,23 @@ type quicConn struct { Orig quic.Stream PseudoLocalAddr net.Addr PseudoRemoteAddr net.Addr + Established bool } func (w *quicConn) Read(b []byte) (n int, err error) { + if !w.Established { + var sr serverResponse + err := struc.Unpack(w.Orig, &sr) + if err != nil { + _ = w.Close() + return 0, err + } + if !sr.OK { + _ = w.Close() + return 0, fmt.Errorf("connection rejected: %s", sr.Message) + } + w.Established = true + } return w.Orig.Read(b) } diff --git a/transport/hysteria/core/stream.go b/transport/hysteria/core/stream.go index 8ace4a1dde..627b478943 100644 --- a/transport/hysteria/core/stream.go +++ b/transport/hysteria/core/stream.go @@ -2,7 +2,7 @@ package core import ( "context" - "github.com/lucas-clemente/quic-go" + "github.com/metacubex/quic-go" "time" ) diff --git a/transport/hysteria/transport/client.go b/transport/hysteria/transport/client.go index 43330cd360..e65e5016b0 100644 --- a/transport/hysteria/transport/client.go +++ b/transport/hysteria/transport/client.go @@ -1,35 +1,46 @@ package transport import ( - "context" "crypto/tls" "fmt" + "net" + "strings" + "time" + + "github.com/metacubex/quic-go" + "github.com/Dreamacro/clash/transport/hysteria/conns/faketcp" "github.com/Dreamacro/clash/transport/hysteria/conns/udp" "github.com/Dreamacro/clash/transport/hysteria/conns/wechat" obfsPkg "github.com/Dreamacro/clash/transport/hysteria/obfs" - "github.com/lucas-clemente/quic-go" - "net" + "github.com/Dreamacro/clash/transport/hysteria/utils" ) type ClientTransport struct { Dialer *net.Dialer } -func (ct *ClientTransport) quicPacketConn(proto string, server string, obfs obfsPkg.Obfuscator, dialer PacketDialer) (net.PacketConn, error) { +func (ct *ClientTransport) quicPacketConn(proto string, rAddr net.Addr, serverPorts string, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (net.PacketConn, error) { + server := rAddr.String() if len(proto) == 0 || proto == "udp" { - conn, err := dialer.ListenPacket() + conn, err := dialer.ListenPacket(rAddr) if err != nil { return nil, err } if obfs != nil { + if serverPorts != "" { + return udp.NewObfsUDPHopClientPacketConn(server, serverPorts, hopInterval, obfs, dialer) + } oc := udp.NewObfsUDPConn(conn, obfs) return oc, nil } else { + if serverPorts != "" { + return udp.NewObfsUDPHopClientPacketConn(server, serverPorts, hopInterval, nil, dialer) + } return conn, nil } } else if proto == "wechat-video" { - conn, err := dialer.ListenPacket() + conn, err := dialer.ListenPacket(rAddr) if err != nil { return nil, err } @@ -54,19 +65,13 @@ func (ct *ClientTransport) quicPacketConn(proto string, server string, obfs obfs } } -type PacketDialer interface { - ListenPacket() (net.PacketConn, error) - Context() context.Context - RemoteAddr(host string) (net.Addr, error) -} - -func (ct *ClientTransport) QUICDial(proto string, server string, tlsConfig *tls.Config, quicConfig *quic.Config, obfs obfsPkg.Obfuscator, dialer PacketDialer) (quic.Connection, error) { +func (ct *ClientTransport) QUICDial(proto string, server string, serverPorts string, tlsConfig *tls.Config, quicConfig *quic.Config, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (quic.Connection, error) { serverUDPAddr, err := dialer.RemoteAddr(server) if err != nil { return nil, err } - pktConn, err := ct.quicPacketConn(proto, serverUDPAddr.String(), obfs, dialer) + pktConn, err := ct.quicPacketConn(proto, serverUDPAddr, serverPorts, obfs, hopInterval, dialer) if err != nil { return nil, err } @@ -90,3 +95,11 @@ func (ct *ClientTransport) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) { func (ct *ClientTransport) ListenUDP() (*net.UDPConn, error) { return net.ListenUDP("udp", nil) } + +func isMultiPortAddr(addr string) bool { + _, portStr, err := net.SplitHostPort(addr) + if err == nil && (strings.Contains(portStr, ",") || strings.Contains(portStr, "-")) { + return true + } + return false +} diff --git a/transport/hysteria/utils/misc.go b/transport/hysteria/utils/misc.go index 29c7cf0cc6..670d737c75 100644 --- a/transport/hysteria/utils/misc.go +++ b/transport/hysteria/utils/misc.go @@ -1,6 +1,7 @@ package utils import ( + "context" "net" "strconv" ) @@ -40,3 +41,9 @@ func last(s string, b byte) int { } return i } + +type PacketDialer interface { + ListenPacket(rAddr net.Addr) (net.PacketConn, error) + Context() context.Context + RemoteAddr(host string) (net.Addr, error) +} diff --git a/transport/shadowsocks/core/cipher.go b/transport/shadowsocks/core/cipher.go index 2f5acf6318..7f4f7f71c2 100644 --- a/transport/shadowsocks/core/cipher.go +++ b/transport/shadowsocks/core/cipher.go @@ -59,6 +59,7 @@ var streamList = map[string]struct { "AES-128-CFB": {16, shadowstream.AESCFB}, "AES-192-CFB": {24, shadowstream.AESCFB}, "AES-256-CFB": {32, shadowstream.AESCFB}, + "CHACHA20": {32, shadowstream.ChaCha20}, "CHACHA20-IETF": {32, shadowstream.Chacha20IETF}, "XCHACHA20": {32, shadowstream.Xchacha20}, } diff --git a/transport/shadowsocks/shadowstream/old_chacha20.go b/transport/shadowsocks/shadowstream/old_chacha20.go new file mode 100644 index 0000000000..65737fccd1 --- /dev/null +++ b/transport/shadowsocks/shadowstream/old_chacha20.go @@ -0,0 +1,22 @@ +package shadowstream + +import ( + "crypto/cipher" + "github.com/aead/chacha20/chacha" +) + +type chacha20key []byte + +func (k chacha20key) IVSize() int { + return chacha.NonceSize +} +func (k chacha20key) Encrypter(iv []byte) cipher.Stream { + c, _ := chacha.NewCipher(iv, k, 20) + return c +} +func (k chacha20key) Decrypter(iv []byte) cipher.Stream { + return k.Encrypter(iv) +} +func ChaCha20(key []byte) (Cipher, error) { + return chacha20key(key), nil +} diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 2950819af9..7d4f11aeee 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net" + "net/netip" "strconv" "github.com/Dreamacro/clash/component/auth" @@ -398,6 +399,21 @@ func ParseAddrToSocksAddr(addr net.Addr) Addr { return parsed } +func AddrFromStdAddrPort(addrPort netip.AddrPort) Addr { + addr := addrPort.Addr() + if addr.Is4() { + ip4 := addr.As4() + return []byte{AtypIPv4, ip4[0], ip4[1], ip4[2], ip4[3], byte(addrPort.Port() >> 8), byte(addrPort.Port())} + } + + buf := make([]byte, 1+net.IPv6len+2) + buf[0] = AtypIPv6 + copy(buf[1:], addr.AsSlice()) + buf[1+net.IPv6len] = byte(addrPort.Port() >> 8) + buf[1+net.IPv6len+1] = byte(addrPort.Port()) + return buf +} + // DecodeUDPPacket split `packet` to addr payload, and this function is mutable with `packet` func DecodeUDPPacket(packet []byte) (addr Addr, payload []byte, err error) { if len(packet) < 5 { diff --git a/transport/tuic/client.go b/transport/tuic/client.go new file mode 100644 index 0000000000..1d1c3d157d --- /dev/null +++ b/transport/tuic/client.go @@ -0,0 +1,420 @@ +package tuic + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "errors" + "math/rand" + "net" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/metacubex/quic-go" + + N "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" +) + +var ( + ClientClosed = errors.New("tuic: client closed") + TooManyOpenStreams = errors.New("tuic: too many open streams") +) + +type DialFunc func(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) + +type ClientOption struct { + TlsConfig *tls.Config + QuicConfig *quic.Config + Host string + Token [32]byte + UdpRelayMode string + CongestionController string + ReduceRtt bool + RequestTimeout time.Duration + MaxUdpRelayPacketSize int + FastOpen bool + MaxOpenStreams int64 +} + +type clientImpl struct { + *ClientOption + udp bool + + quicConn quic.Connection + connMutex sync.Mutex + + openStreams atomic.Int64 + closed atomic.Bool + + udpInputMap sync.Map + + // only ready for PoolClient + dialerRef C.Dialer + lastVisited time.Time +} + +func (t *clientImpl) getQuicConn(ctx context.Context, dialer C.Dialer, dialFn DialFunc) (quic.Connection, error) { + t.connMutex.Lock() + defer t.connMutex.Unlock() + if t.quicConn != nil { + return t.quicConn, nil + } + pc, addr, err := dialFn(ctx, dialer) + if err != nil { + return nil, err + } + var quicConn quic.Connection + if t.ReduceRtt { + quicConn, err = quic.DialEarlyContext(ctx, pc, addr, t.Host, t.TlsConfig, t.QuicConfig) + } else { + quicConn, err = quic.DialContext(ctx, pc, addr, t.Host, t.TlsConfig, t.QuicConfig) + } + if err != nil { + return nil, err + } + + SetCongestionController(quicConn, t.CongestionController) + + go func() { + _ = t.sendAuthentication(quicConn) + }() + + if t.udp { + go func() { + _ = t.parseUDP(quicConn) + }() + } + + t.quicConn = quicConn + t.openStreams.Store(0) + return quicConn, nil +} + +func (t *clientImpl) sendAuthentication(quicConn quic.Connection) (err error) { + defer func() { + t.deferQuicConn(quicConn, err) + }() + stream, err := quicConn.OpenUniStream() + if err != nil { + return err + } + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + err = NewAuthenticate(t.Token).WriteTo(buf) + if err != nil { + return err + } + _, err = buf.WriteTo(stream) + if err != nil { + return err + } + err = stream.Close() + if err != nil { + return + } + return nil +} + +func (t *clientImpl) parseUDP(quicConn quic.Connection) (err error) { + defer func() { + t.deferQuicConn(quicConn, err) + }() + switch t.UdpRelayMode { + case "quic": + for { + var stream quic.ReceiveStream + stream, err = quicConn.AcceptUniStream(context.Background()) + if err != nil { + return err + } + go func() (err error) { + var assocId uint32 + defer func() { + t.deferQuicConn(quicConn, err) + if err != nil && assocId != 0 { + if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { + if conn, ok := val.(net.Conn); ok { + _ = conn.Close() + } + } + } + stream.CancelRead(0) + }() + reader := bufio.NewReader(stream) + packet, err := ReadPacket(reader) + if err != nil { + return + } + assocId = packet.ASSOC_ID + if val, ok := t.udpInputMap.Load(assocId); ok { + if conn, ok := val.(net.Conn); ok { + writer := bufio.NewWriterSize(conn, packet.BytesLen()) + _ = packet.WriteTo(writer) + _ = writer.Flush() + } + } + return + }() + } + default: // native + for { + var message []byte + message, err = quicConn.ReceiveMessage() + if err != nil { + return err + } + go func() (err error) { + var assocId uint32 + defer func() { + t.deferQuicConn(quicConn, err) + if err != nil && assocId != 0 { + if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { + if conn, ok := val.(net.Conn); ok { + _ = conn.Close() + } + } + } + }() + buffer := bytes.NewBuffer(message) + packet, err := ReadPacket(buffer) + if err != nil { + return + } + assocId = packet.ASSOC_ID + if val, ok := t.udpInputMap.Load(assocId); ok { + if conn, ok := val.(net.Conn); ok { + _, _ = conn.Write(message) + } + } + return + }() + } + } +} + +func (t *clientImpl) deferQuicConn(quicConn quic.Connection, err error) { + var netError net.Error + if err != nil && errors.As(err, &netError) { + t.forceClose(quicConn, err) + } +} + +func (t *clientImpl) forceClose(quicConn quic.Connection, err error) { + t.connMutex.Lock() + defer t.connMutex.Unlock() + if quicConn == nil { + quicConn = t.quicConn + } + if quicConn != nil { + if quicConn == t.quicConn { + t.quicConn = nil + } + } + errStr := "" + if err != nil { + errStr = err.Error() + } + if quicConn != nil { + _ = quicConn.CloseWithError(ProtocolError, errStr) + } + udpInputMap := &t.udpInputMap + udpInputMap.Range(func(key, value any) bool { + if conn, ok := value.(net.Conn); ok { + _ = conn.Close() + } + udpInputMap.Delete(key) + return true + }) +} + +func (t *clientImpl) Close() { + t.closed.Store(true) + if t.openStreams.Load() == 0 { + t.forceClose(nil, ClientClosed) + } +} + +func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.Conn, error) { + quicConn, err := t.getQuicConn(ctx, dialer, dialFn) + if err != nil { + return nil, err + } + openStreams := t.openStreams.Add(1) + if openStreams >= t.MaxOpenStreams { + t.openStreams.Add(-1) + return nil, TooManyOpenStreams + } + stream, err := func() (stream *quicStreamConn, err error) { + defer func() { + t.deferQuicConn(quicConn, err) + }() + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + err = NewConnect(NewAddress(metadata)).WriteTo(buf) + if err != nil { + return nil, err + } + quicStream, err := quicConn.OpenStream() + if err != nil { + return nil, err + } + stream = &quicStreamConn{ + Stream: quicStream, + lAddr: quicConn.LocalAddr(), + rAddr: quicConn.RemoteAddr(), + closeDeferFn: func() { + time.AfterFunc(C.DefaultTCPTimeout, func() { + openStreams := t.openStreams.Add(-1) + if openStreams == 0 && t.closed.Load() { + t.forceClose(quicConn, ClientClosed) + } + }) + }, + } + _, err = buf.WriteTo(stream) + if err != nil { + _ = stream.Close() + return nil, err + } + return stream, err + }() + if err != nil { + return nil, err + } + + conn := &earlyConn{BufferedConn: N.NewBufferedConn(stream), RequestTimeout: t.RequestTimeout} + if !t.FastOpen { + err = conn.Response() + if err != nil { + return nil, err + } + } + return conn, nil +} + +type earlyConn struct { + *N.BufferedConn + resOnce sync.Once + resErr error + + RequestTimeout time.Duration +} + +func (conn *earlyConn) response() error { + if conn.RequestTimeout > 0 { + _ = conn.SetReadDeadline(time.Now().Add(conn.RequestTimeout)) + } + response, err := ReadResponse(conn) + if err != nil { + _ = conn.Close() + return err + } + if response.IsFailed() { + _ = conn.Close() + return errors.New("connect failed") + } + _ = conn.SetReadDeadline(time.Time{}) + return nil +} + +func (conn *earlyConn) Response() error { + conn.resOnce.Do(func() { + conn.resErr = conn.response() + }) + return conn.resErr +} + +func (conn *earlyConn) Read(b []byte) (n int, err error) { + err = conn.Response() + if err != nil { + return 0, err + } + return conn.BufferedConn.Read(b) +} + +func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { + quicConn, err := t.getQuicConn(ctx, dialer, dialFn) + if err != nil { + return nil, err + } + openStreams := t.openStreams.Add(1) + if openStreams >= t.MaxOpenStreams { + t.openStreams.Add(-1) + return nil, TooManyOpenStreams + } + + pipe1, pipe2 := net.Pipe() + var connId uint32 + for { + connId = rand.Uint32() + _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) + if !loaded { + break + } + } + pc := &quicStreamPacketConn{ + connId: connId, + quicConn: quicConn, + lAddr: quicConn.LocalAddr(), + inputConn: N.NewBufferedConn(pipe2), + udpRelayMode: t.UdpRelayMode, + maxUdpRelayPacketSize: t.MaxUdpRelayPacketSize, + deferQuicConnFn: t.deferQuicConn, + closeDeferFn: func() { + t.udpInputMap.Delete(connId) + time.AfterFunc(C.DefaultUDPTimeout, func() { + openStreams := t.openStreams.Add(-1) + if openStreams == 0 && t.closed.Load() { + t.forceClose(quicConn, ClientClosed) + } + }) + }, + } + return pc, nil +} + +type Client struct { + *clientImpl // use an independent pointer to let Finalizer can work no matter somewhere handle an influence in clientImpl inner +} + +func (t *Client) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.Conn, error) { + conn, err := t.clientImpl.DialContextWithDialer(ctx, metadata, dialer, dialFn) + if err != nil { + return nil, err + } + return N.NewRefConn(conn, t), err +} + +func (t *Client) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { + pc, err := t.clientImpl.ListenPacketWithDialer(ctx, metadata, dialer, dialFn) + if err != nil { + return nil, err + } + return N.NewRefPacketConn(pc, t), nil +} + +func (t *Client) forceClose() { + t.clientImpl.forceClose(nil, ClientClosed) +} + +func NewClient(clientOption *ClientOption, udp bool) *Client { + ci := &clientImpl{ + ClientOption: clientOption, + udp: udp, + } + c := &Client{ci} + runtime.SetFinalizer(c, closeClient) + log.Debugln("New Tuic Client at %p", c) + return c +} + +func closeClient(client *Client) { + log.Debugln("Close Tuic Client at %p", client) + client.forceClose() +} diff --git a/transport/tuic/congestion/bandwidth.go b/transport/tuic/congestion/bandwidth.go new file mode 100644 index 0000000000..2a6b3a2e0a --- /dev/null +++ b/transport/tuic/congestion/bandwidth.go @@ -0,0 +1,25 @@ +package congestion + +import ( + "math" + "time" + + "github.com/metacubex/quic-go/congestion" +) + +// Bandwidth of a connection +type Bandwidth uint64 + +const infBandwidth Bandwidth = math.MaxUint64 + +const ( + // BitsPerSecond is 1 bit per second + BitsPerSecond Bandwidth = 1 + // BytesPerSecond is 1 byte per second + BytesPerSecond = 8 * BitsPerSecond +) + +// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta +func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { + return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond +} diff --git a/transport/tuic/congestion/bandwidth_sampler.go b/transport/tuic/congestion/bandwidth_sampler.go new file mode 100644 index 0000000000..b82d391f8d --- /dev/null +++ b/transport/tuic/congestion/bandwidth_sampler.go @@ -0,0 +1,376 @@ +package congestion + +import ( + "math" + "time" + + "github.com/metacubex/quic-go/congestion" +) + +var ( + InfiniteBandwidth = Bandwidth(math.MaxUint64) +) + +// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned +// to the caller when the packet is acked or lost. +type SendTimeState struct { + // Whether other states in this object is valid. + isValid bool + // Whether the sender is app limited at the time the packet was sent. + // App limited bandwidth sample might be artificially low because the sender + // did not have enough data to send in order to saturate the link. + isAppLimited bool + // Total number of sent bytes at the time the packet was sent. + // Includes the packet itself. + totalBytesSent congestion.ByteCount + // Total number of acked bytes at the time the packet was sent. + totalBytesAcked congestion.ByteCount + // Total number of lost bytes at the time the packet was sent. + totalBytesLost congestion.ByteCount +} + +// ConnectionStateOnSentPacket represents the information about a sent packet +// and the state of the connection at the moment the packet was sent, +// specifically the information about the most recently acknowledged packet at +// that moment. +type ConnectionStateOnSentPacket struct { + packetNumber congestion.PacketNumber + // Time at which the packet is sent. + sendTime time.Time + // Size of the packet. + size congestion.ByteCount + // The value of |totalBytesSentAtLastAckedPacket| at the time the + // packet was sent. + totalBytesSentAtLastAckedPacket congestion.ByteCount + // The value of |lastAckedPacketSentTime| at the time the packet was + // sent. + lastAckedPacketSentTime time.Time + // The value of |lastAckedPacketAckTime| at the time the packet was + // sent. + lastAckedPacketAckTime time.Time + // Send time states that are returned to the congestion controller when the + // packet is acked or lost. + sendTimeState SendTimeState +} + +// BandwidthSample +type BandwidthSample struct { + // The bandwidth at that particular sample. Zero if no valid bandwidth sample + // is available. + bandwidth Bandwidth + // The RTT measurement at this particular sample. Zero if no RTT sample is + // available. Does not correct for delayed ack time. + rtt time.Duration + // States captured when the packet was sent. + stateAtSend SendTimeState +} + +func NewBandwidthSample() *BandwidthSample { + return &BandwidthSample{ + // FIXME: the default value of original code is zero. + rtt: InfiniteRTT, + } +} + +// BandwidthSampler keeps track of sent and acknowledged packets and outputs a +// bandwidth sample for every packet acknowledged. The samples are taken for +// individual packets, and are not filtered; the consumer has to filter the +// bandwidth samples itself. In certain cases, the sampler will locally severely +// underestimate the bandwidth, hence a maximum filter with a size of at least +// one RTT is recommended. +// +// This class bases its samples on the slope of two curves: the number of bytes +// sent over time, and the number of bytes acknowledged as received over time. +// It produces a sample of both slopes for every packet that gets acknowledged, +// based on a slope between two points on each of the corresponding curves. Note +// that due to the packet loss, the number of bytes on each curve might get +// further and further away from each other, meaning that it is not feasible to +// compare byte values coming from different curves with each other. +// +// The obvious points for measuring slope sample are the ones corresponding to +// the packet that was just acknowledged. Let us denote them as S_1 (point at +// which the current packet was sent) and A_1 (point at which the current packet +// was acknowledged). However, taking a slope requires two points on each line, +// so estimating bandwidth requires picking a packet in the past with respect to +// which the slope is measured. +// +// For that purpose, BandwidthSampler always keeps track of the most recently +// acknowledged packet, and records it together with every outgoing packet. +// When a packet gets acknowledged (A_1), it has not only information about when +// it itself was sent (S_1), but also the information about the latest +// acknowledged packet right before it was sent (S_0 and A_0). +// +// Based on that data, send and ack rate are estimated as: +// +// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) +// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) +// +// Here, the ack rate is intuitively the rate we want to treat as bandwidth. +// However, in certain cases (e.g. ack compression) the ack rate at a point may +// end up higher than the rate at which the data was originally sent, which is +// not indicative of the real bandwidth. Hence, we use the send rate as an upper +// bound, and the sample value is +// +// rate_sample = min(send_rate, ack_rate) +// +// An important edge case handled by the sampler is tracking the app-limited +// samples. There are multiple meaning of "app-limited" used interchangeably, +// hence it is important to understand and to be able to distinguish between +// them. +// +// Meaning 1: connection state. The connection is said to be app-limited when +// there is no outstanding data to send. This means that certain bandwidth +// samples in the future would not be an accurate indication of the link +// capacity, and it is important to inform consumer about that. Whenever +// connection becomes app-limited, the sampler is notified via OnAppLimited() +// method. +// +// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth +// sampler becomes notified about the connection being app-limited, it enters +// app-limited phase. In that phase, all *sent* packets are marked as +// app-limited. Note that the connection itself does not have to be +// app-limited during the app-limited phase, and in fact it will not be +// (otherwise how would it send packets?). The boolean flag below indicates +// whether the sampler is in that phase. +// +// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is +// sent during the app-limited phase, the resulting sample related to the +// packet will be marked as app-limited. +// +// With the terminology issue out of the way, let us consider the question of +// what kind of situation it addresses. +// +// Consider a scenario where we first send packets 1 to 20 at a regular +// bandwidth, and then immediately run out of data. After a few seconds, we send +// packets 21 to 60, and only receive ack for 21 between sending packets 40 and +// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 +// we use to compute the slope is going to be packet 20, a few seconds apart +// from the current packet, hence the resulting estimate would be extremely low +// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, +// meaning that the bandwidth sample would exclude the quiescence. +// +// Based on the analysis of that scenario, we implement the following rule: once +// OnAppLimited() is called, all sent packets will produce app-limited samples +// up until an ack for a packet that was sent after OnAppLimited() was called. +// Note that while the scenario above is not the only scenario when the +// connection is app-limited, the approach works in other cases too. +type BandwidthSampler struct { + // The total number of congestion controlled bytes sent during the connection. + totalBytesSent congestion.ByteCount + // The total number of congestion controlled bytes which were acknowledged. + totalBytesAcked congestion.ByteCount + // The total number of congestion controlled bytes which were lost. + totalBytesLost congestion.ByteCount + // The value of |totalBytesSent| at the time the last acknowledged packet + // was sent. Valid only when |lastAckedPacketSentTime| is valid. + totalBytesSentAtLastAckedPacket congestion.ByteCount + // The time at which the last acknowledged packet was sent. Set to + // QuicTime::Zero() if no valid timestamp is available. + lastAckedPacketSentTime time.Time + // The time at which the most recent packet was acknowledged. + lastAckedPacketAckTime time.Time + // The most recently sent packet. + lastSendPacket congestion.PacketNumber + // Indicates whether the bandwidth sampler is currently in an app-limited + // phase. + isAppLimited bool + // The packet that will be acknowledged after this one will cause the sampler + // to exit the app-limited phase. + endOfAppLimitedPhase congestion.PacketNumber + // Record of the connection state at the point where each packet in flight was + // sent, indexed by the packet number. + connectionStats *ConnectionStates +} + +func NewBandwidthSampler() *BandwidthSampler { + return &BandwidthSampler{ + connectionStats: &ConnectionStates{ + stats: make(map[congestion.PacketNumber]*ConnectionStateOnSentPacket), + }, + } +} + +// OnPacketSent Inputs the sent packet information into the sampler. Assumes that all +// packets are sent in order. The information about the packet will not be +// released from the sampler until it the packet is either acknowledged or +// declared lost. +func (s *BandwidthSampler) OnPacketSent(sentTime time.Time, lastSentPacket congestion.PacketNumber, sentBytes, bytesInFlight congestion.ByteCount, hasRetransmittableData bool) { + s.lastSendPacket = lastSentPacket + + if !hasRetransmittableData { + return + } + + s.totalBytesSent += sentBytes + + // If there are no packets in flight, the time at which the new transmission + // opens can be treated as the A_0 point for the purpose of bandwidth + // sampling. This underestimates bandwidth to some extent, and produces some + // artificially low samples for most packets in flight, but it provides with + // samples at important points where we would not have them otherwise, most + // importantly at the beginning of the connection. + if bytesInFlight == 0 { + s.lastAckedPacketAckTime = sentTime + s.totalBytesSentAtLastAckedPacket = s.totalBytesSent + + // In this situation ack compression is not a concern, set send rate to + // effectively infinite. + s.lastAckedPacketSentTime = sentTime + } + + s.connectionStats.Insert(lastSentPacket, sentTime, sentBytes, s) +} + +// OnPacketAcked Notifies the sampler that the |lastAckedPacket| is acknowledged. Returns a +// bandwidth sample. If no bandwidth sample is available, +// QuicBandwidth::Zero() is returned. +func (s *BandwidthSampler) OnPacketAcked(ackTime time.Time, lastAckedPacket congestion.PacketNumber) *BandwidthSample { + sentPacketState := s.connectionStats.Get(lastAckedPacket) + if sentPacketState == nil { + return NewBandwidthSample() + } + + sample := s.onPacketAckedInner(ackTime, lastAckedPacket, sentPacketState) + s.connectionStats.Remove(lastAckedPacket) + + return sample +} + +// onPacketAckedInner Handles the actual bandwidth calculations, whereas the outer method handles +// retrieving and removing |sentPacket|. +func (s *BandwidthSampler) onPacketAckedInner(ackTime time.Time, lastAckedPacket congestion.PacketNumber, sentPacket *ConnectionStateOnSentPacket) *BandwidthSample { + s.totalBytesAcked += sentPacket.size + + s.totalBytesSentAtLastAckedPacket = sentPacket.sendTimeState.totalBytesSent + s.lastAckedPacketSentTime = sentPacket.sendTime + s.lastAckedPacketAckTime = ackTime + + // Exit app-limited phase once a packet that was sent while the connection is + // not app-limited is acknowledged. + if s.isAppLimited && lastAckedPacket > s.endOfAppLimitedPhase { + s.isAppLimited = false + } + + // There might have been no packets acknowledged at the moment when the + // current packet was sent. In that case, there is no bandwidth sample to + // make. + if sentPacket.lastAckedPacketSentTime.IsZero() { + return NewBandwidthSample() + } + + // Infinite rate indicates that the sampler is supposed to discard the + // current send rate sample and use only the ack rate. + sendRate := InfiniteBandwidth + if sentPacket.sendTime.After(sentPacket.lastAckedPacketSentTime) { + sendRate = BandwidthFromDelta(sentPacket.sendTimeState.totalBytesSent-sentPacket.totalBytesSentAtLastAckedPacket, sentPacket.sendTime.Sub(sentPacket.lastAckedPacketSentTime)) + } + + // During the slope calculation, ensure that ack time of the current packet is + // always larger than the time of the previous packet, otherwise division by + // zero or integer underflow can occur. + if !ackTime.After(sentPacket.lastAckedPacketAckTime) { + // TODO(wub): Compare this code count before and after fixing clock jitter + // issue. + // if sentPacket.lastAckedPacketAckTime.Equal(sentPacket.sendTime) { + // This is the 1st packet after quiescense. + // QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 1, 2); + // } else { + // QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 2, 2); + // } + + return NewBandwidthSample() + } + + ackRate := BandwidthFromDelta(s.totalBytesAcked-sentPacket.sendTimeState.totalBytesAcked, + ackTime.Sub(sentPacket.lastAckedPacketAckTime)) + + // Note: this sample does not account for delayed acknowledgement time. This + // means that the RTT measurements here can be artificially high, especially + // on low bandwidth connections. + sample := &BandwidthSample{ + bandwidth: minBandwidth(sendRate, ackRate), + rtt: ackTime.Sub(sentPacket.sendTime), + } + + SentPacketToSendTimeState(sentPacket, &sample.stateAtSend) + return sample +} + +// OnPacketLost Informs the sampler that a packet is considered lost and it should no +// longer keep track of it. +func (s *BandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber) SendTimeState { + ok, sentPacket := s.connectionStats.Remove(packetNumber) + sendTimeState := SendTimeState{ + isValid: ok, + } + if sentPacket != nil { + s.totalBytesLost += sentPacket.size + SentPacketToSendTimeState(sentPacket, &sendTimeState) + } + + return sendTimeState +} + +// OnAppLimited Informs the sampler that the connection is currently app-limited, causing +// the sampler to enter the app-limited phase. The phase will expire by +// itself. +func (s *BandwidthSampler) OnAppLimited() { + s.isAppLimited = true + s.endOfAppLimitedPhase = s.lastSendPacket +} + +// SentPacketToSendTimeState Copy a subset of the (private) ConnectionStateOnSentPacket to the (public) +// SendTimeState. Always set send_time_state->is_valid to true. +func SentPacketToSendTimeState(sentPacket *ConnectionStateOnSentPacket, sendTimeState *SendTimeState) { + sendTimeState.isAppLimited = sentPacket.sendTimeState.isAppLimited + sendTimeState.totalBytesSent = sentPacket.sendTimeState.totalBytesSent + sendTimeState.totalBytesAcked = sentPacket.sendTimeState.totalBytesAcked + sendTimeState.totalBytesLost = sentPacket.sendTimeState.totalBytesLost + sendTimeState.isValid = true +} + +// ConnectionStates Record of the connection state at the point where each packet in flight was +// sent, indexed by the packet number. +// FIXME: using LinkedList replace map to fast remove all the packets lower than the specified packet number. +type ConnectionStates struct { + stats map[congestion.PacketNumber]*ConnectionStateOnSentPacket +} + +func (s *ConnectionStates) Insert(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) bool { + if _, ok := s.stats[packetNumber]; ok { + return false + } + + s.stats[packetNumber] = NewConnectionStateOnSentPacket(packetNumber, sentTime, bytes, sampler) + return true +} + +func (s *ConnectionStates) Get(packetNumber congestion.PacketNumber) *ConnectionStateOnSentPacket { + return s.stats[packetNumber] +} + +func (s *ConnectionStates) Remove(packetNumber congestion.PacketNumber) (bool, *ConnectionStateOnSentPacket) { + state, ok := s.stats[packetNumber] + if ok { + delete(s.stats, packetNumber) + } + return ok, state +} + +func NewConnectionStateOnSentPacket(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) *ConnectionStateOnSentPacket { + return &ConnectionStateOnSentPacket{ + packetNumber: packetNumber, + sendTime: sentTime, + size: bytes, + lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, + lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, + totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, + sendTimeState: SendTimeState{ + isValid: true, + isAppLimited: sampler.isAppLimited, + totalBytesSent: sampler.totalBytesSent, + totalBytesAcked: sampler.totalBytesAcked, + totalBytesLost: sampler.totalBytesLost, + }, + } +} diff --git a/transport/tuic/congestion/bbr_sender.go b/transport/tuic/congestion/bbr_sender.go new file mode 100644 index 0000000000..d4ba20d1e6 --- /dev/null +++ b/transport/tuic/congestion/bbr_sender.go @@ -0,0 +1,992 @@ +package congestion + +// src from https://quiche.googlesource.com/quiche.git/+/66dea072431f94095dfc3dd2743cb94ef365f7ef/quic/core/congestion_control/bbr_sender.cc + +import ( + "fmt" + "math" + "math/rand" + "net" + "time" + + "github.com/metacubex/quic-go/congestion" +) + +const DefaultTCPMSS congestion.ByteCount = 1460 +const DefaultBBRMaxCongestionWindow congestion.ByteCount = 2000 * DefaultTCPMSS +const InitialCongestionWindow congestion.ByteCount = 32 * DefaultTCPMSS +const MinInitialPacketSize = 1200 +const InitialPacketSizeIPv4 = 1252 +const InitialPacketSizeIPv6 = 1232 + +func GetMaxPacketSize(addr net.Addr) congestion.ByteCount { + maxSize := congestion.ByteCount(MinInitialPacketSize) + // If this is not a UDP address, we don't know anything about the MTU. + // Use the minimum size of an Initial packet as the max packet size. + if udpAddr, ok := addr.(*net.UDPAddr); ok { + if udpAddr.IP.To4() != nil { + maxSize = InitialPacketSizeIPv4 + } else { + maxSize = InitialPacketSizeIPv6 + } + } + return maxSize +} + +var ( + // The maximum outgoing packet size allowed. + // The maximum packet size of any QUIC packet over IPv6, based on ethernet's max + // size, minus the IP and UDP headers. IPv6 has a 40 byte header, UDP adds an + // additional 8 bytes. This is a total overhead of 48 bytes. Ethernet's + // max packet size is 1500 bytes, 1500 - 48 = 1452. + MaxOutgoingPacketSize = congestion.ByteCount(1452) + + // Default maximum packet size used in the Linux TCP implementation. + // Used in QUIC for congestion window computations in bytes. + MaxSegmentSize = DefaultTCPMSS + + // Default initial rtt used before any samples are received. + InitialRtt = 100 * time.Millisecond + + // Constants based on TCP defaults. + // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. + // Does not inflate the pacing rate. + DefaultMinimumCongestionWindow = 4 * DefaultTCPMSS + + // The gain used for the STARTUP, equal to 2/ln(2). + DefaultHighGain = 2.885 + + // The gain used in STARTUP after loss has been detected. + // 1.5 is enough to allow for 25% exogenous loss and still observe a 25% growth + // in measured bandwidth. + StartupAfterLossGain = 1.5 + + // The cycle of gains used during the PROBE_BW stage. + PacingGain = []float64{1.25, 0.75, 1, 1, 1, 1, 1, 1} + + // The length of the gain cycle. + GainCycleLength = len(PacingGain) + + // The size of the bandwidth filter window, in round-trips. + BandwidthWindowSize = GainCycleLength + 2 + + // The time after which the current min_rtt value expires. + MinRttExpiry = 10 * time.Second + + // The minimum time the connection can spend in PROBE_RTT mode. + ProbeRttTime = time.Millisecond * 200 + + // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| + // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection + // will exit the STARTUP mode. + StartupGrowthTarget = 1.25 + RoundTripsWithoutGrowthBeforeExitingStartup = int64(3) + + // Coefficient of target congestion window to use when basing PROBE_RTT on BDP. + ModerateProbeRttMultiplier = 0.75 + + // Coefficient to determine if a new RTT is sufficiently similar to min_rtt that + // we don't need to enter PROBE_RTT. + SimilarMinRttThreshold = 1.125 + + // Congestion window gain for QUIC BBR during PROBE_BW phase. + DefaultCongestionWindowGainConst = 2.0 +) + +type bbrMode int + +const ( + // Startup phase of the connection. + STARTUP = iota + // After achieving the highest possible bandwidth during the startup, lower + // the pacing rate in order to drain the queue. + DRAIN + // Cruising mode. + PROBE_BW + // Temporarily slow down sending in order to empty the buffer and measure + // the real minimum RTT. + PROBE_RTT +) + +type bbrRecoveryState int + +const ( + // Do not limit. + NOT_IN_RECOVERY = iota + + // Allow an extra outstanding byte for each byte acknowledged. + CONSERVATION + + // Allow two extra outstanding bytes for each byte acknowledged (slow + // start). + GROWTH +) + +type bbrSender struct { + mode bbrMode + clock Clock + rttStats congestion.RTTStatsProvider + bytesInFlight congestion.ByteCount + // return total bytes of unacked packets. + //GetBytesInFlight func() congestion.ByteCount + // Bandwidth sampler provides BBR with the bandwidth measurements at + // individual points. + sampler *BandwidthSampler + // The number of the round trips that have occurred during the connection. + roundTripCount int64 + // The packet number of the most recently sent packet. + lastSendPacket congestion.PacketNumber + // Acknowledgement of any packet after |current_round_trip_end_| will cause + // the round trip counter to advance. + currentRoundTripEnd congestion.PacketNumber + // The filter that tracks the maximum bandwidth over the multiple recent + // round-trips. + maxBandwidth *WindowedFilter + // Tracks the maximum number of bytes acked faster than the sending rate. + maxAckHeight *WindowedFilter + // The time this aggregation started and the number of bytes acked during it. + aggregationEpochStartTime time.Time + aggregationEpochBytes congestion.ByteCount + // Minimum RTT estimate. Automatically expires within 10 seconds (and + // triggers PROBE_RTT mode) if no new value is sampled during that period. + minRtt time.Duration + // The time at which the current value of |min_rtt_| was assigned. + minRttTimestamp time.Time + // The maximum allowed number of bytes in flight. + congestionWindow congestion.ByteCount + // The initial value of the |congestion_window_|. + initialCongestionWindow congestion.ByteCount + // The largest value the |congestion_window_| can achieve. + maxCongestionWindow congestion.ByteCount + // The smallest value the |congestion_window_| can achieve. + minCongestionWindow congestion.ByteCount + // The pacing gain applied during the STARTUP phase. + highGain float64 + // The CWND gain applied during the STARTUP phase. + highCwndGain float64 + // The pacing gain applied during the DRAIN phase. + drainGain float64 + // The current pacing rate of the connection. + pacingRate Bandwidth + // The gain currently applied to the pacing rate. + pacingGain float64 + // The gain currently applied to the congestion window. + congestionWindowGain float64 + // The gain used for the congestion window during PROBE_BW. Latched from + // quic_bbr_cwnd_gain flag. + congestionWindowGainConst float64 + // The number of RTTs to stay in STARTUP mode. Defaults to 3. + numStartupRtts int64 + // If true, exit startup if 1RTT has passed with no bandwidth increase and + // the connection is in recovery. + exitStartupOnLoss bool + // Number of round-trips in PROBE_BW mode, used for determining the current + // pacing gain cycle. + cycleCurrentOffset int + // The time at which the last pacing gain cycle was started. + lastCycleStart time.Time + // Indicates whether the connection has reached the full bandwidth mode. + isAtFullBandwidth bool + // Number of rounds during which there was no significant bandwidth increase. + roundsWithoutBandwidthGain int64 + // The bandwidth compared to which the increase is measured. + bandwidthAtLastRound Bandwidth + // Set to true upon exiting quiescence. + exitingQuiescence bool + // Time at which PROBE_RTT has to be exited. Setting it to zero indicates + // that the time is yet unknown as the number of packets in flight has not + // reached the required value. + exitProbeRttAt time.Time + // Indicates whether a round-trip has passed since PROBE_RTT became active. + probeRttRoundPassed bool + // Indicates whether the most recent bandwidth sample was marked as + // app-limited. + lastSampleIsAppLimited bool + // Indicates whether any non app-limited samples have been recorded. + hasNoAppLimitedSample bool + // Indicates app-limited calls should be ignored as long as there's + // enough data inflight to see more bandwidth when necessary. + flexibleAppLimited bool + // Current state of recovery. + recoveryState bbrRecoveryState + // Receiving acknowledgement of a packet after |end_recovery_at_| will cause + // BBR to exit the recovery mode. A value above zero indicates at least one + // loss has been detected, so it must not be set back to zero. + endRecoveryAt congestion.PacketNumber + // A window used to limit the number of bytes in flight during loss recovery. + recoveryWindow congestion.ByteCount + // If true, consider all samples in recovery app-limited. + isAppLimitedRecovery bool + // When true, pace at 1.5x and disable packet conservation in STARTUP. + slowerStartup bool + // When true, disables packet conservation in STARTUP. + rateBasedStartup bool + // When non-zero, decreases the rate in STARTUP by the total number of bytes + // lost in STARTUP divided by CWND. + startupRateReductionMultiplier int64 + // Sum of bytes lost in STARTUP. + startupBytesLost congestion.ByteCount + // When true, add the most recent ack aggregation measurement during STARTUP. + enableAckAggregationDuringStartup bool + // When true, expire the windowed ack aggregation values in STARTUP when + // bandwidth increases more than 25%. + expireAckAggregationInStartup bool + // If true, will not exit low gain mode until bytes_in_flight drops below BDP + // or it's time for high gain mode. + drainToTarget bool + // If true, use a CWND of 0.75*BDP during probe_rtt instead of 4 packets. + probeRttBasedOnBdp bool + // If true, skip probe_rtt and update the timestamp of the existing min_rtt to + // now if min_rtt over the last cycle is within 12.5% of the current min_rtt. + // Even if the min_rtt is 12.5% too low, the 25% gain cycling and 2x CWND gain + // should overcome an overly small min_rtt. + probeRttSkippedIfSimilarRtt bool + // If true, disable PROBE_RTT entirely as long as the connection was recently + // app limited. + probeRttDisabledIfAppLimited bool + appLimitedSinceLastProbeRtt bool + minRttSinceLastProbeRtt time.Duration + // Latched value of --quic_always_get_bw_sample_when_acked. + alwaysGetBwSampleWhenAcked bool + + pacer *pacer + maxDatagramSize congestion.ByteCount +} + +func NewBBRSender(clock Clock, initialMaxDatagramSize, initialCongestionWindow, maxCongestionWindow congestion.ByteCount) *bbrSender { + b := &bbrSender{ + mode: STARTUP, + clock: clock, + sampler: NewBandwidthSampler(), + maxBandwidth: NewWindowedFilter(int64(BandwidthWindowSize), MaxFilter), + maxAckHeight: NewWindowedFilter(int64(BandwidthWindowSize), MaxFilter), + congestionWindow: initialCongestionWindow, + initialCongestionWindow: initialCongestionWindow, + maxCongestionWindow: maxCongestionWindow, + minCongestionWindow: DefaultMinimumCongestionWindow, + highGain: DefaultHighGain, + highCwndGain: DefaultHighGain, + drainGain: 1.0 / DefaultHighGain, + pacingGain: 1.0, + congestionWindowGain: 1.0, + congestionWindowGainConst: DefaultCongestionWindowGainConst, + numStartupRtts: RoundTripsWithoutGrowthBeforeExitingStartup, + recoveryState: NOT_IN_RECOVERY, + recoveryWindow: maxCongestionWindow, + minRttSinceLastProbeRtt: InfiniteRTT, + maxDatagramSize: initialMaxDatagramSize, + } + b.pacer = newPacer(b.BandwidthEstimate) + return b +} + +func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { + b.rttStats = provider +} + +func (b *bbrSender) GetBytesInFlight() congestion.ByteCount { + return b.bytesInFlight +} + +// TimeUntilSend returns when the next packet should be sent. +func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { + b.bytesInFlight = bytesInFlight + return b.pacer.TimeUntilSend() +} + +func (b *bbrSender) HasPacingBudget() bool { + return b.pacer.Budget(b.clock.Now()) >= b.maxDatagramSize +} + +func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { + if s < b.maxDatagramSize { + panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) + } + cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow + b.maxDatagramSize = s + if cwndIsMinCwnd { + b.congestionWindow = b.minCongestionWindow + } + b.pacer.SetMaxDatagramSize(s) +} + +func (b *bbrSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) { + b.pacer.SentPacket(sentTime, bytes) + b.lastSendPacket = packetNumber + + b.bytesInFlight = bytesInFlight + if bytesInFlight == 0 && b.sampler.isAppLimited { + b.exitingQuiescence = true + } + + if b.aggregationEpochStartTime.IsZero() { + b.aggregationEpochStartTime = sentTime + } + + b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) +} + +func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { + b.bytesInFlight = bytesInFlight + return bytesInFlight < b.GetCongestionWindow() +} + +func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { + if b.mode == PROBE_RTT { + return b.ProbeRttCongestionWindow() + } + + if b.InRecovery() && !(b.rateBasedStartup && b.mode == STARTUP) { + return minByteCount(b.congestionWindow, b.recoveryWindow) + } + + return b.congestionWindow +} + +func (b *bbrSender) MaybeExitSlowStart() { + +} + +func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime time.Time) { + totalBytesAckedBefore := b.sampler.totalBytesAcked + isRoundStart, minRttExpired := false, false + lastAckedPacket := number + + isRoundStart = b.UpdateRoundTripCounter(lastAckedPacket) + minRttExpired = b.UpdateBandwidthAndMinRtt(eventTime, number, ackedBytes) + b.UpdateRecoveryState(false, isRoundStart) + bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore + excessAcked := b.UpdateAckAggregationBytes(eventTime, bytesAcked) + + // Handle logic specific to STARTUP and DRAIN modes. + if isRoundStart && !b.isAtFullBandwidth { + b.CheckIfFullBandwidthReached() + } + b.MaybeExitStartupOrDrain(eventTime) + + // Handle logic specific to PROBE_RTT. + b.MaybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) + + // After the model is updated, recalculate the pacing rate and congestion + // window. + b.CalculatePacingRate() + b.CalculateCongestionWindow(bytesAcked, excessAcked) + b.CalculateRecoveryWindow(bytesAcked, congestion.ByteCount(0)) +} + +func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount, priorInFlight congestion.ByteCount) { + eventTime := time.Now() + totalBytesAckedBefore := b.sampler.totalBytesAcked + isRoundStart, minRttExpired := false, false + + b.DiscardLostPackets(number, lostBytes) + + // Input the new data into the BBR model of the connection. + var excessAcked congestion.ByteCount + + // Handle logic specific to PROBE_BW mode. + if b.mode == PROBE_BW { + b.UpdateGainCyclePhase(time.Now(), priorInFlight, true) + } + + // Handle logic specific to STARTUP and DRAIN modes. + b.MaybeExitStartupOrDrain(eventTime) + + // Handle logic specific to PROBE_RTT. + b.MaybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) + + // Calculate number of packets acked and lost. + bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore + bytesLost := lostBytes + + // After the model is updated, recalculate the pacing rate and congestion + // window. + b.CalculatePacingRate() + b.CalculateCongestionWindow(bytesAcked, excessAcked) + b.CalculateRecoveryWindow(bytesAcked, bytesLost) +} + +//func (b *bbrSender) OnCongestionEvent(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets, lostPackets []*congestion.Packet) { +// totalBytesAckedBefore := b.sampler.totalBytesAcked +// isRoundStart, minRttExpired := false, false +// +// if lostPackets != nil { +// b.DiscardLostPackets(lostPackets) +// } +// +// // Input the new data into the BBR model of the connection. +// var excessAcked congestion.ByteCount +// if len(ackedPackets) > 0 { +// lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber +// isRoundStart = b.UpdateRoundTripCounter(lastAckedPacket) +// minRttExpired = b.UpdateBandwidthAndMinRtt(eventTime, ackedPackets) +// b.UpdateRecoveryState(lastAckedPacket, len(lostPackets) > 0, isRoundStart) +// bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore +// excessAcked = b.UpdateAckAggregationBytes(eventTime, bytesAcked) +// } +// +// // Handle logic specific to PROBE_BW mode. +// if b.mode == PROBE_BW { +// b.UpdateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) > 0) +// } +// +// // Handle logic specific to STARTUP and DRAIN modes. +// if isRoundStart && !b.isAtFullBandwidth { +// b.CheckIfFullBandwidthReached() +// } +// b.MaybeExitStartupOrDrain(eventTime) +// +// // Handle logic specific to PROBE_RTT. +// b.MaybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) +// +// // Calculate number of packets acked and lost. +// bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore +// bytesLost := congestion.ByteCount(0) +// for _, packet := range lostPackets { +// bytesLost += packet.Length +// } +// +// // After the model is updated, recalculate the pacing rate and congestion +// // window. +// b.CalculatePacingRate() +// b.CalculateCongestionWindow(bytesAcked, excessAcked) +// b.CalculateRecoveryWindow(bytesAcked, bytesLost) +//} + +//func (b *bbrSender) SetNumEmulatedConnections(n int) { +// +//} + +func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { + +} + +//func (b *bbrSender) OnConnectionMigration() { +// +//} + +//// Experiments +//func (b *bbrSender) SetSlowStartLargeReduction(enabled bool) { +// +//} + +func (b *bbrSender) BandwidthEstimate() Bandwidth { + return Bandwidth(b.maxBandwidth.GetBest()) +} + +//func (b *bbrSender) HybridSlowStart() *HybridSlowStart { +// return nil +//} + +//func (b *bbrSender) SlowstartThreshold() congestion.ByteCount { +// return 0 +//} + +//func (b *bbrSender) RenoBeta() float32 { +// return 0.0 +//} + +func (b *bbrSender) InRecovery() bool { + return b.recoveryState != NOT_IN_RECOVERY +} + +func (b *bbrSender) InSlowStart() bool { + return b.mode == STARTUP +} + +//func (b *bbrSender) ShouldSendProbingPacket() bool { +// if b.pacingGain <= 1 { +// return false +// } +// // TODO(b/77975811): If the pipe is highly under-utilized, consider not +// // sending a probing transmission, because the extra bandwidth is not needed. +// // If flexible_app_limited is enabled, check if the pipe is sufficiently full. +// if b.flexibleAppLimited { +// return !b.IsPipeSufficientlyFull() +// } else { +// return true +// } +//} + +//func (b *bbrSender) IsPipeSufficientlyFull() bool { +// // See if we need more bytes in flight to see more bandwidth. +// if b.mode == STARTUP { +// // STARTUP exits if it doesn't observe a 25% bandwidth increase, so the CWND +// // must be more than 25% above the target. +// return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(1.5) +// } +// if b.pacingGain > 1 { +// // Super-unity PROBE_BW doesn't exit until 1.25 * BDP is achieved. +// return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(b.pacingGain) +// } +// // If bytes_in_flight are above the target congestion window, it should be +// // possible to observe the same or more bandwidth if it's available. +// return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(1.1) +//} + +//func (b *bbrSender) SetFromConfig() { +// // TODO: not impl. +//} + +func (b *bbrSender) UpdateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { + if b.currentRoundTripEnd == 0 || lastAckedPacket > b.currentRoundTripEnd { + b.currentRoundTripEnd = lastAckedPacket + b.roundTripCount++ + // if b.rttStats != nil && b.InSlowStart() { + // TODO: ++stats_->slowstart_num_rtts; + // } + return true + } + return false +} + +func (b *bbrSender) UpdateBandwidthAndMinRtt(now time.Time, number congestion.PacketNumber, ackedBytes congestion.ByteCount) bool { + sampleMinRtt := InfiniteRTT + + if !b.alwaysGetBwSampleWhenAcked && ackedBytes == 0 { + // Skip acked packets with 0 in flight bytes when updating bandwidth. + return false + } + bandwidthSample := b.sampler.OnPacketAcked(now, number) + if b.alwaysGetBwSampleWhenAcked && !bandwidthSample.stateAtSend.isValid { + // From the sampler's perspective, the packet has never been sent, or the + // packet has been acked or marked as lost previously. + return false + } + b.lastSampleIsAppLimited = bandwidthSample.stateAtSend.isAppLimited + // has_non_app_limited_sample_ |= + // !bandwidth_sample.state_at_send.is_app_limited; + if !bandwidthSample.stateAtSend.isAppLimited { + b.hasNoAppLimitedSample = true + } + if bandwidthSample.rtt > 0 { + sampleMinRtt = minRtt(sampleMinRtt, bandwidthSample.rtt) + } + if !bandwidthSample.stateAtSend.isAppLimited || bandwidthSample.bandwidth > b.BandwidthEstimate() { + b.maxBandwidth.Update(int64(bandwidthSample.bandwidth), b.roundTripCount) + } + + // If none of the RTT samples are valid, return immediately. + if sampleMinRtt == InfiniteRTT { + return false + } + + b.minRttSinceLastProbeRtt = minRtt(b.minRttSinceLastProbeRtt, sampleMinRtt) + // Do not expire min_rtt if none was ever available. + minRttExpired := b.minRtt > 0 && (now.After(b.minRttTimestamp.Add(MinRttExpiry))) + if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { + if minRttExpired && b.ShouldExtendMinRttExpiry() { + minRttExpired = false + } else { + b.minRtt = sampleMinRtt + } + b.minRttTimestamp = now + // Reset since_last_probe_rtt fields. + b.minRttSinceLastProbeRtt = InfiniteRTT + b.appLimitedSinceLastProbeRtt = false + } + + return minRttExpired +} + +func (b *bbrSender) ShouldExtendMinRttExpiry() bool { + if b.probeRttDisabledIfAppLimited && b.appLimitedSinceLastProbeRtt { + // Extend the current min_rtt if we've been app limited recently. + return true + } + + minRttIncreasedSinceLastProbe := b.minRttSinceLastProbeRtt > time.Duration(float64(b.minRtt)*SimilarMinRttThreshold) + if b.probeRttSkippedIfSimilarRtt && b.appLimitedSinceLastProbeRtt && !minRttIncreasedSinceLastProbe { + // Extend the current min_rtt if we've been app limited recently and an rtt + // has been measured in that time that's less than 12.5% more than the + // current min_rtt. + return true + } + + return false +} + +func (b *bbrSender) DiscardLostPackets(number congestion.PacketNumber, lostBytes congestion.ByteCount) { + b.sampler.OnPacketLost(number) + if b.mode == STARTUP { + // if b.rttStats != nil { + // TODO: slow start. + // } + if b.startupRateReductionMultiplier != 0 { + b.startupBytesLost += lostBytes + } + } +} + +func (b *bbrSender) UpdateRecoveryState(hasLosses, isRoundStart bool) { + // Exit recovery when there are no losses for a round. + if !hasLosses { + b.endRecoveryAt = b.lastSendPacket + } + switch b.recoveryState { + case NOT_IN_RECOVERY: + // Enter conservation on the first loss. + if hasLosses { + b.recoveryState = CONSERVATION + // This will cause the |recovery_window_| to be set to the correct + // value in CalculateRecoveryWindow(). + b.recoveryWindow = 0 + // Since the conservation phase is meant to be lasting for a whole + // round, extend the current round as if it were started right now. + b.currentRoundTripEnd = b.lastSendPacket + if false && b.lastSampleIsAppLimited { + b.isAppLimitedRecovery = true + } + } + case CONSERVATION: + if isRoundStart { + b.recoveryState = GROWTH + } + fallthrough + case GROWTH: + // Exit recovery if appropriate. + if !hasLosses && b.lastSendPacket > b.endRecoveryAt { + b.recoveryState = NOT_IN_RECOVERY + b.isAppLimitedRecovery = false + } + } + + if b.recoveryState != NOT_IN_RECOVERY && b.isAppLimitedRecovery { + b.sampler.OnAppLimited() + } +} + +func (b *bbrSender) UpdateAckAggregationBytes(ackTime time.Time, ackedBytes congestion.ByteCount) congestion.ByteCount { + // Compute how many bytes are expected to be delivered, assuming max bandwidth + // is correct. + expectedAckedBytes := congestion.ByteCount(b.maxBandwidth.GetBest()) * + congestion.ByteCount((ackTime.Sub(b.aggregationEpochStartTime))) + // Reset the current aggregation epoch as soon as the ack arrival rate is less + // than or equal to the max bandwidth. + if b.aggregationEpochBytes <= expectedAckedBytes { + // Reset to start measuring a new aggregation epoch. + b.aggregationEpochBytes = ackedBytes + b.aggregationEpochStartTime = ackTime + return 0 + } + // Compute how many extra bytes were delivered vs max bandwidth. + // Include the bytes most recently acknowledged to account for stretch acks. + b.aggregationEpochBytes += ackedBytes + b.maxAckHeight.Update(int64(b.aggregationEpochBytes-expectedAckedBytes), b.roundTripCount) + return b.aggregationEpochBytes - expectedAckedBytes +} + +func (b *bbrSender) UpdateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) { + bytesInFlight := b.GetBytesInFlight() + // In most cases, the cycle is advanced after an RTT passes. + shouldAdvanceGainCycling := now.Sub(b.lastCycleStart) > b.GetMinRtt() + + // If the pacing gain is above 1.0, the connection is trying to probe the + // bandwidth by increasing the number of bytes in flight to at least + // pacing_gain * BDP. Make sure that it actually reaches the target, as long + // as there are no losses suggesting that the buffers are not able to hold + // that much. + if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.GetTargetCongestionWindow(b.pacingGain) { + shouldAdvanceGainCycling = false + } + // If pacing gain is below 1.0, the connection is trying to drain the extra + // queue which could have been incurred by probing prior to it. If the number + // of bytes in flight falls down to the estimated BDP value earlier, conclude + // that the queue has been successfully drained and exit this cycle early. + if b.pacingGain < 1.0 && bytesInFlight <= b.GetTargetCongestionWindow(1.0) { + shouldAdvanceGainCycling = true + } + + if shouldAdvanceGainCycling { + b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % GainCycleLength + b.lastCycleStart = now + // Stay in low gain mode until the target BDP is hit. + // Low gain mode will be exited immediately when the target BDP is achieved. + if b.drainToTarget && b.pacingGain < 1.0 && PacingGain[b.cycleCurrentOffset] == 1.0 && + bytesInFlight > b.GetTargetCongestionWindow(1.0) { + return + } + b.pacingGain = PacingGain[b.cycleCurrentOffset] + } +} + +func (b *bbrSender) GetTargetCongestionWindow(gain float64) congestion.ByteCount { + bdp := congestion.ByteCount(b.GetMinRtt()) * congestion.ByteCount(b.BandwidthEstimate()) + congestionWindow := congestion.ByteCount(gain * float64(bdp)) + + // BDP estimate will be zero if no bandwidth samples are available yet. + if congestionWindow == 0 { + congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) + } + + return maxByteCount(congestionWindow, b.minCongestionWindow) +} + +func (b *bbrSender) CheckIfFullBandwidthReached() { + if b.lastSampleIsAppLimited { + return + } + + target := Bandwidth(float64(b.bandwidthAtLastRound) * StartupGrowthTarget) + if b.BandwidthEstimate() >= target { + b.bandwidthAtLastRound = b.BandwidthEstimate() + b.roundsWithoutBandwidthGain = 0 + if b.expireAckAggregationInStartup { + // Expire old excess delivery measurements now that bandwidth increased. + b.maxAckHeight.Reset(0, b.roundTripCount) + } + return + } + b.roundsWithoutBandwidthGain++ + if b.roundsWithoutBandwidthGain >= b.numStartupRtts || (b.exitStartupOnLoss && b.InRecovery()) { + b.isAtFullBandwidth = true + } +} + +func (b *bbrSender) MaybeExitStartupOrDrain(now time.Time) { + if b.mode == STARTUP && b.isAtFullBandwidth { + b.OnExitStartup(now) + b.mode = DRAIN + b.pacingGain = b.drainGain + b.congestionWindowGain = b.highCwndGain + } + if b.mode == DRAIN && b.GetBytesInFlight() <= b.GetTargetCongestionWindow(1) { + b.EnterProbeBandwidthMode(now) + } +} + +func (b *bbrSender) EnterProbeBandwidthMode(now time.Time) { + b.mode = PROBE_BW + b.congestionWindowGain = b.congestionWindowGainConst + + // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is + // excluded because in that case increased gain and decreased gain would not + // follow each other. + b.cycleCurrentOffset = rand.Int() % (GainCycleLength - 1) + if b.cycleCurrentOffset >= 1 { + b.cycleCurrentOffset += 1 + } + + b.lastCycleStart = now + b.pacingGain = PacingGain[b.cycleCurrentOffset] +} + +func (b *bbrSender) MaybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) { + if minRttExpired && !b.exitingQuiescence && b.mode != PROBE_RTT { + if b.InSlowStart() { + b.OnExitStartup(now) + } + b.mode = PROBE_RTT + b.pacingGain = 1.0 + // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| + // is at the target small value. + b.exitProbeRttAt = time.Time{} + } + + if b.mode == PROBE_RTT { + b.sampler.OnAppLimited() + if b.exitProbeRttAt.IsZero() { + // If the window has reached the appropriate size, schedule exiting + // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but + // we allow an extra packet since QUIC checks CWND before sending a + // packet. + if b.GetBytesInFlight() < b.ProbeRttCongestionWindow()+MaxOutgoingPacketSize { + b.exitProbeRttAt = now.Add(ProbeRttTime) + b.probeRttRoundPassed = false + } + } else { + if isRoundStart { + b.probeRttRoundPassed = true + } + if !now.Before(b.exitProbeRttAt) && b.probeRttRoundPassed { + b.minRttTimestamp = now + if !b.isAtFullBandwidth { + b.EnterStartupMode(now) + } else { + b.EnterProbeBandwidthMode(now) + } + } + } + } + b.exitingQuiescence = false +} + +func (b *bbrSender) ProbeRttCongestionWindow() congestion.ByteCount { + if b.probeRttBasedOnBdp { + return b.GetTargetCongestionWindow(ModerateProbeRttMultiplier) + } else { + return b.minCongestionWindow + } +} + +func (b *bbrSender) EnterStartupMode(now time.Time) { + // if b.rttStats != nil { + // TODO: slow start. + // } + b.mode = STARTUP + b.pacingGain = b.highGain + b.congestionWindowGain = b.highCwndGain +} + +func (b *bbrSender) OnExitStartup(now time.Time) { + if b.rttStats == nil { + return + } + // TODO: slow start. +} + +func (b *bbrSender) CalculatePacingRate() { + if b.BandwidthEstimate() == 0 { + return + } + + targetRate := Bandwidth(b.pacingGain * float64(b.BandwidthEstimate())) + if b.isAtFullBandwidth { + b.pacingRate = targetRate + return + } + + // Pace at the rate of initial_window / RTT as soon as RTT measurements are + // available. + if b.pacingRate == 0 && b.rttStats.MinRTT() > 0 { + b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) + return + } + // Slow the pacing rate in STARTUP once loss has ever been detected. + hasEverDetectedLoss := b.endRecoveryAt > 0 + if b.slowerStartup && hasEverDetectedLoss && b.hasNoAppLimitedSample { + b.pacingRate = Bandwidth(StartupAfterLossGain * float64(b.BandwidthEstimate())) + return + } + + // Slow the pacing rate in STARTUP by the bytes_lost / CWND. + if b.startupRateReductionMultiplier != 0 && hasEverDetectedLoss && b.hasNoAppLimitedSample { + b.pacingRate = Bandwidth((1.0 - (float64(b.startupBytesLost) * float64(b.startupRateReductionMultiplier) / float64(b.congestionWindow))) * float64(targetRate)) + // Ensure the pacing rate doesn't drop below the startup growth target times + // the bandwidth estimate. + b.pacingRate = maxBandwidth(b.pacingRate, Bandwidth(StartupGrowthTarget*float64(b.BandwidthEstimate()))) + return + } + + // Do not decrease the pacing rate during startup. + b.pacingRate = maxBandwidth(b.pacingRate, targetRate) +} + +func (b *bbrSender) CalculateCongestionWindow(ackedBytes, excessAcked congestion.ByteCount) { + if b.mode == PROBE_RTT { + return + } + + targetWindow := b.GetTargetCongestionWindow(b.congestionWindowGain) + if b.isAtFullBandwidth { + // Add the max recently measured ack aggregation to CWND. + targetWindow += congestion.ByteCount(b.maxAckHeight.GetBest()) + } else if b.enableAckAggregationDuringStartup { + // Add the most recent excess acked. Because CWND never decreases in + // STARTUP, this will automatically create a very localized max filter. + targetWindow += excessAcked + } + + // Instead of immediately setting the target CWND as the new one, BBR grows + // the CWND towards |target_window| by only increasing it |bytes_acked| at a + // time. + addBytesAcked := true || !b.InRecovery() + if b.isAtFullBandwidth { + b.congestionWindow = minByteCount(targetWindow, b.congestionWindow+ackedBytes) + } else if addBytesAcked && (b.congestionWindow < targetWindow || b.sampler.totalBytesAcked < b.initialCongestionWindow) { + // If the connection is not yet out of startup phase, do not decrease the + // window. + b.congestionWindow += ackedBytes + } + + // Enforce the limits on the congestion window. + b.congestionWindow = maxByteCount(b.congestionWindow, b.minCongestionWindow) + b.congestionWindow = minByteCount(b.congestionWindow, b.maxCongestionWindow) +} + +func (b *bbrSender) CalculateRecoveryWindow(ackedBytes, lostBytes congestion.ByteCount) { + if b.rateBasedStartup && b.mode == STARTUP { + return + } + + if b.recoveryState == NOT_IN_RECOVERY { + return + } + + // Set up the initial recovery window. + if b.recoveryWindow == 0 { + b.recoveryWindow = maxByteCount(b.GetBytesInFlight()+ackedBytes, b.minCongestionWindow) + return + } + + // Remove losses from the recovery window, while accounting for a potential + // integer underflow. + if b.recoveryWindow >= lostBytes { + b.recoveryWindow -= lostBytes + } else { + b.recoveryWindow = MaxSegmentSize + } + // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, + // release additional |bytes_acked| to achieve a slow-start-like behavior. + if b.recoveryState == GROWTH { + b.recoveryWindow += ackedBytes + } + // Sanity checks. Ensure that we always allow to send at least an MSS or + // |bytes_acked| in response, whichever is larger. + b.recoveryWindow = maxByteCount(b.recoveryWindow, b.GetBytesInFlight()+ackedBytes) + b.recoveryWindow = maxByteCount(b.recoveryWindow, b.minCongestionWindow) +} + +var _ congestion.CongestionControl = &bbrSender{} + +func (b *bbrSender) GetMinRtt() time.Duration { + if b.minRtt > 0 { + return b.minRtt + } else { + return InitialRtt + } +} + +func minRtt(a, b time.Duration) time.Duration { + if a < b { + return a + } else { + return b + } +} + +func minBandwidth(a, b Bandwidth) Bandwidth { + if a < b { + return a + } else { + return b + } +} + +func maxBandwidth(a, b Bandwidth) Bandwidth { + if a > b { + return a + } else { + return b + } +} + +func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount { + if a > b { + return a + } else { + return b + } +} + +func minByteCount(a, b congestion.ByteCount) congestion.ByteCount { + if a < b { + return a + } else { + return b + } +} + +var ( + InfiniteRTT = time.Duration(math.MaxInt64) +) diff --git a/transport/tuic/congestion/clock.go b/transport/tuic/congestion/clock.go new file mode 100644 index 0000000000..405fae70f9 --- /dev/null +++ b/transport/tuic/congestion/clock.go @@ -0,0 +1,18 @@ +package congestion + +import "time" + +// A Clock returns the current time +type Clock interface { + Now() time.Time +} + +// DefaultClock implements the Clock interface using the Go stdlib clock. +type DefaultClock struct{} + +var _ Clock = DefaultClock{} + +// Now gets the current time +func (DefaultClock) Now() time.Time { + return time.Now() +} diff --git a/transport/tuic/congestion/cubic.go b/transport/tuic/congestion/cubic.go new file mode 100644 index 0000000000..dd491a326a --- /dev/null +++ b/transport/tuic/congestion/cubic.go @@ -0,0 +1,213 @@ +package congestion + +import ( + "math" + "time" + + "github.com/metacubex/quic-go/congestion" +) + +// This cubic implementation is based on the one found in Chromiums's QUIC +// implementation, in the files net/quic/congestion_control/cubic.{hh,cc}. + +// Constants based on TCP defaults. +// The following constants are in 2^10 fractions of a second instead of ms to +// allow a 10 shift right to divide. + +// 1024*1024^3 (first 1024 is from 0.100^3) +// where 0.100 is 100 ms which is the scaling round trip time. +const ( + cubeScale = 40 + cubeCongestionWindowScale = 410 + cubeFactor congestion.ByteCount = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize + // TODO: when re-enabling cubic, make sure to use the actual packet size here + maxDatagramSize = congestion.ByteCount(InitialPacketSizeIPv4) +) + +const defaultNumConnections = 1 + +// Default Cubic backoff factor +const beta float32 = 0.7 + +// Additional backoff factor when loss occurs in the concave part of the Cubic +// curve. This additional backoff factor is expected to give up bandwidth to +// new concurrent flows and speed up convergence. +const betaLastMax float32 = 0.85 + +// Cubic implements the cubic algorithm from TCP +type Cubic struct { + clock Clock + + // Number of connections to simulate. + numConnections int + + // Time when this cycle started, after last loss event. + epoch time.Time + + // Max congestion window used just before last loss event. + // Note: to improve fairness to other streams an additional back off is + // applied to this value if the new value is below our latest value. + lastMaxCongestionWindow congestion.ByteCount + + // Number of acked bytes since the cycle started (epoch). + ackedBytesCount congestion.ByteCount + + // TCP Reno equivalent congestion window in packets. + estimatedTCPcongestionWindow congestion.ByteCount + + // Origin point of cubic function. + originPointCongestionWindow congestion.ByteCount + + // Time to origin point of cubic function in 2^10 fractions of a second. + timeToOriginPoint uint32 + + // Last congestion window in packets computed by cubic function. + lastTargetCongestionWindow congestion.ByteCount +} + +// NewCubic returns a new Cubic instance +func NewCubic(clock Clock) *Cubic { + c := &Cubic{ + clock: clock, + numConnections: defaultNumConnections, + } + c.Reset() + return c +} + +// Reset is called after a timeout to reset the cubic state +func (c *Cubic) Reset() { + c.epoch = time.Time{} + c.lastMaxCongestionWindow = 0 + c.ackedBytesCount = 0 + c.estimatedTCPcongestionWindow = 0 + c.originPointCongestionWindow = 0 + c.timeToOriginPoint = 0 + c.lastTargetCongestionWindow = 0 +} + +func (c *Cubic) alpha() float32 { + // TCPFriendly alpha is described in Section 3.3 of the CUBIC paper. Note that + // beta here is a cwnd multiplier, and is equal to 1-beta from the paper. + // We derive the equivalent alpha for an N-connection emulation as: + b := c.beta() + return 3 * float32(c.numConnections) * float32(c.numConnections) * (1 - b) / (1 + b) +} + +func (c *Cubic) beta() float32 { + // kNConnectionBeta is the backoff factor after loss for our N-connection + // emulation, which emulates the effective backoff of an ensemble of N + // TCP-Reno connections on a single loss event. The effective multiplier is + // computed as: + return (float32(c.numConnections) - 1 + beta) / float32(c.numConnections) +} + +func (c *Cubic) betaLastMax() float32 { + // betaLastMax is the additional backoff factor after loss for our + // N-connection emulation, which emulates the additional backoff of + // an ensemble of N TCP-Reno connections on a single loss event. The + // effective multiplier is computed as: + return (float32(c.numConnections) - 1 + betaLastMax) / float32(c.numConnections) +} + +// OnApplicationLimited is called on ack arrival when sender is unable to use +// the available congestion window. Resets Cubic state during quiescence. +func (c *Cubic) OnApplicationLimited() { + // When sender is not using the available congestion window, the window does + // not grow. But to be RTT-independent, Cubic assumes that the sender has been + // using the entire window during the time since the beginning of the current + // "epoch" (the end of the last loss recovery period). Since + // application-limited periods break this assumption, we reset the epoch when + // in such a period. This reset effectively freezes congestion window growth + // through application-limited periods and allows Cubic growth to continue + // when the entire window is being used. + c.epoch = time.Time{} +} + +// CongestionWindowAfterPacketLoss computes a new congestion window to use after +// a loss event. Returns the new congestion window in packets. The new +// congestion window is a multiplicative decrease of our current window. +func (c *Cubic) CongestionWindowAfterPacketLoss(currentCongestionWindow congestion.ByteCount) congestion.ByteCount { + if currentCongestionWindow+maxDatagramSize < c.lastMaxCongestionWindow { + // We never reached the old max, so assume we are competing with another + // flow. Use our extra back off factor to allow the other flow to go up. + c.lastMaxCongestionWindow = congestion.ByteCount(c.betaLastMax() * float32(currentCongestionWindow)) + } else { + c.lastMaxCongestionWindow = currentCongestionWindow + } + c.epoch = time.Time{} // Reset time. + return congestion.ByteCount(float32(currentCongestionWindow) * c.beta()) +} + +// CongestionWindowAfterAck computes a new congestion window to use after a received ACK. +// Returns the new congestion window in packets. The new congestion window +// follows a cubic function that depends on the time passed since last +// packet loss. +func (c *Cubic) CongestionWindowAfterAck( + ackedBytes congestion.ByteCount, + currentCongestionWindow congestion.ByteCount, + delayMin time.Duration, + eventTime time.Time, +) congestion.ByteCount { + c.ackedBytesCount += ackedBytes + + if c.epoch.IsZero() { + // First ACK after a loss event. + c.epoch = eventTime // Start of epoch. + c.ackedBytesCount = ackedBytes // Reset count. + // Reset estimated_tcp_congestion_window_ to be in sync with cubic. + c.estimatedTCPcongestionWindow = currentCongestionWindow + if c.lastMaxCongestionWindow <= currentCongestionWindow { + c.timeToOriginPoint = 0 + c.originPointCongestionWindow = currentCongestionWindow + } else { + c.timeToOriginPoint = uint32(math.Cbrt(float64(cubeFactor * (c.lastMaxCongestionWindow - currentCongestionWindow)))) + c.originPointCongestionWindow = c.lastMaxCongestionWindow + } + } + + // Change the time unit from microseconds to 2^10 fractions per second. Take + // the round trip time in account. This is done to allow us to use shift as a + // divide operator. + elapsedTime := int64(eventTime.Add(delayMin).Sub(c.epoch)/time.Microsecond) << 10 / (1000 * 1000) + + // Right-shifts of negative, signed numbers have implementation-dependent + // behavior, so force the offset to be positive, as is done in the kernel. + offset := int64(c.timeToOriginPoint) - elapsedTime + if offset < 0 { + offset = -offset + } + + deltaCongestionWindow := congestion.ByteCount(cubeCongestionWindowScale*offset*offset*offset) * maxDatagramSize >> cubeScale + var targetCongestionWindow congestion.ByteCount + if elapsedTime > int64(c.timeToOriginPoint) { + targetCongestionWindow = c.originPointCongestionWindow + deltaCongestionWindow + } else { + targetCongestionWindow = c.originPointCongestionWindow - deltaCongestionWindow + } + // Limit the CWND increase to half the acked bytes. + targetCongestionWindow = Min(targetCongestionWindow, currentCongestionWindow+c.ackedBytesCount/2) + + // Increase the window by approximately Alpha * 1 MSS of bytes every + // time we ack an estimated tcp window of bytes. For small + // congestion windows (less than 25), the formula below will + // increase slightly slower than linearly per estimated tcp window + // of bytes. + c.estimatedTCPcongestionWindow += congestion.ByteCount(float32(c.ackedBytesCount) * c.alpha() * float32(maxDatagramSize) / float32(c.estimatedTCPcongestionWindow)) + c.ackedBytesCount = 0 + + // We have a new cubic congestion window. + c.lastTargetCongestionWindow = targetCongestionWindow + + // Compute target congestion_window based on cubic target and estimated TCP + // congestion_window, use highest (fastest). + if targetCongestionWindow < c.estimatedTCPcongestionWindow { + targetCongestionWindow = c.estimatedTCPcongestionWindow + } + return targetCongestionWindow +} + +// SetNumConnections sets the number of emulated connections +func (c *Cubic) SetNumConnections(n int) { + c.numConnections = n +} diff --git a/transport/tuic/congestion/cubic_sender.go b/transport/tuic/congestion/cubic_sender.go new file mode 100644 index 0000000000..c55db7bddc --- /dev/null +++ b/transport/tuic/congestion/cubic_sender.go @@ -0,0 +1,318 @@ +package congestion + +import ( + "fmt" + "time" + + "github.com/metacubex/quic-go/congestion" + "github.com/metacubex/quic-go/logging" +) + +const ( + maxBurstPackets = 3 + renoBeta = 0.7 // Reno backoff factor. + minCongestionWindowPackets = 2 + initialCongestionWindow = 32 +) + +const InvalidPacketNumber congestion.PacketNumber = -1 +const MaxCongestionWindowPackets = 20000 +const MaxByteCount = congestion.ByteCount(1<<62 - 1) + +type cubicSender struct { + hybridSlowStart HybridSlowStart + rttStats congestion.RTTStatsProvider + cubic *Cubic + pacer *pacer + clock Clock + + reno bool + + // Track the largest packet that has been sent. + largestSentPacketNumber congestion.PacketNumber + + // Track the largest packet that has been acked. + largestAckedPacketNumber congestion.PacketNumber + + // Track the largest packet number outstanding when a CWND cutback occurs. + largestSentAtLastCutback congestion.PacketNumber + + // Whether the last loss event caused us to exit slowstart. + // Used for stats collection of slowstartPacketsLost + lastCutbackExitedSlowstart bool + + // Congestion window in bytes. + congestionWindow congestion.ByteCount + + // Slow start congestion window in bytes, aka ssthresh. + slowStartThreshold congestion.ByteCount + + // ACK counter for the Reno implementation. + numAckedPackets uint64 + + initialCongestionWindow congestion.ByteCount + initialMaxCongestionWindow congestion.ByteCount + + maxDatagramSize congestion.ByteCount + + lastState logging.CongestionState + tracer logging.ConnectionTracer +} + +var ( + _ congestion.CongestionControl = &cubicSender{} +) + +// NewCubicSender makes a new cubic sender +func NewCubicSender( + clock Clock, + initialMaxDatagramSize congestion.ByteCount, + reno bool, + tracer logging.ConnectionTracer, +) *cubicSender { + return newCubicSender( + clock, + reno, + initialMaxDatagramSize, + initialCongestionWindow*initialMaxDatagramSize, + MaxCongestionWindowPackets*initialMaxDatagramSize, + tracer, + ) +} + +func newCubicSender( + clock Clock, + reno bool, + initialMaxDatagramSize, + initialCongestionWindow, + initialMaxCongestionWindow congestion.ByteCount, + tracer logging.ConnectionTracer, +) *cubicSender { + c := &cubicSender{ + largestSentPacketNumber: InvalidPacketNumber, + largestAckedPacketNumber: InvalidPacketNumber, + largestSentAtLastCutback: InvalidPacketNumber, + initialCongestionWindow: initialCongestionWindow, + initialMaxCongestionWindow: initialMaxCongestionWindow, + congestionWindow: initialCongestionWindow, + slowStartThreshold: MaxByteCount, + cubic: NewCubic(clock), + clock: clock, + reno: reno, + tracer: tracer, + maxDatagramSize: initialMaxDatagramSize, + } + c.pacer = newPacer(c.BandwidthEstimate) + if c.tracer != nil { + c.lastState = logging.CongestionStateSlowStart + c.tracer.UpdatedCongestionState(logging.CongestionStateSlowStart) + } + return c +} + +func (c *cubicSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { + c.rttStats = provider +} + +// TimeUntilSend returns when the next packet should be sent. +func (c *cubicSender) TimeUntilSend(_ congestion.ByteCount) time.Time { + return c.pacer.TimeUntilSend() +} + +func (c *cubicSender) HasPacingBudget() bool { + return c.pacer.Budget(c.clock.Now()) >= c.maxDatagramSize +} + +func (c *cubicSender) maxCongestionWindow() congestion.ByteCount { + return c.maxDatagramSize * MaxCongestionWindowPackets +} + +func (c *cubicSender) minCongestionWindow() congestion.ByteCount { + return c.maxDatagramSize * minCongestionWindowPackets +} + +func (c *cubicSender) OnPacketSent( + sentTime time.Time, + _ congestion.ByteCount, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + isRetransmittable bool, +) { + c.pacer.SentPacket(sentTime, bytes) + if !isRetransmittable { + return + } + c.largestSentPacketNumber = packetNumber + c.hybridSlowStart.OnPacketSent(packetNumber) +} + +func (c *cubicSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight < c.GetCongestionWindow() +} + +func (c *cubicSender) InRecovery() bool { + return c.largestAckedPacketNumber != InvalidPacketNumber && c.largestAckedPacketNumber <= c.largestSentAtLastCutback +} + +func (c *cubicSender) InSlowStart() bool { + return c.GetCongestionWindow() < c.slowStartThreshold +} + +func (c *cubicSender) GetCongestionWindow() congestion.ByteCount { + return c.congestionWindow +} + +func (c *cubicSender) MaybeExitSlowStart() { + if c.InSlowStart() && + c.hybridSlowStart.ShouldExitSlowStart(c.rttStats.LatestRTT(), c.rttStats.MinRTT(), c.GetCongestionWindow()/c.maxDatagramSize) { + // exit slow start + c.slowStartThreshold = c.congestionWindow + c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance) + } +} + +func (c *cubicSender) OnPacketAcked( + ackedPacketNumber congestion.PacketNumber, + ackedBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, + eventTime time.Time, +) { + c.largestAckedPacketNumber = Max(ackedPacketNumber, c.largestAckedPacketNumber) + if c.InRecovery() { + return + } + c.maybeIncreaseCwnd(ackedPacketNumber, ackedBytes, priorInFlight, eventTime) + if c.InSlowStart() { + c.hybridSlowStart.OnPacketAcked(ackedPacketNumber) + } +} + +func (c *cubicSender) OnPacketLost(packetNumber congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // TCP NewReno (RFC6582) says that once a loss occurs, any losses in packets + // already sent should be treated as a single loss event, since it's expected. + if packetNumber <= c.largestSentAtLastCutback { + return + } + c.lastCutbackExitedSlowstart = c.InSlowStart() + c.maybeTraceStateChange(logging.CongestionStateRecovery) + + if c.reno { + c.congestionWindow = congestion.ByteCount(float64(c.congestionWindow) * renoBeta) + } else { + c.congestionWindow = c.cubic.CongestionWindowAfterPacketLoss(c.congestionWindow) + } + if minCwnd := c.minCongestionWindow(); c.congestionWindow < minCwnd { + c.congestionWindow = minCwnd + } + c.slowStartThreshold = c.congestionWindow + c.largestSentAtLastCutback = c.largestSentPacketNumber + // reset packet count from congestion avoidance mode. We start + // counting again when we're out of recovery. + c.numAckedPackets = 0 +} + +// Called when we receive an ack. Normal TCP tracks how many packets one ack +// represents, but quic has a separate ack for each packet. +func (c *cubicSender) maybeIncreaseCwnd( + _ congestion.PacketNumber, + ackedBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, + eventTime time.Time, +) { + // Do not increase the congestion window unless the sender is close to using + // the current window. + if !c.isCwndLimited(priorInFlight) { + c.cubic.OnApplicationLimited() + c.maybeTraceStateChange(logging.CongestionStateApplicationLimited) + return + } + if c.congestionWindow >= c.maxCongestionWindow() { + return + } + if c.InSlowStart() { + // TCP slow start, exponential growth, increase by one for each ACK. + c.congestionWindow += c.maxDatagramSize + c.maybeTraceStateChange(logging.CongestionStateSlowStart) + return + } + // Congestion avoidance + c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance) + if c.reno { + // Classic Reno congestion avoidance. + c.numAckedPackets++ + if c.numAckedPackets >= uint64(c.congestionWindow/c.maxDatagramSize) { + c.congestionWindow += c.maxDatagramSize + c.numAckedPackets = 0 + } + } else { + c.congestionWindow = Min(c.maxCongestionWindow(), c.cubic.CongestionWindowAfterAck(ackedBytes, c.congestionWindow, c.rttStats.MinRTT(), eventTime)) + } +} + +func (c *cubicSender) isCwndLimited(bytesInFlight congestion.ByteCount) bool { + congestionWindow := c.GetCongestionWindow() + if bytesInFlight >= congestionWindow { + return true + } + availableBytes := congestionWindow - bytesInFlight + slowStartLimited := c.InSlowStart() && bytesInFlight > congestionWindow/2 + return slowStartLimited || availableBytes <= maxBurstPackets*c.maxDatagramSize +} + +// BandwidthEstimate returns the current bandwidth estimate +func (c *cubicSender) BandwidthEstimate() Bandwidth { + if c.rttStats == nil { + return infBandwidth + } + srtt := c.rttStats.SmoothedRTT() + if srtt == 0 { + // If we haven't measured an rtt, the bandwidth estimate is unknown. + return infBandwidth + } + return BandwidthFromDelta(c.GetCongestionWindow(), srtt) +} + +// OnRetransmissionTimeout is called on an retransmission timeout +func (c *cubicSender) OnRetransmissionTimeout(packetsRetransmitted bool) { + c.largestSentAtLastCutback = InvalidPacketNumber + if !packetsRetransmitted { + return + } + c.hybridSlowStart.Restart() + c.cubic.Reset() + c.slowStartThreshold = c.congestionWindow / 2 + c.congestionWindow = c.minCongestionWindow() +} + +// OnConnectionMigration is called when the connection is migrated (?) +func (c *cubicSender) OnConnectionMigration() { + c.hybridSlowStart.Restart() + c.largestSentPacketNumber = InvalidPacketNumber + c.largestAckedPacketNumber = InvalidPacketNumber + c.largestSentAtLastCutback = InvalidPacketNumber + c.lastCutbackExitedSlowstart = false + c.cubic.Reset() + c.numAckedPackets = 0 + c.congestionWindow = c.initialCongestionWindow + c.slowStartThreshold = c.initialMaxCongestionWindow +} + +func (c *cubicSender) maybeTraceStateChange(new logging.CongestionState) { + if c.tracer == nil || new == c.lastState { + return + } + c.tracer.UpdatedCongestionState(new) + c.lastState = new +} + +func (c *cubicSender) SetMaxDatagramSize(s congestion.ByteCount) { + if s < c.maxDatagramSize { + panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", c.maxDatagramSize, s)) + } + cwndIsMinCwnd := c.congestionWindow == c.minCongestionWindow() + c.maxDatagramSize = s + if cwndIsMinCwnd { + c.congestionWindow = c.minCongestionWindow() + } + c.pacer.SetMaxDatagramSize(s) +} diff --git a/transport/tuic/congestion/hybrid_slow_start.go b/transport/tuic/congestion/hybrid_slow_start.go new file mode 100644 index 0000000000..7586774e68 --- /dev/null +++ b/transport/tuic/congestion/hybrid_slow_start.go @@ -0,0 +1,112 @@ +package congestion + +import ( + "time" + + "github.com/metacubex/quic-go/congestion" +) + +// Note(pwestin): the magic clamping numbers come from the original code in +// tcp_cubic.c. +const hybridStartLowWindow = congestion.ByteCount(16) + +// Number of delay samples for detecting the increase of delay. +const hybridStartMinSamples = uint32(8) + +// Exit slow start if the min rtt has increased by more than 1/8th. +const hybridStartDelayFactorExp = 3 // 2^3 = 8 +// The original paper specifies 2 and 8ms, but those have changed over time. +const ( + hybridStartDelayMinThresholdUs = int64(4000) + hybridStartDelayMaxThresholdUs = int64(16000) +) + +// HybridSlowStart implements the TCP hybrid slow start algorithm +type HybridSlowStart struct { + endPacketNumber congestion.PacketNumber + lastSentPacketNumber congestion.PacketNumber + started bool + currentMinRTT time.Duration + rttSampleCount uint32 + hystartFound bool +} + +// StartReceiveRound is called for the start of each receive round (burst) in the slow start phase. +func (s *HybridSlowStart) StartReceiveRound(lastSent congestion.PacketNumber) { + s.endPacketNumber = lastSent + s.currentMinRTT = 0 + s.rttSampleCount = 0 + s.started = true +} + +// IsEndOfRound returns true if this ack is the last packet number of our current slow start round. +func (s *HybridSlowStart) IsEndOfRound(ack congestion.PacketNumber) bool { + return s.endPacketNumber < ack +} + +// ShouldExitSlowStart should be called on every new ack frame, since a new +// RTT measurement can be made then. +// rtt: the RTT for this ack packet. +// minRTT: is the lowest delay (RTT) we have seen during the session. +// congestionWindow: the congestion window in packets. +func (s *HybridSlowStart) ShouldExitSlowStart(latestRTT time.Duration, minRTT time.Duration, congestionWindow congestion.ByteCount) bool { + if !s.started { + // Time to start the hybrid slow start. + s.StartReceiveRound(s.lastSentPacketNumber) + } + if s.hystartFound { + return true + } + // Second detection parameter - delay increase detection. + // Compare the minimum delay (s.currentMinRTT) of the current + // burst of packets relative to the minimum delay during the session. + // Note: we only look at the first few(8) packets in each burst, since we + // only want to compare the lowest RTT of the burst relative to previous + // bursts. + s.rttSampleCount++ + if s.rttSampleCount <= hybridStartMinSamples { + if s.currentMinRTT == 0 || s.currentMinRTT > latestRTT { + s.currentMinRTT = latestRTT + } + } + // We only need to check this once per round. + if s.rttSampleCount == hybridStartMinSamples { + // Divide minRTT by 8 to get a rtt increase threshold for exiting. + minRTTincreaseThresholdUs := int64(minRTT / time.Microsecond >> hybridStartDelayFactorExp) + // Ensure the rtt threshold is never less than 2ms or more than 16ms. + minRTTincreaseThresholdUs = Min(minRTTincreaseThresholdUs, hybridStartDelayMaxThresholdUs) + minRTTincreaseThreshold := time.Duration(Max(minRTTincreaseThresholdUs, hybridStartDelayMinThresholdUs)) * time.Microsecond + + if s.currentMinRTT > (minRTT + minRTTincreaseThreshold) { + s.hystartFound = true + } + } + // Exit from slow start if the cwnd is greater than 16 and + // increasing delay is found. + return congestionWindow >= hybridStartLowWindow && s.hystartFound +} + +// OnPacketSent is called when a packet was sent +func (s *HybridSlowStart) OnPacketSent(packetNumber congestion.PacketNumber) { + s.lastSentPacketNumber = packetNumber +} + +// OnPacketAcked gets invoked after ShouldExitSlowStart, so it's best to end +// the round when the final packet of the burst is received and start it on +// the next incoming ack. +func (s *HybridSlowStart) OnPacketAcked(ackedPacketNumber congestion.PacketNumber) { + if s.IsEndOfRound(ackedPacketNumber) { + s.started = false + } +} + +// Started returns true if started +func (s *HybridSlowStart) Started() bool { + return s.started +} + +// Restart the slow start phase +func (s *HybridSlowStart) Restart() { + s.started = false + s.hystartFound = false +} diff --git a/transport/tuic/congestion/minmax.go b/transport/tuic/congestion/minmax.go new file mode 100644 index 0000000000..ed75072e01 --- /dev/null +++ b/transport/tuic/congestion/minmax.go @@ -0,0 +1,72 @@ +package congestion + +import ( + "math" + "time" + + "golang.org/x/exp/constraints" +) + +// InfDuration is a duration of infinite length +const InfDuration = time.Duration(math.MaxInt64) + +func Max[T constraints.Ordered](a, b T) T { + if a < b { + return b + } + return a +} + +func Min[T constraints.Ordered](a, b T) T { + if a < b { + return a + } + return b +} + +// MinNonZeroDuration return the minimum duration that's not zero. +func MinNonZeroDuration(a, b time.Duration) time.Duration { + if a == 0 { + return b + } + if b == 0 { + return a + } + return Min(a, b) +} + +// AbsDuration returns the absolute value of a time duration +func AbsDuration(d time.Duration) time.Duration { + if d >= 0 { + return d + } + return -d +} + +// MinTime returns the earlier time +func MinTime(a, b time.Time) time.Time { + if a.After(b) { + return b + } + return a +} + +// MinNonZeroTime returns the earlist time that is not time.Time{} +// If both a and b are time.Time{}, it returns time.Time{} +func MinNonZeroTime(a, b time.Time) time.Time { + if a.IsZero() { + return b + } + if b.IsZero() { + return a + } + return MinTime(a, b) +} + +// MaxTime returns the later time +func MaxTime(a, b time.Time) time.Time { + if a.After(b) { + return a + } + return b +} diff --git a/transport/tuic/congestion/pacer.go b/transport/tuic/congestion/pacer.go new file mode 100644 index 0000000000..f60ef5fe1b --- /dev/null +++ b/transport/tuic/congestion/pacer.go @@ -0,0 +1,79 @@ +package congestion + +import ( + "math" + "time" + + "github.com/metacubex/quic-go/congestion" +) + +const initialMaxDatagramSize = congestion.ByteCount(1252) +const MinPacingDelay = time.Millisecond +const TimerGranularity = time.Millisecond +const maxBurstSizePackets = 10 + +// The pacer implements a token bucket pacing algorithm. +type pacer struct { + budgetAtLastSent congestion.ByteCount + maxDatagramSize congestion.ByteCount + lastSentTime time.Time + getAdjustedBandwidth func() uint64 // in bytes/s +} + +func newPacer(getBandwidth func() Bandwidth) *pacer { + p := &pacer{ + maxDatagramSize: initialMaxDatagramSize, + getAdjustedBandwidth: func() uint64 { + // Bandwidth is in bits/s. We need the value in bytes/s. + bw := uint64(getBandwidth() / BytesPerSecond) + // Use a slightly higher value than the actual measured bandwidth. + // RTT variations then won't result in under-utilization of the congestion window. + // Ultimately, this will result in sending packets as acknowledgments are received rather than when timers fire, + // provided the congestion window is fully utilized and acknowledgments arrive at regular intervals. + return bw * 5 / 4 + }, + } + p.budgetAtLastSent = p.maxBurstSize() + return p +} + +func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { + budget := p.Budget(sendTime) + if size > budget { + p.budgetAtLastSent = 0 + } else { + p.budgetAtLastSent = budget - size + } + p.lastSentTime = sendTime +} + +func (p *pacer) Budget(now time.Time) congestion.ByteCount { + if p.lastSentTime.IsZero() { + return p.maxBurstSize() + } + budget := p.budgetAtLastSent + (congestion.ByteCount(p.getAdjustedBandwidth())*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 + return Min(p.maxBurstSize(), budget) +} + +func (p *pacer) maxBurstSize() congestion.ByteCount { + return Max( + congestion.ByteCount(uint64((MinPacingDelay+TimerGranularity).Nanoseconds())*p.getAdjustedBandwidth())/1e9, + maxBurstSizePackets*p.maxDatagramSize, + ) +} + +// TimeUntilSend returns when the next packet should be sent. +// It returns the zero value of time.Time if a packet can be sent immediately. +func (p *pacer) TimeUntilSend() time.Time { + if p.budgetAtLastSent >= p.maxDatagramSize { + return time.Time{} + } + return p.lastSentTime.Add(Max( + MinPacingDelay, + time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/float64(p.getAdjustedBandwidth())))*time.Nanosecond, + )) +} + +func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) { + p.maxDatagramSize = s +} diff --git a/transport/tuic/congestion/windowed_filter.go b/transport/tuic/congestion/windowed_filter.go new file mode 100644 index 0000000000..4da595b94f --- /dev/null +++ b/transport/tuic/congestion/windowed_filter.go @@ -0,0 +1,132 @@ +package congestion + +// WindowedFilter Use the following to construct a windowed filter object of type T. +// For example, a min filter using QuicTime as the time type: +// +// WindowedFilter, QuicTime, QuicTime::Delta> ObjectName; +// +// A max filter using 64-bit integers as the time type: +// +// WindowedFilter, uint64_t, int64_t> ObjectName; +// +// Specifically, this template takes four arguments: +// 1. T -- type of the measurement that is being filtered. +// 2. Compare -- MinFilter or MaxFilter, depending on the type of filter +// desired. +// 3. TimeT -- the type used to represent timestamps. +// 4. TimeDeltaT -- the type used to represent continuous time intervals between +// two timestamps. Has to be the type of (a - b) if both |a| and |b| are +// of type TimeT. +type WindowedFilter struct { + // Time length of window. + windowLength int64 + estimates []Sample + comparator func(int64, int64) bool +} + +type Sample struct { + sample int64 + time int64 +} + +// Compares two values and returns true if the first is greater than or equal +// to the second. +func MaxFilter(a, b int64) bool { + return a >= b +} + +// Compares two values and returns true if the first is less than or equal +// to the second. +func MinFilter(a, b int64) bool { + return a <= b +} + +func NewWindowedFilter(windowLength int64, comparator func(int64, int64) bool) *WindowedFilter { + return &WindowedFilter{ + windowLength: windowLength, + estimates: make([]Sample, 3), + comparator: comparator, + } +} + +// Changes the window length. Does not update any current samples. +func (f *WindowedFilter) SetWindowLength(windowLength int64) { + f.windowLength = windowLength +} + +func (f *WindowedFilter) GetBest() int64 { + return f.estimates[0].sample +} + +func (f *WindowedFilter) GetSecondBest() int64 { + return f.estimates[1].sample +} + +func (f *WindowedFilter) GetThirdBest() int64 { + return f.estimates[2].sample +} + +func (f *WindowedFilter) Update(sample int64, time int64) { + if f.estimates[0].time == 0 || f.comparator(sample, f.estimates[0].sample) || (time-f.estimates[2].time) > f.windowLength { + f.Reset(sample, time) + return + } + + if f.comparator(sample, f.estimates[1].sample) { + f.estimates[1].sample = sample + f.estimates[1].time = time + f.estimates[2].sample = sample + f.estimates[2].time = time + } else if f.comparator(sample, f.estimates[2].sample) { + f.estimates[2].sample = sample + f.estimates[2].time = time + } + + // Expire and update estimates as necessary. + if time-f.estimates[0].time > f.windowLength { + // The best estimate hasn't been updated for an entire window, so promote + // second and third best estimates. + f.estimates[0].sample = f.estimates[1].sample + f.estimates[0].time = f.estimates[1].time + f.estimates[1].sample = f.estimates[2].sample + f.estimates[1].time = f.estimates[2].time + f.estimates[2].sample = sample + f.estimates[2].time = time + // Need to iterate one more time. Check if the new best estimate is + // outside the window as well, since it may also have been recorded a + // long time ago. Don't need to iterate once more since we cover that + // case at the beginning of the method. + if time-f.estimates[0].time > f.windowLength { + f.estimates[0].sample = f.estimates[1].sample + f.estimates[0].time = f.estimates[1].time + f.estimates[1].sample = f.estimates[2].sample + f.estimates[1].time = f.estimates[2].time + } + return + } + if f.estimates[1].sample == f.estimates[0].sample && time-f.estimates[1].time > f.windowLength>>2 { + // A quarter of the window has passed without a better sample, so the + // second-best estimate is taken from the second quarter of the window. + f.estimates[1].sample = sample + f.estimates[1].time = time + f.estimates[2].sample = sample + f.estimates[2].time = time + return + } + + if f.estimates[2].sample == f.estimates[1].sample && time-f.estimates[2].time > f.windowLength>>1 { + // We've passed a half of the window without a better estimate, so take + // a third-best estimate from the second half of the window. + f.estimates[2].sample = sample + f.estimates[2].time = time + } +} + +func (f *WindowedFilter) Reset(newSample int64, newTime int64) { + f.estimates[0].sample = newSample + f.estimates[0].time = newTime + f.estimates[1].sample = newSample + f.estimates[1].time = newTime + f.estimates[2].sample = newSample + f.estimates[2].time = newTime +} diff --git a/transport/tuic/conn.go b/transport/tuic/conn.go new file mode 100644 index 0000000000..81e8c40e3f --- /dev/null +++ b/transport/tuic/conn.go @@ -0,0 +1,257 @@ +package tuic + +import ( + "fmt" + "net" + "net/netip" + "sync" + "sync/atomic" + "time" + + "github.com/metacubex/quic-go" + + N "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/tuic/congestion" +) + +const ( + DefaultStreamReceiveWindow = 15728640 // 15 MB/s + DefaultConnectionReceiveWindow = 67108864 // 64 MB/s +) + +func SetCongestionController(quicConn quic.Connection, cc string) { + switch cc { + case "cubic": + quicConn.SetCongestionControl( + congestion.NewCubicSender( + congestion.DefaultClock{}, + congestion.GetMaxPacketSize(quicConn.RemoteAddr()), + false, + nil, + ), + ) + case "new_reno": + quicConn.SetCongestionControl( + congestion.NewCubicSender( + congestion.DefaultClock{}, + congestion.GetMaxPacketSize(quicConn.RemoteAddr()), + true, + nil, + ), + ) + case "bbr": + quicConn.SetCongestionControl( + congestion.NewBBRSender( + congestion.DefaultClock{}, + congestion.GetMaxPacketSize(quicConn.RemoteAddr()), + congestion.InitialCongestionWindow, + congestion.DefaultBBRMaxCongestionWindow, + ), + ) + } +} + +type quicStreamConn struct { + quic.Stream + lock sync.Mutex + lAddr net.Addr + rAddr net.Addr + + closeDeferFn func() + + closeOnce sync.Once + closeErr error +} + +func (q *quicStreamConn) Write(p []byte) (n int, err error) { + q.lock.Lock() + defer q.lock.Unlock() + return q.Stream.Write(p) +} + +func (q *quicStreamConn) Close() error { + q.closeOnce.Do(func() { + q.closeErr = q.close() + }) + return q.closeErr +} + +func (q *quicStreamConn) close() error { + if q.closeDeferFn != nil { + defer q.closeDeferFn() + } + + // https://github.com/cloudflare/cloudflared/commit/ed2bac026db46b239699ac5ce4fcf122d7cab2cd + // Make sure a possible writer does not block the lock forever. We need it, so we can close the writer + // side of the stream safely. + _ = q.Stream.SetWriteDeadline(time.Now()) + + // This lock is eventually acquired despite Write also acquiring it, because we set a deadline to writes. + q.lock.Lock() + defer q.lock.Unlock() + + // We have to clean up the receiving stream ourselves since the Close in the bottom does not handle that. + q.Stream.CancelRead(0) + return q.Stream.Close() +} + +func (q *quicStreamConn) LocalAddr() net.Addr { + return q.lAddr +} + +func (q *quicStreamConn) RemoteAddr() net.Addr { + return q.rAddr +} + +var _ net.Conn = &quicStreamConn{} + +type quicStreamPacketConn struct { + connId uint32 + quicConn quic.Connection + lAddr net.Addr + inputConn *N.BufferedConn + + udpRelayMode string + maxUdpRelayPacketSize int + + deferQuicConnFn func(quicConn quic.Connection, err error) + closeDeferFn func() + writeClosed *atomic.Bool + + closeOnce sync.Once + closeErr error + closed bool +} + +func (q *quicStreamPacketConn) Close() error { + q.closeOnce.Do(func() { + q.closed = true + q.closeErr = q.close() + }) + return q.closeErr +} + +func (q *quicStreamPacketConn) close() (err error) { + if q.closeDeferFn != nil { + defer q.closeDeferFn() + } + if q.deferQuicConnFn != nil { + defer func() { + q.deferQuicConnFn(q.quicConn, err) + }() + } + if q.inputConn != nil { + _ = q.inputConn.Close() + q.inputConn = nil + + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + err = NewDissociate(q.connId).WriteTo(buf) + if err != nil { + return + } + var stream quic.SendStream + stream, err = q.quicConn.OpenUniStream() + if err != nil { + return + } + _, err = buf.WriteTo(stream) + if err != nil { + return + } + err = stream.Close() + if err != nil { + return + } + } + return +} + +func (q *quicStreamPacketConn) SetDeadline(t time.Time) error { + //TODO implement me + return nil +} + +func (q *quicStreamPacketConn) SetReadDeadline(t time.Time) error { + if q.inputConn != nil { + return q.inputConn.SetReadDeadline(t) + } + return nil +} + +func (q *quicStreamPacketConn) SetWriteDeadline(t time.Time) error { + //TODO implement me + return nil +} + +func (q *quicStreamPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + if q.inputConn != nil { + var packet Packet + packet, err = ReadPacket(q.inputConn) + if err != nil { + return + } + n = copy(p, packet.DATA) + addr = packet.ADDR.UDPAddr() + } else { + err = net.ErrClosed + } + return +} + +func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if len(p) > q.maxUdpRelayPacketSize { + return 0, fmt.Errorf("udp packet too large(%d > %d)", len(p), q.maxUdpRelayPacketSize) + } + if q.closed { + return 0, net.ErrClosed + } + if q.writeClosed != nil && q.writeClosed.Load() { + _ = q.Close() + return 0, net.ErrClosed + } + if q.deferQuicConnFn != nil { + defer func() { + q.deferQuicConnFn(q.quicConn, err) + }() + } + addr.String() + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + addrPort, err := netip.ParseAddrPort(addr.String()) + if err != nil { + return + } + err = NewPacket(q.connId, uint16(len(p)), NewAddressAddrPort(addrPort), p).WriteTo(buf) + if err != nil { + return + } + switch q.udpRelayMode { + case "quic": + var stream quic.SendStream + stream, err = q.quicConn.OpenUniStream() + if err != nil { + return + } + defer stream.Close() + _, err = buf.WriteTo(stream) + if err != nil { + return + } + default: // native + err = q.quicConn.SendMessage(buf.Bytes()) + if err != nil { + return + } + } + n = len(p) + + return +} + +func (q *quicStreamPacketConn) LocalAddr() net.Addr { + return q.lAddr +} + +var _ net.PacketConn = &quicStreamPacketConn{} diff --git a/transport/tuic/pool_client.go b/transport/tuic/pool_client.go new file mode 100644 index 0000000000..fe06c2f369 --- /dev/null +++ b/transport/tuic/pool_client.go @@ -0,0 +1,181 @@ +package tuic + +import ( + "context" + "errors" + "net" + "runtime" + "sync" + "time" + + "github.com/Dreamacro/clash/common/generics/list" + N "github.com/Dreamacro/clash/common/net" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" +) + +type dialResult struct { + pc net.PacketConn + addr net.Addr + err error +} + +type PoolClient struct { + *ClientOption + + newClientOption *ClientOption + dialResultMap map[C.Dialer]dialResult + dialResultMutex *sync.Mutex + tcpClients *list.List[*Client] + tcpClientsMutex *sync.Mutex + udpClients *list.List[*Client] + udpClientsMutex *sync.Mutex +} + +func (t *PoolClient) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.Conn, error) { + conn, err := t.getClient(false, dialer).DialContextWithDialer(ctx, metadata, dialer, dialFn) + if errors.Is(err, TooManyOpenStreams) { + conn, err = t.newClient(false, dialer).DialContextWithDialer(ctx, metadata, dialer, dialFn) + } + if err != nil { + return nil, err + } + return N.NewRefConn(conn, t), err +} + +func (t *PoolClient) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { + pc, err := t.getClient(true, dialer).ListenPacketWithDialer(ctx, metadata, dialer, dialFn) + if errors.Is(err, TooManyOpenStreams) { + pc, err = t.newClient(true, dialer).ListenPacketWithDialer(ctx, metadata, dialer, dialFn) + } + if err != nil { + return nil, err + } + return N.NewRefPacketConn(pc, t), nil +} + +func (t *PoolClient) dial(ctx context.Context, dialer C.Dialer, dialFn DialFunc) (pc net.PacketConn, addr net.Addr, err error) { + t.dialResultMutex.Lock() + dr, ok := t.dialResultMap[dialer] + t.dialResultMutex.Unlock() + if ok { + return dr.pc, dr.addr, dr.err + } + + pc, addr, err = dialFn(ctx, dialer) + if err != nil { + return nil, nil, err + } + + dr.pc, dr.addr, dr.err = pc, addr, err + + t.dialResultMutex.Lock() + t.dialResultMap[dialer] = dr + t.dialResultMutex.Unlock() + return pc, addr, err +} + +func (t *PoolClient) forceClose() { + t.dialResultMutex.Lock() + defer t.dialResultMutex.Unlock() + for key := range t.dialResultMap { + pc := t.dialResultMap[key].pc + if pc != nil { + _ = pc.Close() + } + delete(t.dialResultMap, key) + } +} + +func (t *PoolClient) newClient(udp bool, dialer C.Dialer) *Client { + clients := t.tcpClients + clientsMutex := t.tcpClientsMutex + if udp { + clients = t.udpClients + clientsMutex = t.udpClientsMutex + } + + clientsMutex.Lock() + defer clientsMutex.Unlock() + + client := NewClient(t.newClientOption, udp) + client.dialerRef = dialer + client.lastVisited = time.Now() + + clients.PushFront(client) + return client +} + +func (t *PoolClient) getClient(udp bool, dialer C.Dialer) *Client { + clients := t.tcpClients + clientsMutex := t.tcpClientsMutex + if udp { + clients = t.udpClients + clientsMutex = t.udpClientsMutex + } + var bestClient *Client + + func() { + clientsMutex.Lock() + defer clientsMutex.Unlock() + for it := clients.Front(); it != nil; { + client := it.Value + if client == nil { + next := it.Next() + clients.Remove(it) + it = next + continue + } + if client.dialerRef == dialer { + if bestClient == nil { + bestClient = client + } else { + if client.openStreams.Load() < bestClient.openStreams.Load() { + bestClient = client + } + } + } + it = it.Next() + } + }() + for it := clients.Front(); it != nil; { + client := it.Value + if client != bestClient && client.openStreams.Load() == 0 && time.Now().Sub(client.lastVisited) > 30*time.Minute { + client.Close() + next := it.Next() + clients.Remove(it) + it = next + continue + } + it = it.Next() + } + + if bestClient == nil { + return t.newClient(udp, dialer) + } else { + bestClient.lastVisited = time.Now() + return bestClient + } +} + +func NewPoolClient(clientOption *ClientOption) *PoolClient { + p := &PoolClient{ + ClientOption: clientOption, + dialResultMap: make(map[C.Dialer]dialResult), + dialResultMutex: &sync.Mutex{}, + tcpClients: list.New[*Client](), + tcpClientsMutex: &sync.Mutex{}, + udpClients: list.New[*Client](), + udpClientsMutex: &sync.Mutex{}, + } + newClientOption := *clientOption + p.newClientOption = &newClientOption + runtime.SetFinalizer(p, closeClientPool) + log.Debugln("New Tuic PoolClient at %p", p) + return p +} + +func closeClientPool(client *PoolClient) { + log.Debugln("Close Tuic PoolClient at %p", client) + client.forceClose() +} diff --git a/transport/tuic/protocol.go b/transport/tuic/protocol.go new file mode 100644 index 0000000000..ab696e79b2 --- /dev/null +++ b/transport/tuic/protocol.go @@ -0,0 +1,595 @@ +package tuic + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "net/netip" + "strconv" + + "github.com/metacubex/quic-go" + "lukechampine.com/blake3" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type BufferedReader interface { + io.Reader + io.ByteReader +} + +type BufferedWriter interface { + io.Writer + io.ByteWriter +} + +type CommandType byte + +const ( + AuthenticateType = CommandType(0x00) + ConnectType = CommandType(0x01) + PacketType = CommandType(0x02) + DissociateType = CommandType(0x03) + HeartbeatType = CommandType(0x04) + ResponseType = CommandType(0xff) +) + +func (c CommandType) String() string { + switch c { + case AuthenticateType: + return "Authenticate" + case ConnectType: + return "Connect" + case PacketType: + return "Packet" + case DissociateType: + return "Dissociate" + case HeartbeatType: + return "Heartbeat" + case ResponseType: + return "Response" + default: + return fmt.Sprintf("UnknowCommand: %#x", byte(c)) + } +} + +func (c CommandType) BytesLen() int { + return 1 +} + +type CommandHead struct { + VER byte + TYPE CommandType +} + +func NewCommandHead(TYPE CommandType) CommandHead { + return CommandHead{ + VER: 0x04, + TYPE: TYPE, + } +} + +func ReadCommandHead(reader BufferedReader) (c CommandHead, err error) { + c.VER, err = reader.ReadByte() + if err != nil { + return + } + TYPE, err := reader.ReadByte() + if err != nil { + return + } + c.TYPE = CommandType(TYPE) + return +} + +func (c CommandHead) WriteTo(writer BufferedWriter) (err error) { + err = writer.WriteByte(c.VER) + if err != nil { + return + } + err = writer.WriteByte(byte(c.TYPE)) + if err != nil { + return + } + return +} + +func (c CommandHead) BytesLen() int { + return 1 + c.TYPE.BytesLen() +} + +type Authenticate struct { + CommandHead + TKN [32]byte +} + +func NewAuthenticate(TKN [32]byte) Authenticate { + return Authenticate{ + CommandHead: NewCommandHead(AuthenticateType), + TKN: TKN, + } +} + +func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { + c.CommandHead = head + if err != nil { + return + } + if c.CommandHead.TYPE != AuthenticateType { + err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) + return + } + _, err = io.ReadFull(reader, c.TKN[:]) + if err != nil { + return + } + return +} + +func ReadAuthenticate(reader BufferedReader) (c Authenticate, err error) { + head, err := ReadCommandHead(reader) + if err != nil { + return + } + return ReadAuthenticateWithHead(head, reader) +} + +func GenTKN(token string) [32]byte { + return blake3.Sum256([]byte(token)) +} + +func (c Authenticate) WriteTo(writer BufferedWriter) (err error) { + err = c.CommandHead.WriteTo(writer) + if err != nil { + return + } + _, err = writer.Write(c.TKN[:]) + if err != nil { + return + } + return +} + +func (c Authenticate) BytesLen() int { + return c.CommandHead.BytesLen() + 32 +} + +type Connect struct { + CommandHead + ADDR Address +} + +func NewConnect(ADDR Address) Connect { + return Connect{ + CommandHead: NewCommandHead(ConnectType), + ADDR: ADDR, + } +} + +func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { + c.CommandHead = head + if err != nil { + return + } + if c.CommandHead.TYPE != ConnectType { + err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) + return + } + c.ADDR, err = ReadAddress(reader) + if err != nil { + return + } + return +} + +func ReadConnect(reader BufferedReader) (c Connect, err error) { + head, err := ReadCommandHead(reader) + if err != nil { + return + } + return ReadConnectWithHead(head, reader) +} + +func (c Connect) WriteTo(writer BufferedWriter) (err error) { + err = c.CommandHead.WriteTo(writer) + if err != nil { + return + } + err = c.ADDR.WriteTo(writer) + if err != nil { + return + } + return +} + +func (c Connect) BytesLen() int { + return c.CommandHead.BytesLen() + c.ADDR.BytesLen() +} + +type Packet struct { + CommandHead + ASSOC_ID uint32 + LEN uint16 + ADDR Address + DATA []byte +} + +func NewPacket(ASSOC_ID uint32, LEN uint16, ADDR Address, DATA []byte) Packet { + return Packet{ + CommandHead: NewCommandHead(PacketType), + ASSOC_ID: ASSOC_ID, + LEN: LEN, + ADDR: ADDR, + DATA: DATA, + } +} + +func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { + c.CommandHead = head + if err != nil { + return + } + if c.CommandHead.TYPE != PacketType { + err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) + return + } + err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) + if err != nil { + return + } + err = binary.Read(reader, binary.BigEndian, &c.LEN) + if err != nil { + return + } + c.ADDR, err = ReadAddress(reader) + if err != nil { + return + } + c.DATA = make([]byte, c.LEN) + _, err = io.ReadFull(reader, c.DATA) + if err != nil { + return + } + return +} + +func ReadPacket(reader BufferedReader) (c Packet, err error) { + head, err := ReadCommandHead(reader) + if err != nil { + return + } + return ReadPacketWithHead(head, reader) +} + +func (c Packet) WriteTo(writer BufferedWriter) (err error) { + err = c.CommandHead.WriteTo(writer) + if err != nil { + return + } + err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) + if err != nil { + return + } + err = binary.Write(writer, binary.BigEndian, c.LEN) + if err != nil { + return + } + err = c.ADDR.WriteTo(writer) + if err != nil { + return + } + _, err = writer.Write(c.DATA) + if err != nil { + return + } + return +} + +func (c Packet) BytesLen() int { + return c.CommandHead.BytesLen() + 4 + 2 + c.ADDR.BytesLen() + len(c.DATA) +} + +type Dissociate struct { + CommandHead + ASSOC_ID uint32 +} + +func NewDissociate(ASSOC_ID uint32) Dissociate { + return Dissociate{ + CommandHead: NewCommandHead(DissociateType), + ASSOC_ID: ASSOC_ID, + } +} + +func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { + c.CommandHead = head + if err != nil { + return + } + if c.CommandHead.TYPE != DissociateType { + err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) + return + } + err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) + if err != nil { + return + } + return +} + +func ReadDissociate(reader BufferedReader) (c Dissociate, err error) { + head, err := ReadCommandHead(reader) + if err != nil { + return + } + return ReadDissociateWithHead(head, reader) +} + +func (c Dissociate) WriteTo(writer BufferedWriter) (err error) { + err = c.CommandHead.WriteTo(writer) + if err != nil { + return + } + err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) + if err != nil { + return + } + return +} + +func (c Dissociate) BytesLen() int { + return c.CommandHead.BytesLen() + 4 +} + +type Heartbeat struct { + CommandHead +} + +func NewHeartbeat() Heartbeat { + return Heartbeat{ + CommandHead: NewCommandHead(HeartbeatType), + } +} + +func ReadHeartbeatWithHead(head CommandHead, reader BufferedReader) (c Heartbeat, err error) { + c.CommandHead = head + if c.CommandHead.TYPE != HeartbeatType { + err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) + return + } + return +} + +func ReadHeartbeat(reader BufferedReader) (c Heartbeat, err error) { + head, err := ReadCommandHead(reader) + if err != nil { + return + } + return ReadHeartbeatWithHead(head, reader) +} + +type Response struct { + CommandHead + REP byte +} + +func NewResponse(REP byte) Response { + return Response{ + CommandHead: NewCommandHead(ResponseType), + REP: REP, + } +} + +func NewResponseSucceed() Response { + return NewResponse(0x00) +} + +func NewResponseFailed() Response { + return NewResponse(0xff) +} + +func ReadResponseWithHead(head CommandHead, reader BufferedReader) (c Response, err error) { + c.CommandHead = head + if c.CommandHead.TYPE != ResponseType { + err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) + return + } + c.REP, err = reader.ReadByte() + if err != nil { + return + } + return +} + +func ReadResponse(reader BufferedReader) (c Response, err error) { + head, err := ReadCommandHead(reader) + if err != nil { + return + } + return ReadResponseWithHead(head, reader) +} + +func (c Response) WriteTo(writer BufferedWriter) (err error) { + err = c.CommandHead.WriteTo(writer) + if err != nil { + return + } + err = writer.WriteByte(c.REP) + if err != nil { + return + } + return +} + +func (c Response) IsSucceed() bool { + return c.REP == 0x00 +} + +func (c Response) IsFailed() bool { + return c.REP == 0xff +} + +func (c Response) BytesLen() int { + return c.CommandHead.BytesLen() + 1 +} + +// Addr types +const ( + AtypDomainName byte = 0 + AtypIPv4 byte = 1 + AtypIPv6 byte = 2 +) + +type Address struct { + TYPE byte + ADDR []byte + PORT uint16 +} + +func NewAddress(metadata *C.Metadata) Address { + var addrType byte + var addr []byte + switch metadata.AddrType() { + case socks5.AtypIPv4: + addrType = AtypIPv4 + addr = make([]byte, net.IPv4len) + copy(addr[:], metadata.DstIP.AsSlice()) + case socks5.AtypIPv6: + addrType = AtypIPv6 + addr = make([]byte, net.IPv6len) + copy(addr[:], metadata.DstIP.AsSlice()) + case socks5.AtypDomainName: + addrType = AtypDomainName + addr = make([]byte, len(metadata.Host)+1) + addr[0] = byte(len(metadata.Host)) + copy(addr[1:], metadata.Host) + } + + port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) + + return Address{ + TYPE: addrType, + ADDR: addr, + PORT: uint16(port), + } +} + +func NewAddressAddrPort(addrPort netip.AddrPort) Address { + var addrType byte + var addr []byte + if addrPort.Addr().Is4() { + addrType = AtypIPv4 + addr = make([]byte, net.IPv4len) + } else { + addrType = AtypIPv6 + addr = make([]byte, net.IPv6len) + } + copy(addr[:], addrPort.Addr().AsSlice()) + return Address{ + TYPE: addrType, + ADDR: addr, + PORT: addrPort.Port(), + } +} + +func ReadAddress(reader BufferedReader) (c Address, err error) { + c.TYPE, err = reader.ReadByte() + if err != nil { + return + } + switch c.TYPE { + case AtypIPv4: + c.ADDR = make([]byte, net.IPv4len) + _, err = io.ReadFull(reader, c.ADDR) + if err != nil { + return + } + case AtypIPv6: + c.ADDR = make([]byte, net.IPv6len) + _, err = io.ReadFull(reader, c.ADDR) + if err != nil { + return + } + case AtypDomainName: + var addrLen byte + addrLen, err = reader.ReadByte() + if err != nil { + return + } + c.ADDR = make([]byte, addrLen+1) + c.ADDR[0] = addrLen + _, err = io.ReadFull(reader, c.ADDR[1:]) + if err != nil { + return + } + } + + err = binary.Read(reader, binary.BigEndian, &c.PORT) + if err != nil { + return + } + return +} + +func (c Address) WriteTo(writer BufferedWriter) (err error) { + err = writer.WriteByte(c.TYPE) + if err != nil { + return + } + _, err = writer.Write(c.ADDR[:]) + if err != nil { + return + } + err = binary.Write(writer, binary.BigEndian, c.PORT) + if err != nil { + return + } + return +} + +func (c Address) String() string { + switch c.TYPE { + case AtypDomainName: + return net.JoinHostPort(string(c.ADDR[1:]), strconv.Itoa(int(c.PORT))) + default: + addr, _ := netip.AddrFromSlice(c.ADDR) + addrPort := netip.AddrPortFrom(addr, c.PORT) + return addrPort.String() + } +} + +func (c Address) SocksAddr() socks5.Addr { + addr := make([]byte, 1+len(c.ADDR)+2) + switch c.TYPE { + case AtypIPv4: + addr[0] = socks5.AtypIPv4 + case AtypIPv6: + addr[0] = socks5.AtypIPv6 + case AtypDomainName: + addr[0] = socks5.AtypDomainName + } + copy(addr[1:], c.ADDR) + binary.BigEndian.PutUint16(addr[len(addr)-2:], c.PORT) + return addr +} + +func (c Address) UDPAddr() *net.UDPAddr { + return &net.UDPAddr{ + IP: c.ADDR, + Port: int(c.PORT), + Zone: "", + } +} + +func (c Address) BytesLen() int { + return 1 + len(c.ADDR) + 2 +} + +const ( + ProtocolError = quic.ApplicationErrorCode(0xfffffff0) + AuthenticationFailed = quic.ApplicationErrorCode(0xfffffff1) + AuthenticationTimeout = quic.ApplicationErrorCode(0xfffffff2) + BadCommand = quic.ApplicationErrorCode(0xfffffff3) +) diff --git a/transport/tuic/server.go b/transport/tuic/server.go new file mode 100644 index 0000000000..3f459fd6a7 --- /dev/null +++ b/transport/tuic/server.go @@ -0,0 +1,314 @@ +package tuic + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/gofrs/uuid" + "github.com/metacubex/quic-go" + + N "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type ServerOption struct { + HandleTcpFn func(conn net.Conn, addr socks5.Addr) error + HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket) error + + TlsConfig *tls.Config + QuicConfig *quic.Config + Tokens [][32]byte + CongestionController string + AuthenticationTimeout time.Duration + MaxUdpRelayPacketSize int +} + +type Server struct { + *ServerOption + listener quic.EarlyListener +} + +func NewServer(option *ServerOption, pc net.PacketConn) (*Server, error) { + listener, err := quic.ListenEarly(pc, option.TlsConfig, option.QuicConfig) + if err != nil { + return nil, err + } + return &Server{ + ServerOption: option, + listener: listener, + }, err +} + +func (s *Server) Serve() error { + for { + conn, err := s.listener.Accept(context.Background()) + if err != nil { + return err + } + uuid, err := uuid.NewV4() + if err != nil { + return err + } + h := &serverHandler{ + Server: s, + quicConn: conn, + uuid: uuid, + authCh: make(chan struct{}), + } + go h.handle() + } +} + +func (s *Server) Close() error { + return s.listener.Close() +} + +type serverHandler struct { + *Server + quicConn quic.Connection + uuid uuid.UUID + + authCh chan struct{} + authOk bool + authOnce sync.Once + + udpInputMap sync.Map +} + +func (s *serverHandler) handle() { + time.AfterFunc(s.AuthenticationTimeout, func() { + s.authOnce.Do(func() { + _ = s.quicConn.CloseWithError(AuthenticationTimeout, "") + s.authOk = false + close(s.authCh) + }) + }) + go func() { + _ = s.handleUniStream() + }() + go func() { + _ = s.handleStream() + }() + go func() { + _ = s.handleMessage() + }() +} + +func (s *serverHandler) handleMessage() (err error) { + for { + var message []byte + message, err = s.quicConn.ReceiveMessage() + if err != nil { + return err + } + go func() (err error) { + buffer := bytes.NewBuffer(message) + packet, err := ReadPacket(buffer) + if err != nil { + return + } + return s.parsePacket(packet, "native") + }() + } +} + +func (s *serverHandler) parsePacket(packet Packet, udpRelayMode string) (err error) { + <-s.authCh + if !s.authOk { + return + } + var assocId uint32 + + assocId = packet.ASSOC_ID + + v, _ := s.udpInputMap.LoadOrStore(assocId, &atomic.Bool{}) + writeClosed := v.(*atomic.Bool) + if writeClosed.Load() { + return nil + } + + pc := &quicStreamPacketConn{ + connId: assocId, + quicConn: s.quicConn, + lAddr: s.quicConn.LocalAddr(), + inputConn: nil, + udpRelayMode: udpRelayMode, + maxUdpRelayPacketSize: s.MaxUdpRelayPacketSize, + deferQuicConnFn: nil, + closeDeferFn: nil, + writeClosed: writeClosed, + } + + return s.HandleUdpFn(packet.ADDR.SocksAddr(), &serverUDPPacket{ + pc: pc, + packet: &packet, + rAddr: s.genServerAssocIdAddr(assocId), + }) +} + +func (s *serverHandler) genServerAssocIdAddr(assocId uint32) net.Addr { + return ServerAssocIdAddr(fmt.Sprintf("tuic-%s-%d", s.uuid.String(), assocId)) +} + +func (s *serverHandler) handleStream() (err error) { + for { + var quicStream quic.Stream + quicStream, err = s.quicConn.AcceptStream(context.Background()) + if err != nil { + return err + } + + go func() (err error) { + stream := &quicStreamConn{ + Stream: quicStream, + lAddr: s.quicConn.LocalAddr(), + rAddr: s.quicConn.RemoteAddr(), + } + conn := N.NewBufferedConn(stream) + connect, err := ReadConnect(conn) + if err != nil { + return err + } + <-s.authCh + if !s.authOk { + return conn.Close() + } + + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + err = s.HandleTcpFn(conn, connect.ADDR.SocksAddr()) + if err != nil { + err = NewResponseFailed().WriteTo(buf) + defer conn.Close() + } else { + err = NewResponseSucceed().WriteTo(buf) + } + if err != nil { + _ = conn.Close() + return err + } + _, err = buf.WriteTo(stream) + if err != nil { + _ = conn.Close() + return err + } + + return + }() + } +} + +func (s *serverHandler) handleUniStream() (err error) { + for { + var stream quic.ReceiveStream + stream, err = s.quicConn.AcceptUniStream(context.Background()) + if err != nil { + return err + } + go func() (err error) { + defer func() { + stream.CancelRead(0) + }() + reader := bufio.NewReader(stream) + commandHead, err := ReadCommandHead(reader) + if err != nil { + return + } + switch commandHead.TYPE { + case AuthenticateType: + var authenticate Authenticate + authenticate, err = ReadAuthenticateWithHead(commandHead, reader) + if err != nil { + return + } + ok := false + for _, tkn := range s.Tokens { + if authenticate.TKN == tkn { + ok = true + break + } + } + s.authOnce.Do(func() { + if !ok { + _ = s.quicConn.CloseWithError(AuthenticationFailed, "") + } + s.authOk = ok + close(s.authCh) + }) + case PacketType: + var packet Packet + packet, err = ReadPacketWithHead(commandHead, reader) + if err != nil { + return + } + return s.parsePacket(packet, "quic") + case DissociateType: + var disassociate Dissociate + disassociate, err = ReadDissociateWithHead(commandHead, reader) + if err != nil { + return + } + if v, loaded := s.udpInputMap.LoadAndDelete(disassociate.ASSOC_ID); loaded { + writeClosed := v.(*atomic.Bool) + writeClosed.Store(true) + } + case HeartbeatType: + var heartbeat Heartbeat + heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) + if err != nil { + return + } + heartbeat.BytesLen() + } + return + }() + } +} + +type ServerAssocIdAddr string + +func (a ServerAssocIdAddr) Network() string { + return "ServerAssocIdAddr" +} + +func (a ServerAssocIdAddr) String() string { + return string(a) +} + +type serverUDPPacket struct { + pc *quicStreamPacketConn + packet *Packet + rAddr net.Addr +} + +func (s *serverUDPPacket) InAddr() net.Addr { + return s.pc.LocalAddr() +} + +func (s *serverUDPPacket) LocalAddr() net.Addr { + return s.rAddr +} + +func (s *serverUDPPacket) Data() []byte { + return s.packet.DATA +} + +func (s *serverUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { + return s.pc.WriteTo(b, addr) +} + +func (s *serverUDPPacket) Drop() { + s.packet.DATA = nil +} + +var _ C.UDPPacket = &serverUDPPacket{} +var _ C.UDPPacketInAddr = &serverUDPPacket{} diff --git a/transport/vmess/websocket.go b/transport/vmess/websocket.go index f769dcce84..b7b369fd3d 100644 --- a/transport/vmess/websocket.go +++ b/transport/vmess/websocket.go @@ -256,10 +256,16 @@ func streamWebsocketConn(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buf dialer.TLSClientConfig = c.TLSConfig } + u, err := url.Parse(c.Path) + if err != nil { + return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) + } + uri := url.URL{ - Scheme: scheme, - Host: net.JoinHostPort(c.Host, c.Port), - Path: c.Path, + Scheme: scheme, + Host: net.JoinHostPort(c.Host, c.Port), + Path: u.Path, + RawQuery: u.RawQuery, } headers := http.Header{} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index ddf771b963..8f0ef8f6f7 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -3,7 +3,6 @@ package tunnel import ( "context" "fmt" - P "github.com/Dreamacro/clash/component/process" "net" "net/netip" "path/filepath" @@ -12,8 +11,10 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/adapter/inbound" + "github.com/jpillora/backoff" + "github.com/Dreamacro/clash/component/nat" + P "github.com/Dreamacro/clash/component/process" "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/sniffer" C "github.com/Dreamacro/clash/constant" @@ -25,13 +26,15 @@ import ( var ( tcpQueue = make(chan C.ConnContext, 200) - udpQueue = make(chan *inbound.PacketAdapter, 200) + udpQueue = make(chan C.PacketAdapter, 200) natTable = nat.New() rules []C.Rule + listeners = make(map[string]C.InboundListener) + subRules map[string][]C.Rule proxies = make(map[string]C.Proxy) providers map[string]provider.ProxyProvider ruleProviders map[string]provider.RuleProvider - sniffingEnable bool + sniffingEnable = false configMux sync.RWMutex // Outbound Rule @@ -41,8 +44,18 @@ var ( udpTimeout = 60 * time.Second alwaysFindProcess = false + + fakeIPRange netip.Prefix ) +func SetFakeIPRange(p netip.Prefix) { + fakeIPRange = p +} + +func FakeIPRange() netip.Prefix { + return fakeIPRange +} + func SetSniffing(b bool) { if sniffer.Dispatcher.Enable() { configMux.Lock() @@ -65,7 +78,7 @@ func TCPIn() chan<- C.ConnContext { } // UDPIn return fan-in udp queue -func UDPIn() chan<- *inbound.PacketAdapter { +func UDPIn() chan<- C.PacketAdapter { return udpQueue } @@ -74,11 +87,16 @@ func Rules() []C.Rule { return rules } +func Listeners() map[string]C.InboundListener { + return listeners +} + // UpdateRules handle update rules -func UpdateRules(newRules []C.Rule, rp map[string]provider.RuleProvider) { +func UpdateRules(newRules []C.Rule, newSubRule map[string][]C.Rule, rp map[string]provider.RuleProvider) { configMux.Lock() rules = newRules ruleProviders = rp + subRules = newSubRule configMux.Unlock() } @@ -105,10 +123,16 @@ func UpdateProxies(newProxies map[string]C.Proxy, newProviders map[string]provid configMux.Unlock() } +func UpdateListeners(newListeners map[string]C.InboundListener) { + configMux.Lock() + defer configMux.Unlock() + listeners = newListeners +} + func UpdateSniffer(dispatcher *sniffer.SnifferDispatcher) { configMux.Lock() sniffer.Dispatcher = dispatcher - sniffingEnable = true + sniffingEnable = dispatcher.Enable() configMux.Unlock() } @@ -159,11 +183,6 @@ func preHandleMetadata(metadata *C.Metadata) error { if ip, err := netip.ParseAddr(metadata.Host); err == nil { metadata.DstIP = ip metadata.Host = "" - if ip.Is4() { - metadata.AddrType = C.AtypIPv4 - } else { - metadata.AddrType = C.AtypIPv6 - } } // preprocess enhanced-mode metadata @@ -171,14 +190,13 @@ func preHandleMetadata(metadata *C.Metadata) error { host, exist := resolver.FindHostByIP(metadata.DstIP) if exist { metadata.Host = host - metadata.AddrType = C.AtypDomainName metadata.DNSMode = C.DNSMapping if resolver.FakeIPEnabled() { metadata.DstIP = netip.Addr{} metadata.DNSMode = C.DNSFakeIP } else if node := resolver.DefaultHosts.Search(host); node != nil { // redir-host should lookup the hosts - metadata.DstIP = node.Data + metadata.DstIP = node.Data() } } else if resolver.IsFakeIP(metadata.DstIP) { return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) @@ -188,7 +206,16 @@ func preHandleMetadata(metadata *C.Metadata) error { return nil } -func resolveMetadata(_ C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { +func resolveMetadata(ctx C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { + if metadata.SpecialProxy != "" { + var exist bool + proxy, exist = proxies[metadata.SpecialProxy] + if !exist { + err = fmt.Errorf("proxy %s not found", metadata.SpecialProxy) + } + return + } + switch mode { case Direct: proxy = proxies["DIRECT"] @@ -201,7 +228,7 @@ func resolveMetadata(_ C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rul return } -func handleUDPConn(packet *inbound.PacketAdapter) { +func handleUDPConn(packet C.PacketAdapter) { metadata := packet.Metadata() if !metadata.Valid() { log.Warnln("[Metadata] not valid: %#v", metadata) @@ -221,7 +248,7 @@ func handleUDPConn(packet *inbound.PacketAdapter) { // local resolve UDP dns if !metadata.Resolved() { - ip, err := resolver.ResolveIP(metadata.Host) + ip, err := resolver.ResolveIP(context.Background(), metadata.Host) if err != nil { return } @@ -269,13 +296,22 @@ func handleUDPConn(packet *inbound.PacketAdapter) { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) defer cancel() - rawPc, err := proxy.ListenPacketContext(ctx, metadata.Pure()) - if err != nil { + rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) { + return proxy.ListenPacketContext(ctx, metadata.Pure()) + }, func(err error) { if rule == nil { - log.Warnln("[UDP] dial %s to %s error: %s", proxy.Name(), metadata.RemoteAddress(), err.Error()) + log.Warnln( + "[UDP] dial %s %s --> %s error: %s", + proxy.Name(), + metadata.SourceAddress(), + metadata.RemoteAddress(), + err.Error(), + ) } else { - log.Warnln("[UDP] dial %s (match %s/%s) to %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.RemoteAddress(), err.Error()) + log.Warnln("[UDP] dial %s (match %s/%s) %s --> %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceAddress(), metadata.RemoteAddress(), err.Error()) } + }) + if err != nil { return } pCtx.InjectPacketConn(rawPc) @@ -283,6 +319,8 @@ func handleUDPConn(packet *inbound.PacketAdapter) { pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule) switch true { + case metadata.SpecialProxy != "": + log.Infoln("[UDP] %s --> %s using %s", metadata.SourceAddress(), metadata.RemoteAddress(), metadata.SpecialProxy) case rule != nil: if rule.Payload() != "" { log.Infoln("[UDP] %s --> %s match %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), rawPc.Chains().String()) @@ -298,7 +336,7 @@ func handleUDPConn(packet *inbound.PacketAdapter) { } oAddr := metadata.DstIP - go handleUDPToLocal(packet.UDPPacket, pc, key, oAddr, fAddr) + go handleUDPToLocal(packet, pc, key, oAddr, fAddr) natTable.Set(key, pc) handle() @@ -334,21 +372,32 @@ func handleTCPConn(connCtx C.ConnContext) { dialMetadata := metadata if len(metadata.Host) > 0 { if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - dialMetadata.DstIP = node.Data - dialMetadata.DNSMode = C.DNSHosts - dialMetadata = dialMetadata.Pure() + if dstIp := node.Data(); !FakeIPRange().Contains(dstIp) { + dialMetadata.DstIP = dstIp + dialMetadata.DNSMode = C.DNSHosts + dialMetadata = dialMetadata.Pure() + } } } ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) defer cancel() - remoteConn, err := proxy.DialContext(ctx, dialMetadata) - if err != nil { + remoteConn, err := retry(ctx, func(ctx context.Context) (C.Conn, error) { + return proxy.DialContext(ctx, dialMetadata) + }, func(err error) { if rule == nil { - log.Warnln("[TCP] dial %s to %s error: %s", proxy.Name(), metadata.RemoteAddress(), err.Error()) + log.Warnln( + "[TCP] dial %s %s --> %s error: %s", + proxy.Name(), + metadata.SourceAddress(), + metadata.RemoteAddress(), + err.Error(), + ) } else { - log.Warnln("[TCP] dial %s (match %s(%s)) to %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.RemoteAddress(), err.Error()) + log.Warnln("[TCP] dial %s (match %s/%s) %s --> %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceAddress(), metadata.RemoteAddress(), err.Error()) } + }) + if err != nil { return } @@ -358,6 +407,8 @@ func handleTCPConn(connCtx C.ConnContext) { }(remoteConn) switch true { + case metadata.SpecialProxy != "": + log.Infoln("[TCP] %s --> %s using %s", metadata.SourceAddress(), metadata.RemoteAddress(), metadata.SpecialProxy) case rule != nil: if rule.Payload() != "" { log.Infoln("[TCP] %s --> %s match %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), remoteConn.Chains().String()) @@ -369,7 +420,11 @@ func handleTCPConn(connCtx C.ConnContext) { case mode == Direct: log.Infoln("[TCP] %s --> %s using DIRECT", metadata.SourceDetail(), metadata.RemoteAddress()) default: - log.Infoln("[TCP] %s --> %s doesn't match any rule using DIRECT", metadata.SourceAddress(), metadata.RemoteAddress()) + log.Infoln( + "[TCP] %s --> %s doesn't match any rule using DIRECT", + metadata.SourceAddress(), + metadata.RemoteAddress(), + ) } handleSocket(connCtx, remoteConn) @@ -388,20 +443,24 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { ) if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - metadata.DstIP = node.Data + metadata.DstIP = node.Data() resolved = true } - for _, rule := range rules { + for _, rule := range getRules(metadata) { if !resolved && shouldResolveIP(rule, metadata) { - ip, err := resolver.ResolveIP(metadata.Host) - if err != nil { - log.Debugln("[DNS] resolve %s error: %s", metadata.Host, err.Error()) - } else { - log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String()) - metadata.DstIP = ip - } - resolved = true + func() { + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + defer cancel() + ip, err := resolver.ResolveIP(ctx, metadata.Host) + if err != nil { + log.Debugln("[DNS] resolve %s error: %s", metadata.Host, err.Error()) + } else { + log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String()) + metadata.DstIP = ip + } + resolved = true + }() } if !processFound && (alwaysFindProcess || rule.ShouldFindProcess()) { @@ -412,9 +471,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { } else { metadata.Process = filepath.Base(path) metadata.ProcessPath = path - if uid != -1 { - metadata.Uid = &uid - } + metadata.Uid = uid processFound = true } } @@ -449,3 +506,39 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { return proxies["DIRECT"], nil, nil } + +func getRules(metadata *C.Metadata) []C.Rule { + if sr, ok := subRules[metadata.SpecialRules]; ok { + log.Debugln("[Rule] use %s rules", metadata.SpecialRules) + return sr + } else { + log.Debugln("[Rule] use default rules") + return rules + } +} + +func retry[T any](ctx context.Context, ft func(context.Context) (T, error), fe func(err error)) (t T, err error) { + b := &backoff.Backoff{ + Min: 10 * time.Millisecond, + Max: 1 * time.Second, + Factor: 2, + Jitter: true, + } + for i := 0; i < 10; i++ { + t, err = ft(ctx) + if err != nil { + if fe != nil { + fe(err) + } + select { + case <-time.After(b.Duration()): + continue + case <-ctx.Done(): + return + } + } else { + break + } + } + return +}