Compare commits

..

2 Commits

Author SHA1 Message Date
世界
637a8b6ed5 refactor: udp 2022-06-09 21:16:42 +08:00
世界
cd466f05d3 chore: reformat code 2022-06-09 21:08:46 +08:00
393 changed files with 10272 additions and 26184 deletions

View File

@@ -5,14 +5,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code into the Go module directory - name: Set up Go
uses: actions/checkout@v3 uses: actions/setup-go@v1
- name: Setup Go
uses: actions/setup-go@v3
with: with:
go-version: '1.19' go-version: 1.18
check-latest: true - name: Check out code
cache: true uses: actions/checkout@v1
- name: Build - name: Build
run: make all run: make all
- name: Release - name: Release

View File

@@ -1,6 +1,5 @@
name: Prerelease name: Prerelease
on: on:
workflow_dispatch:
push: push:
branches: branches:
- Alpha - Alpha
@@ -13,15 +12,26 @@ jobs:
Build: Build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Go - name: Cache go module
uses: actions/setup-go@v3 uses: actions/cache@v2
with: with:
go-version: '1.19' path: ~/go/pkg/mod
check-latest: true key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
cache: true restore-keys: |
${{ runner.os }}-go-
- name: Test - name: Test
if: ${{github.ref_name=='Beta'}} if: ${{github.ref_name=='Beta'}}
@@ -36,16 +46,14 @@ jobs:
run: make -j$(($(nproc) + 1)) releases run: make -j$(($(nproc) + 1)) releases
- name: Delete current release assets - name: Delete current release assets
uses: mknejp/delete-release-assets@v1 uses: andreaswilli/delete-release-assets-action@v2.0.0
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
tag_name: Prerelease-${{ github.ref_name }} tag: Prerelease-${{ github.ref_name }}
assets: | deleteOnlyFromDrafts: false
*.zip
*.gz
- name: Tag Repo - name: Tag Repo
uses: richardsimko/update-tag@v1.0.6 uses: richardsimko/update-tag@v1
with: with:
tag_name: Prerelease-${{ github.ref_name }} tag_name: Prerelease-${{ github.ref_name }}
env: env:
@@ -55,6 +63,7 @@ jobs:
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ success() }} if: ${{ success() }}
with: with:
tag: ${{ github.ref_name }}
tag_name: Prerelease-${{ github.ref_name }} tag_name: Prerelease-${{ github.ref_name }}
files: bin/* files: bin/*
prerelease: true prerelease: true

View File

@@ -7,16 +7,24 @@ jobs:
Build: Build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Cache go module
- name: Setup Go uses: actions/cache@v2
uses: actions/setup-go@v3
with: with:
go-version: '1.19' path: ~/go/pkg/mod
check-latest: true key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
cache: true restore-keys: |
${{ runner.os }}-go-
- name: Test - name: Test
run: | run: |
go test ./... go test ./...

View File

@@ -8,10 +8,9 @@ linters:
linters-settings: linters-settings:
gci: gci:
custom-order: true
sections: sections:
- standard - standard
- prefix(github.com/Dreamacro/clash) - prefix(github.com/Dreamacro/clash)
- default - default
staticcheck: staticcheck:
go: '1.19' go: '1.18'

View File

@@ -16,11 +16,11 @@ RUN go mod download &&\
FROM alpine:latest FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta" LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta"
RUN apk add --no-cache ca-certificates tzdata iptables RUN apk add --no-cache ca-certificates tzdata
VOLUME ["/root/.config/clash/"] VOLUME ["/root/.config/clash/"]
COPY --from=builder /clash-config/ /root/.config/clash/ COPY --from=builder /clash-config/ /root/.config/clash/
COPY --from=builder /clash /clash COPY --from=builder /clash /clash
RUN chmod +x /clash RUN chmod +x /clash
ENTRYPOINT [ "/clash" ] ENTRYPOINT [ "/clash" ]

View File

@@ -12,7 +12,7 @@ VERSION=$(shell git rev-parse --short HEAD)
endif endif
BUILDTIME=$(shell date -u) BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s -buildid=' -w -s -buildid='
@@ -147,11 +147,3 @@ lint:
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*
CLANG ?= clang-14
CFLAGS := -O2 -g -Wall -Werror $(CFLAGS)
ebpf: export BPF_CLANG := $(CLANG)
ebpf: export BPF_CFLAGS := $(CFLAGS)
ebpf:
cd component/ebpf/ && go generate ./...

View File

@@ -34,26 +34,6 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash
## Advanced usage for this branch ## Advanced usage for this branch
## Build
You should install [golang](https://go.dev) first.
Then get the source code of Clash.Meta:
```shell
git clone https://github.com/MetaCubeX/Clash.Meta.git
cd Clash.Meta && go mod download
```
If you can't visit github,you should set proxy first:
```shell
go env -w GOPROXY=https://goproxy.io,direct
```
So now you can build it:
```shell
go build
```
### DNS configuration ### DNS configuration
Support `geosite` with `fallback-filter`. Support `geosite` with `fallback-filter`.
@@ -232,43 +212,6 @@ proxies:
grpc-service-name: grpcname 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 ### IPTABLES configuration
Work on Linux OS who's supported `iptables` Work on Linux OS who's supported `iptables`
@@ -308,8 +251,8 @@ User=clash-meta
Group=clash-meta Group=clash-meta
LimitNPROC=500 LimitNPROC=500
LimitNOFILE=1000000 LimitNOFILE=1000000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CapabilityBoundingSet=cap_net_admin
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE AmbientCapabilities=cap_net_admin
Restart=always Restart=always
ExecStartPre=/usr/bin/sleep 1s ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
@@ -331,7 +274,7 @@ $ systemctl start Clash-Meta
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`. Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
To display process name in GUI please use [Dashboard For Meta](https://github.com/MetaCubeX/clash-dashboard). To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard).
![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png) ![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png)
@@ -343,7 +286,6 @@ the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library
## Credits ## Credits
* [Dreamacro/clash](https://github.com/Dreamacro/clash) * [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) * [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) * [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go) * [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)

View File

@@ -4,15 +4,16 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@@ -39,6 +40,11 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...) conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
wasCancel := false
if err != nil {
wasCancel = strings.Contains(err.Error(), "operation was canceled")
}
p.alive.Store(err == nil || wasCancel)
return conn, err return conn, err
} }
@@ -52,6 +58,7 @@ func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err return pc, err
} }
@@ -92,7 +99,6 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() mapping["udp"] = p.SupportUDP()
mapping["tfo"] = p.SupportTFO()
return json.Marshal(mapping) return json.Marshal(mapping)
} }
@@ -145,32 +151,25 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
} }
client := http.Client{ client := http.Client{
Timeout: 30 * time.Second,
Transport: transport, Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
} }
_ = resp.Body.Close()
if unifiedDelay { if unifiedDelay {
second := time.Now() start = time.Now()
resp, err = client.Do(req) resp, err = client.Do(req)
if err == nil { if err != nil {
_ = resp.Body.Close() return
start = second
} }
} }
_ = resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
@@ -199,9 +198,10 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
} }
addr = C.Metadata{ addr = C.Metadata{
Host: u.Hostname(), AddrType: C.AtypDomainName,
DstIP: netip.Addr{}, Host: u.Hostname(),
DstPort: port, DstIP: netip.Addr{},
DstPort: port,
} }
return return
} }

View File

@@ -1,29 +0,0 @@
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
}
}

View File

@@ -9,20 +9,13 @@ import (
) )
// NewHTTP receive normal http request and return HTTPContext // NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn, additions ...Addition) *context.ConnContext { func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.HTTP metadata.Type = C.HTTP
for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(source.String()); err == nil { if ip, port, err := parseAddr(source.String()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
if ip, port, err := parseAddr(conn.LocalAddr().String()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }

View File

@@ -9,19 +9,12 @@ import (
) )
// NewHTTPS receive CONNECT request and return ConnContext // NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) *context.ConnContext { func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata := parseHTTPAddr(request) metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPS metadata.Type = C.HTTPS
for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil { if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
if ip, port, err := parseAddr(conn.LocalAddr().String()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }

View File

@@ -1,26 +0,0 @@
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)
}

View File

@@ -2,7 +2,7 @@ package inbound
import ( import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" M "github.com/sagernet/sing/common/metadata"
) )
// PacketAdapter is a UDP Packet adapter for socks/redir/tun // PacketAdapter is a UDP Packet adapter for socks/redir/tun
@@ -17,26 +17,17 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
} }
// NewPacket is PacketAdapter generator // NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter { func NewPacket(target M.Socksaddr, packet C.UDPPacket, source C.Type) *PacketAdapter {
metadata := parseSocksAddr(target) metadata := socksAddrToMetadata(target)
metadata.NetWork = C.UDP metadata.NetWork = C.UDP
metadata.Type = source metadata.Type = source
for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil { if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port 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{ return &PacketAdapter{
packet, UDPPacket: packet,
metadata, metadata: metadata,
} }
} }

View File

@@ -10,16 +10,11 @@ import (
) )
// NewSocket receive TCP inbound and return ConnContext // NewSocket receive TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) *context.ConnContext { func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = source metadata.Type = source
for _, addition := range additions {
addition.Apply(metadata)
}
remoteAddr := conn.RemoteAddr() remoteAddr := conn.RemoteAddr()
// Filter when net.Addr interface is nil // Filter when net.Addr interface is nil
if remoteAddr != nil { if remoteAddr != nil {
if ip, port, err := parseAddr(remoteAddr.String()); err == nil { if ip, port, err := parseAddr(remoteAddr.String()); err == nil {
@@ -27,14 +22,6 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Ad
metadata.SrcPort = port 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) return context.NewConnContext(conn, metadata)
} }
@@ -45,12 +32,17 @@ func NewInner(conn net.Conn, dst string, host string) *context.ConnContext {
metadata.Type = C.INNER metadata.Type = C.INNER
metadata.DNSMode = C.DNSMapping metadata.DNSMode = C.DNSMapping
metadata.Host = host metadata.Host = host
metadata.AddrType = C.AtypDomainName
metadata.Process = C.ClashName metadata.Process = C.ClashName
if h, port, err := net.SplitHostPort(dst); err == nil { if h, port, err := net.SplitHostPort(dst); err == nil {
metadata.DstPort = port metadata.DstPort = port
if host == "" { if host == "" {
if ip, err := netip.ParseAddr(h); err == nil { if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip metadata.DstIP = ip
metadata.AddrType = C.AtypIPv4
if ip.Is6() {
metadata.AddrType = C.AtypIPv6
}
} }
} }
} }

View File

@@ -1,19 +1,39 @@
package inbound package inbound
import ( import (
"github.com/Dreamacro/clash/common/nnip"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
M "github.com/sagernet/sing/common/metadata"
) )
func parseSocksAddr(target socks5.Addr) *C.Metadata { func socksAddrToMetadata(addr M.Socksaddr) *C.Metadata {
metadata := &C.Metadata{} metadata := &C.Metadata{}
switch addr.Family() {
case M.AddressFamilyIPv4:
metadata.AddrType = C.AtypIPv4
metadata.DstIP = addr.Addr
case M.AddressFamilyIPv6:
metadata.AddrType = C.AtypIPv6
metadata.DstIP = addr.Addr
case M.AddressFamilyFqdn:
metadata.AddrType = C.AtypDomainName
metadata.Host = addr.Fqdn
}
metadata.DstPort = strconv.Itoa(int(addr.Port))
return metadata
}
func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata := &C.Metadata{
AddrType: int(target[0]),
}
switch target[0] { switch target[0] {
case socks5.AtypDomainName: case socks5.AtypDomainName:
@@ -24,8 +44,7 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len])) metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6: case socks5.AtypIPv6:
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len]) metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv6len]))
metadata.DstIP = ip6.Unmap()
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
@@ -43,14 +62,21 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
host = strings.TrimRight(host, ".") host = strings.TrimRight(host, ".")
metadata := &C.Metadata{ metadata := &C.Metadata{
NetWork: C.TCP, NetWork: C.TCP,
Host: host, AddrType: C.AtypDomainName,
DstIP: netip.Addr{}, Host: host,
DstPort: port, DstIP: netip.Addr{},
DstPort: port,
} }
ip, err := netip.ParseAddr(host) ip, err := netip.ParseAddr(host)
if err == nil { if err == nil {
switch {
case ip.Is6():
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = ip metadata.DstIP = ip
} }

View File

@@ -4,24 +4,26 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/gofrs/uuid"
"net" "net"
"strings" "strings"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
) )
type Base struct { type Base struct {
name string name string
addr string addr string
iface string iface string
tp C.AdapterType tp C.AdapterType
udp bool udp bool
tfo bool rmark int
rmark int id string
id string
prefer C.DNSPrefer
} }
// Name implements C.ProxyAdapter // Name implements C.ProxyAdapter
@@ -57,26 +59,16 @@ func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di
return nil, errors.New("no support") 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 // ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support") return nil, errors.New("no support")
} }
// ListenPacketWithDialer implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return nil, errors.New("no support") return nil, errors.New("no support")
} }
// SupportWithDialer implements C.ProxyAdapter
func (b *Base) SupportWithDialer() bool {
return false
}
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
func (b *Base) SupportUOT() bool { func (b *Base) SupportUOT() bool {
return false return false
@@ -87,11 +79,6 @@ func (b *Base) SupportUDP() bool {
return b.udp return b.udp
} }
// SupportTFO implements C.ProxyAdapter
func (b *Base) SupportTFO() bool {
return b.tfo
}
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) { func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{ return json.Marshal(map[string]string{
@@ -106,7 +93,7 @@ func (b *Base) Addr() string {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil return nil
} }
@@ -120,25 +107,12 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
opts = append(opts, dialer.WithRoutingMark(b.rmark)) opts = append(opts, dialer.WithRoutingMark(b.rmark))
} }
switch b.prefer {
case C.IPv4Only:
opts = append(opts, dialer.WithOnlySingleStack(true))
case C.IPv6Only:
opts = append(opts, dialer.WithOnlySingleStack(false))
case C.IPv4Prefer:
opts = append(opts, dialer.WithPreferIPv4())
case C.IPv6Prefer:
opts = append(opts, dialer.WithPreferIPv6())
default:
}
return opts return opts
} }
type BasicOption struct { type BasicOption struct {
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
} }
type BaseOption struct { type BaseOption struct {
@@ -146,22 +120,18 @@ type BaseOption struct {
Addr string Addr string
Type C.AdapterType Type C.AdapterType
UDP bool UDP bool
TFO bool
Interface string Interface string
RoutingMark int RoutingMark int
Prefer C.DNSPrefer
} }
func NewBase(opt BaseOption) *Base { func NewBase(opt BaseOption) *Base {
return &Base{ return &Base{
name: opt.Name, name: opt.Name,
addr: opt.Addr, addr: opt.Addr,
tp: opt.Type, tp: opt.Type,
udp: opt.UDP, udp: opt.UDP,
tfo: opt.TFO, iface: opt.Interface,
iface: opt.Interface, rmark: opt.RoutingMark,
rmark: opt.RoutingMark,
prefer: opt.Prefer,
} }
} }
@@ -191,6 +161,7 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
type packetConn struct { type packetConn struct {
net.PacketConn net.PacketConn
nc N.PacketConn
chain C.Chain chain C.Chain
actualRemoteDestination string actualRemoteDestination string
} }
@@ -209,8 +180,22 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name()) c.chain = append(c.chain, a.Name())
} }
func (c *packetConn) ReadPacket(buffer *buf.Buffer) (addr M.Socksaddr, err error) {
return c.nc.ReadPacket(buffer)
}
func (c *packetConn) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
return c.nc.WritePacket(buffer, addr)
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())} var nc N.PacketConn
if n, isNc := pc.(N.PacketConn); isNc {
nc = n
} else {
nc = &bufio.PacketConnWrapper{PacketConn: pc}
}
return &packetConn{pc, nc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
} }
func parseRemoteDestination(addr string) string { func parseRemoteDestination(addr string) string {

View File

@@ -5,7 +5,6 @@ import (
"net" "net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@@ -15,7 +14,7 @@ type Direct struct {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) opts = append(opts, dialer.WithDirect())
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -26,8 +25,8 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) opts = append(opts, dialer.WithDirect())
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -41,10 +40,9 @@ type directPacketConn struct {
func NewDirect() *Direct { func NewDirect() *Direct {
return &Direct{ return &Direct{
Base: &Base{ Base: &Base{
name: "DIRECT", name: "DIRECT",
tp: C.Direct, tp: C.Direct,
udp: true, udp: true,
prefer: C.DualStack,
}, },
} }
} }
@@ -52,10 +50,9 @@ func NewDirect() *Direct {
func NewCompatible() *Direct { func NewCompatible() *Direct {
return &Direct{ return &Direct{
Base: &Base{ Base: &Base{
name: "COMPATIBLE", name: "COMPATIBLE",
tp: C.Compatible, tp: C.Compatible,
udp: true, udp: true,
prefer: C.DualStack,
}, },
} }
} }

View File

@@ -7,7 +7,6 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
tlsC "github.com/Dreamacro/clash/component/tls"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -36,7 +35,6 @@ type HttpOption struct {
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
} }
@@ -44,9 +42,7 @@ type HttpOption struct {
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil { if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig) cc := tls.Client(c, h.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) err := cc.Handshake()
defer cancel()
err := cc.HandshakeContext(ctx)
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
@@ -61,20 +57,13 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return h.DialContextWithDialer(ctx, dialer.NewDialer(h.Base.DialOptions(opts...)...), metadata) c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = h.StreamConn(c, metadata) c, err = h.StreamConn(c, metadata)
if err != nil { if err != nil {
@@ -84,11 +73,6 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
return NewConn(c, h), nil 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 { func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
addr := metadata.RemoteAddress() addr := metadata.RemoteAddress()
req := &http.Request{ req := &http.Request{
@@ -102,7 +86,7 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
}, },
} }
//增加headers // 增加headers
if len(h.option.Headers) != 0 { if len(h.option.Headers) != 0 {
for key, value := range h.option.Headers { for key, value := range h.option.Headers {
req.Header.Add(key, value) req.Header.Add(key, value)
@@ -142,41 +126,30 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode) return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
} }
func NewHttp(option HttpOption) (*Http, error) { func NewHttp(option HttpOption) *Http {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
sni := option.Server sni := option.Server
if option.SNI != "" { if option.SNI != "" {
sni = option.SNI sni = option.SNI
} }
if len(option.Fingerprint) == 0 { tlsConfig = &tls.Config{
tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(&tls.Config{ InsecureSkipVerify: option.SkipCertVerify,
InsecureSkipVerify: option.SkipCertVerify, ServerName: sni,
ServerName: sni,
})
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
}, option.Fingerprint); err != nil {
return nil, err
}
} }
} }
return &Http{ return &Http{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
option: &option, option: &option,
}, nil }
} }

View File

@@ -2,43 +2,40 @@ package outbound
import ( import (
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/hex" "errors"
"encoding/pem"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/netip"
"os"
"regexp" "regexp"
"strconv" "strconv"
"time" "time"
"github.com/metacubex/quic-go"
"github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion" "github.com/lucas-clemente/quic-go"
"github.com/Dreamacro/clash/transport/hysteria/core" "github.com/lucas-clemente/quic-go/congestion"
"github.com/Dreamacro/clash/transport/hysteria/obfs" M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/Dreamacro/clash/transport/hysteria/transport" "github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/tobyxdd/hysteria/pkg/pmtud_fix"
"github.com/tobyxdd/hysteria/pkg/transport"
) )
const ( const (
mbpsToBps = 125000 mbpsToBps = 125000
minSpeedBPS = 16384
DefaultStreamReceiveWindow = 15728640 // 15 MB/s DefaultStreamReceiveWindow = 15728640 // 15 MB/s
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
DefaultMaxIncomingStreams = 1024
DefaultALPN = "hysteria" DefaultALPN = "hysteria"
DefaultProtocol = "udp" DefaultProtocol = "udp"
DefaultHopInterval = 10
) )
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
@@ -46,39 +43,24 @@ var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct { type Hysteria struct {
*Base *Base
client *core.Client client *core.Client
clientTransport *transport.ClientTransport
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
hdc := hyDialerWithContext{ tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), hyDialer(func() (net.PacketConn, error) {
ctx: context.Background(), 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(ctx, "udp", addr, h.prefer)
},
}
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), &hdc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(tcpConn, h), nil return NewConn(tcpConn, h), nil
} }
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
hdc := hyDialerWithContext{ udpConn, err := h.client.DialUDP(hyDialer(func() (net.PacketConn, error) {
ctx: context.Background(), 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(ctx, "udp", addr, h.prefer)
},
}
udpConn, err := h.client.DialUDP(&hdc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -87,44 +69,45 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
type HysteriaOption struct { type HysteriaOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port,omitempty"` Port int `proxy:"port"`
Ports string `proxy:"ports,omitempty"` Protocol string `proxy:"protocol,omitempty"`
Protocol string `proxy:"protocol,omitempty"` Up string `proxy:"up,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash UpMbps int `proxy:"up_mbps,omitempty"`
Up string `proxy:"up"` Down string `proxy:"down,omitempty"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash DownMbps int `proxy:"down_mbps,omitempty"`
Down string `proxy:"down"` Auth string `proxy:"auth,omitempty"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash AuthString string `proxy:"auth_str,omitempty"`
Auth string `proxy:"auth,omitempty"` Obfs string `proxy:"obfs,omitempty"`
AuthString string `proxy:"auth-str,omitempty"` SNI string `proxy:"sni,omitempty"`
Obfs string `proxy:"obfs,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
SNI string `proxy:"sni,omitempty"` ALPN string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` CustomCA string `proxy:"ca,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` CustomCAString string `proxy:"ca_str,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` ReceiveWindowConn int `proxy:"recv_window_conn,omitempty"`
CustomCA string `proxy:"ca,omitempty"` ReceiveWindow int `proxy:"recv_window,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"` DisableMTUDiscovery bool `proxy:"disable_mtu_discovery,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) { func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64 var up, down uint64
up = stringToBps(c.Up) if len(c.Up) > 0 {
if up == 0 { up = stringToBps(c.Up)
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) if up == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
up = uint64(c.UpMbps) * mbpsToBps
} }
if len(c.Down) > 0 {
down = stringToBps(c.Down) down = stringToBps(c.Down)
if down == 0 { if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) return 0, 0, errors.New("invalid speed format")
}
} else {
down = uint64(c.DownMbps) * mbpsToBps
} }
return up, down, nil return up, down, nil
} }
@@ -134,131 +117,96 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
Timeout: 8 * time.Second, Timeout: 8 * time.Second,
}, },
} }
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
ports := option.Ports
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
serverName := option.Server serverName := option.Server
if option.SNI != "" { if option.SNI != "" {
serverName = option.SNI serverName = option.Server
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: serverName, ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13, MinVersion: tls.VersionTLS13,
} }
if len(option.ALPN) > 0 {
var bs []byte tlsConfig.NextProtos = []string{option.ALPN}
var err error } else {
tlsConfig.NextProtos = []string{DefaultALPN}
}
if len(option.CustomCA) > 0 { if len(option.CustomCA) > 0 {
bs, err = os.ReadFile(option.CustomCA) bs, err := ioutil.ReadFile(option.CustomCA)
if err != nil { if err != nil {
return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err) return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err)
} }
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
}
tlsConfig.RootCAs = cp
} else if option.CustomCAString != "" { } else if option.CustomCAString != "" {
bs = []byte(option.CustomCAString) cp := x509.NewCertPool()
} if !cp.AppendCertsFromPEM([]byte(option.CustomCAString)) {
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
if len(bs) > 0 {
block, _ := pem.Decode(bs)
if block == nil {
return nil, fmt.Errorf("CA cert is not PEM")
} }
tlsConfig.RootCAs = cp
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{DefaultALPN}
} }
quicConfig := &quic.Config{ quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
KeepAlivePeriod: 10 * time.Second, KeepAlive: true,
DisablePathMTUDiscovery: option.DisableMTUDiscovery, DisablePathMTUDiscovery: option.DisableMTUDiscovery,
EnableDatagrams: true, EnableDatagrams: true,
} }
if option.ObfsProtocol != "" {
option.Protocol = option.ObfsProtocol
}
if option.Protocol == "" { if option.Protocol == "" {
option.Protocol = DefaultProtocol option.Protocol = DefaultProtocol
} }
if option.HopInterval == 0 { if option.ReceiveWindowConn == 0 {
option.HopInterval = DefaultHopInterval quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow
}
hopInterval := time.Duration(int64(option.HopInterval)) * time.Second
if option.ReceiveWindow == 0 {
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
} }
if option.ReceiveWindow == 0 { if option.ReceiveWindow == 0 {
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10 quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
} }
if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery { if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery {
log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform") log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform")
} }
var auth []byte
var auth = []byte(option.AuthString)
if option.Auth != "" { if option.Auth != "" {
auth, err = base64.StdEncoding.DecodeString(option.Auth) authBytes, err := base64.StdEncoding.DecodeString(option.Auth)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("hysteria %s parse auth error: %w", addr, err)
} }
auth = authBytes
} else {
auth = []byte(option.AuthString)
} }
var obfuscator obfs.Obfuscator var obfuscator obfs.Obfuscator
if len(option.Obfs) > 0 { if len(option.Obfs) > 0 {
obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs)) obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs))
} }
up, down, _ := option.Speed()
up, down, err := option.Speed()
if err != nil {
return nil, err
}
if option.UpSpeed != 0 {
up = uint64(option.UpSpeed * mbpsToBps)
}
if option.DownSpeed != 0 {
down = uint64(option.DownSpeed * mbpsToBps)
}
client, err := core.NewClient( client, err := core.NewClient(
addr, ports, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { addr, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator, hopInterval, option.FastOpen, }, obfuscator,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err) return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
} }
return &Hysteria{ return &Hysteria{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Hysteria, tp: C.Hysteria,
udp: true, udp: true,
tfo: option.FastOpen, iface: option.Interface,
iface: option.Interface, rmark: option.RoutingMark,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
clientTransport: clientTransport,
}, nil }, nil
} }
@@ -266,12 +214,6 @@ func stringToBps(s string) uint64 {
if s == "" { if s == "" {
return 0 return 0
} }
// when have not unit, use Mbps
if v, err := strconv.Atoi(s); err == nil {
return stringToBps(fmt.Sprintf("%d Mbps", v))
}
m := rateStringRegexp.FindStringSubmatch(s) m := rateStringRegexp.FindStringSubmatch(s)
if m == nil { if m == nil {
return 0 return 0
@@ -321,24 +263,8 @@ func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return return
} }
type hyDialerWithContext struct { type 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(rAddr net.Addr) (net.PacketConn, error) { func (h hyDialer) ListenPacket() (net.PacketConn, error) {
network := "udp" return h()
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 {
return h.ctx
}
func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) {
return h.remoteAddr(host)
} }

View File

@@ -27,10 +27,9 @@ func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
func NewReject() *Reject { func NewReject() *Reject {
return &Reject{ return &Reject{
Base: &Base{ Base: &Base{
name: "REJECT", name: "REJECT",
tp: C.Reject, tp: C.Reject,
udp: true, udp: true,
prefer: C.DualStack,
}, },
} }
} }
@@ -38,10 +37,9 @@ func NewReject() *Reject {
func NewPass() *Reject { func NewPass() *Reject {
return &Reject{ return &Reject{
Base: &Base{ Base: &Base{
name: "PASS", name: "PASS",
tp: C.Pass, tp: C.Pass,
udp: true, udp: true,
prefer: C.DualStack,
}, },
} }
} }

View File

@@ -13,19 +13,16 @@ import (
obfs "github.com/Dreamacro/clash/transport/simple-obfs" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/sagernet/sing-shadowsocks"
"github.com/metacubex/sing-shadowsocks" "github.com/sagernet/sing-shadowsocks/shadowimpl"
"github.com/metacubex/sing-shadowsocks/shadowimpl"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
) )
type ShadowSocks struct { type ShadowSocks struct {
*Base *Base
method shadowsocks.Method method shadowsocks.Method
option *ShadowSocksOption
// obfs // obfs
obfsMode string obfsMode string
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
@@ -42,7 +39,6 @@ type ShadowSocksOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
} }
type simpleObfsOption struct { type simpleObfsOption struct {
@@ -55,7 +51,6 @@ type v2rayObfsOption struct {
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"` Mux bool `obfs:"mux,omitempty"`
@@ -76,28 +71,18 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
} }
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
return ss.method.DialConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443"))
}
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = ss.StreamConn(c, metadata) c, err = ss.StreamConn(c, metadata)
return NewConn(c, ss), err return NewConn(c, ss), err
@@ -105,49 +90,20 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { 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) pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
}
// 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.DialContextWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
return newPacketConn(uot.NewClientConn(tcpConn), ss), nil
}
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr}) pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
return newPacketConn(pc, ss), nil 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 {
return newPacketConn(uot.NewClientConn(c), ss), nil
}
return nil, errors.New("no support")
}
// SupportUOT implements C.ProxyAdapter
func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowimpl.FetchMethod(option.Cipher, option.Password) method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
@@ -196,17 +152,15 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
return &ShadowSocks{ return &ShadowSocks{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
method: method, method: method,
option: &option,
obfsMode: obfsMode, obfsMode: obfsMode,
v2rayOption: v2rayOption, v2rayOption: v2rayOption,
obfsOption: obfsOption, obfsOption: obfsOption,

View File

@@ -60,20 +60,13 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata) c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = ssr.StreamConn(c, metadata) c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err return NewConn(c, ssr), err
@@ -81,18 +74,14 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata) pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, err return nil, err
} }
pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) addr, err := resolveUDPAddr("udp", ssr.addr)
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
@@ -101,11 +90,6 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil 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) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility // SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056 // https://github.com/Dreamacro/clash/pull/2056
@@ -159,13 +143,12 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
return &ShadowSocksR{ return &ShadowSocksR{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
cipher: coreCiph, cipher: coreCiph,
obfs: obfs, obfs: obfs,

View File

@@ -78,20 +78,13 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return NewConn(c, s), err return NewConn(c, s), err
} }
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata) c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err) return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = s.StreamConn(c, metadata) c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err return NewConn(c, s), err
@@ -99,12 +92,7 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata) c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, err return nil, err
} }
@@ -120,9 +108,10 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return newPacketConn(pc, s), nil return newPacketConn(pc, s), nil
} }
// SupportWithDialer implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (s *Snell) SupportWithDialer() bool { func (s *Snell) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return true pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
} }
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
@@ -163,13 +152,12 @@ func NewSnell(option SnellOption) (*Snell, error) {
s := &Snell{ s := &Snell{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Snell, tp: C.Snell,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,

View File

@@ -5,7 +5,6 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
tlsC "github.com/Dreamacro/clash/component/tls"
"io" "io"
"net" "net"
"strconv" "strconv"
@@ -34,16 +33,13 @@ type Socks5Option struct {
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) err := cc.Handshake()
defer cancel()
err := cc.HandshakeContext(ctx)
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
@@ -65,20 +61,13 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = ss.StreamConn(c, metadata) c, err = ss.StreamConn(c, metadata)
if err != nil { if err != nil {
@@ -88,11 +77,6 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
return NewConn(c, ss), nil return NewConn(c, ss), nil
} }
// SupportWithDialer implements C.ProxyAdapter
func (ss *Socks5) SupportWithDialer() bool {
return true
}
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { 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...)...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
@@ -103,15 +87,11 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) err = cc.Handshake()
defer cancel()
err = cc.HandshakeContext(ctx)
c = cc c = cc
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
tcpKeepAlive(c) tcpKeepAlive(c)
var user *socks5.User var user *socks5.User
@@ -128,21 +108,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return return
} }
// Support unspecified UDP bind address. pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return return
} }
@@ -155,43 +121,47 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
pc.Close() 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())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
} }
func NewSocks5(option Socks5Option) (*Socks5, error) { func NewSocks5(option Socks5Option) *Socks5 {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: option.Server, ServerName: option.Server,
} }
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
}
}
} }
return &Socks5{ return &Socks5{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tls: option.TLS, tls: option.TLS,
skipCertVerify: option.SkipCertVerify, skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
}, nil }
} }
type socksPacketConn struct { type socksPacketConn struct {

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
tlsC "github.com/Dreamacro/clash/component/tls"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@@ -36,7 +35,6 @@ type TrojanOption struct {
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
@@ -120,20 +118,14 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return NewConn(c, t), nil return NewConn(c, t), nil
} }
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = t.StreamConn(c, metadata) c, err = t.StreamConn(c, metadata)
if err != nil { if err != nil {
@@ -153,33 +145,18 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err) } else {
}(c) c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.plainStream(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, 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)) err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
@@ -191,11 +168,6 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
// SupportWithDialer implements C.ProxyAdapter
func (t *Trojan) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := t.instance.PacketConn(c) pc := t.instance.PacketConn(c)
@@ -216,7 +188,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: option.Server, ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow, FlowShow: option.FlowShow,
Fingerprint: option.Fingerprint,
} }
if option.Network != "ws" && len(option.Flow) >= 16 { if option.Network != "ws" && len(option.Flow) >= 16 {
@@ -235,13 +206,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t := &Trojan{ t := &Trojan{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
instance: trojan.New(tOption), instance: trojan.New(tOption),
option: &option, option: &option,
@@ -264,15 +234,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
} }
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
}
}
if t.option.Flow != "" { if t.option.Flow != "" {
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig) t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
} else { } else {

View File

@@ -1,242 +0,0 @@
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
}

View File

@@ -2,11 +2,8 @@ package outbound
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
xtls "github.com/xtls/go"
"net" "net"
"net/netip"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@@ -14,6 +11,7 @@ import (
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
xtls "github.com/xtls/go"
) )
var ( var (
@@ -45,11 +43,10 @@ func getClientXSessionCache() xtls.ClientSessionCache {
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
addrType := metadata.AddrType() aType := uint8(metadata.AddrType)
aType := uint8(addrType)
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16) p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType { switch metadata.AddrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:
lenM := uint8(len(metadata.Host)) lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
@@ -64,69 +61,13 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
return bytes.Join(buf, nil) return bytes.Join(buf, nil)
} }
func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) { func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ip, err := resolver.ResolveProxyServerHost(ctx, host) ip, err := resolver.ResolveProxyServerHost(host)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
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(ctx, host)
case C.IPv6Only:
ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host)
case C.IPv6Prefer:
var ips []netip.Addr
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is6() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
default:
// C.IPv4Prefer, C.DualStack and other
var ips []netip.Addr
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is4() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
}
if !ip.IsValid() && fallback.IsValid() {
ip = fallback
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -134,7 +75,7 @@ func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, pref
} }
func safeConnClose(c net.Conn, err error) { func safeConnClose(c net.Conn, err error) {
if err != nil && c != nil { if err != nil {
_ = c.Close() _ = c.Close()
} }
} }

View File

@@ -15,10 +15,8 @@ import (
"github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess" "github.com/Dreamacro/clash/transport/vmess"
) )
@@ -57,7 +55,6 @@ type VlessOption struct {
WSPath string `proxy:"ws-path,omitempty"` WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
} }
@@ -73,39 +70,30 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
wsOpts.Headers.Add(key, value) header.Add(key, value)
} }
wsOpts.Headers = header
} }
if v.option.TLS {
wsOpts.TLS = true
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if len(v.option.Fingerprint) == 0 { wsOpts.TLS = true
wsOpts.TLSConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig) wsOpts.TLSConfig = &tls.Config{
} else { MinVersion: tls.VersionTLS12,
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) ServerName: host,
} InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
if v.option.ServerName != "" { }
wsOpts.TLSConfig.ServerName = v.option.ServerName if v.option.ServerName != "" {
} else if host := wsOpts.Headers.Get("Host"); host != "" { wsOpts.TLSConfig.ServerName = v.option.ServerName
wsOpts.TLSConfig.ServerName = host } else if host := wsOpts.Headers.Get("Host"); host != "" {
} wsOpts.TLSConfig.ServerName = host
} else { } else {
if host := wsOpts.Headers.Get("Host"); host == "" { wsOpts.Headers.Set("Host", convert.RandHost())
wsOpts.Headers.Set("Host", convert.RandHost()) convert.SetUserAgent(wsOpts.Headers)
convert.SetUserAgent(wsOpts.Headers)
}
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
@@ -162,7 +150,6 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
xtlsOpts := vless.XTLSConfig{ xtlsOpts := vless.XTLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
} }
if isH2 { if isH2 {
@@ -179,7 +166,6 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
} }
if isH2 { if isH2 {
@@ -208,9 +194,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
if err != nil { if err != nil {
@@ -219,19 +203,13 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return NewConn(c, v), nil return NewConn(c, v), nil
} }
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err return NewConn(c, v), err
@@ -241,7 +219,7 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { 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 // vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
if err != nil { if err != nil {
return nil, errors.New("can't resolve ip") return nil, errors.New("can't resolve ip")
} }
@@ -255,41 +233,19 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c)
defer safeConnClose(c, err)
return v.ListenPacketOnStreamConn(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 *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 { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
@@ -298,11 +254,6 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return v.ListenPacketOnStreamConn(c, metadata) return v.ListenPacketOnStreamConn(c, metadata)
} }
// SupportWithDialer implements C.ProxyAdapter
func (v *Vless) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
@@ -316,16 +267,16 @@ func (v *Vless) SupportUOT() bool {
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
var addrType byte var addrType byte
var addr []byte var addr []byte
switch metadata.AddrType() { switch metadata.AddrType {
case socks5.AtypIPv4: case C.AtypIPv4:
addrType = vless.AtypIPv4 addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypIPv6: case C.AtypIPv6:
addrType = vless.AtypIPv6 addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypDomainName: case C.AtypDomainName:
addrType = vless.AtypDomainName addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host)) addr[0] = byte(len(metadata.Host))
@@ -454,12 +405,11 @@ func NewVless(option VlessOption) (*Vless, error) {
v := &Vless{ v := &Vless{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless, tp: C.Vless,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
option: &option, option: &option,
@@ -484,10 +434,10 @@ func NewVless(option VlessOption) (*Vless, error) {
ServiceName: v.option.GrpcOpts.GrpcServiceName, ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName, Host: v.option.ServerName,
} }
tlsConfig := tlsC.GetGlobalFingerprintTLCConfig(&tls.Config{ tlsConfig := &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) }
if v.option.ServerName == "" { if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)

View File

@@ -5,26 +5,19 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
tlsC "github.com/Dreamacro/clash/component/tls"
vmess "github.com/sagernet/sing-vmess"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
clashVMess "github.com/Dreamacro/clash/transport/vmess" "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 { type Vmess struct {
*Base *Base
client *vmess.Client client *vmess.Client
@@ -38,27 +31,25 @@ type Vmess struct {
type VmessOption struct { type VmessOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"` AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` ServerName string `proxy:"servername,omitempty"`
ServerName string `proxy:"servername,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"` // TODO: compatible with VMESS WS older version configurations
XUDP bool `proxy:"xudp,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"` WSPath string `proxy:"ws-path,omitempty"`
GlobalPadding bool `proxy:"global-padding,omitempty"`
AuthenticatedLength bool `proxy:"authenticated-length,omitempty"`
} }
type HTTPOptions struct { type HTTPOptions struct {
@@ -90,13 +81,13 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &clashVMess.WebsocketConfig{ wsOpts := &vmess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
Headers: http.Header{}, Headers: make(http.Header),
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
@@ -107,33 +98,26 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if v.option.TLS { if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
tlsConfig := &tls.Config{ wsOpts.TLSConfig = &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
if len(v.option.Fingerprint) == 0 {
wsOpts.TLSConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
} else {
var err error
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
return nil, err
}
}
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} else {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
} }
c, err = clashVMess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &clashVMess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
} }
@@ -142,24 +126,27 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = clashVMess.StreamTLSConn(c, tlsOpts) c, err = vmess.StreamTLSConn(c, tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
http.Header(v.option.HTTPOpts.Headers).Set("Host", convert.RandHost())
convert.SetUserAgent(v.option.HTTPOpts.Headers)
} }
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
httpOpts := &clashVMess.HTTPConfig{ httpOpts := &vmess.HTTPConfig{
Host: host, Host: host,
Method: v.option.HTTPOpts.Method, Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path, Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers, Headers: v.option.HTTPOpts.Headers,
} }
c = clashVMess.StreamHTTPConn(c, httpOpts) c = vmess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := clashVMess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
NextProtos: []string{"h2"}, NextProtos: []string{"h2"},
@@ -169,24 +156,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = clashVMess.StreamTLSConn(c, &tlsOpts) c, err = vmess.StreamTLSConn(c, &tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
h2Opts := &clashVMess.H2Config{ h2Opts := &vmess.H2Config{
Hosts: v.option.HTTP2Opts.Host, Hosts: v.option.HTTP2Opts.Host,
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = clashVMess.StreamH2Conn(c, h2Opts) c, err = vmess.StreamH2Conn(c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
default: default:
// handle TLS // handle TLS
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &clashVMess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
} }
@@ -195,22 +182,15 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = clashVMess.StreamTLSConn(c, tlsOpts) c, err = vmess.StreamTLSConn(c, tlsOpts)
} }
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if metadata.NetWork == C.UDP {
if v.option.XUDP { return v.client.StreamConn(c, parseVmessAddr(metadata))
return v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} else {
return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
}
} else {
return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
}
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@@ -221,30 +201,22 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(c, v), nil return NewConn(c, v), nil
} }
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err return NewConn(c, v), err
@@ -254,20 +226,13 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { 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 // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
if err != nil { if err != nil {
return nil, errors.New("can't resolve ip") return nil, errors.New("can't resolve ip")
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
if v.option.PacketAddr {
_metadata := *metadata // make a copy
metadata = &_metadata
metadata.Host = packetaddr.SeqPacketMagicAddress
metadata.DstPort = "443"
}
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && len(opts) == 0 {
@@ -275,46 +240,20 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
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()))
}
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
return v.ListenPacketOnStreamConn(c, metadata) tcpKeepAlive(c)
} defer safeConnClose(c, err)
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 { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
@@ -322,18 +261,8 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return v.ListenPacketOnStreamConn(c, metadata) return v.ListenPacketOnStreamConn(c, metadata)
} }
// SupportWithDialer implements C.ProxyAdapter
func (v *Vmess) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
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 newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
@@ -344,28 +273,18 @@ func (v *Vmess) SupportUOT() bool {
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher) security := strings.ToLower(option.Cipher)
var options []vmess.ClientOption client, err := vmess.NewClient(vmess.Config{
if option.GlobalPadding { UUID: option.UUID,
options = append(options, vmess.ClientWithGlobalPadding()) AlterID: uint16(option.AlterID),
} Security: security,
if option.AuthenticatedLength { HostName: option.Server,
options = append(options, vmess.ClientWithAuthenticatedLength()) Port: strconv.Itoa(option.Port),
} IsAead: option.AlterID == 0,
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch option.PacketEncoding {
case "packetaddr", "packet":
option.PacketAddr = true
case "xudp":
option.XUDP = true
}
if option.XUDP {
option.PacketAddr = false
}
switch option.Network { switch option.Network {
case "h2", "grpc": case "h2", "grpc":
if !option.TLS { if !option.TLS {
@@ -375,13 +294,12 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v := &Vmess{ v := &Vmess{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess, tp: C.Vmess,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
option: &option, option: &option,
@@ -421,36 +339,44 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunConfig = gunConfig v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
} }
return v, nil return v, nil
} }
type threadSafePacketConn struct { func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
net.PacketConn var addrType byte
access sync.Mutex var addr []byte
} switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
c.access.Lock() return &vmess.DstAddr{
defer c.access.Unlock() UDP: metadata.NetWork == C.UDP,
return c.PacketConn.WriteTo(b, addr) AddrType: addrType,
Addr: addr,
Port: uint(port),
}
} }
type vmessPacketConn struct { type vmessPacketConn struct {
net.Conn net.Conn
rAddr net.Addr rAddr net.Addr
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) { 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) return uc.Conn.Write(b)
} }

View File

@@ -1,258 +0,0 @@
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
}

View File

@@ -4,11 +4,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
"time"
) )
type Fallback struct { type Fallback struct {
@@ -31,7 +32,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
c.AppendToChains(f) c.AppendToChains(f)
f.onDialSuccess() f.onDialSuccess()
} else { } else {
f.onDialFailed(proxy.Type(), err) f.onDialFailed()
} }
return c, err return c, err
@@ -72,30 +73,24 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy(touch) proxy := f.findAliveProxy(true)
return proxy return proxy
} }
func (f *Fallback) findAliveProxy(touch bool) C.Proxy { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch) proxies := f.GetProxies(touch)
for _, proxy := range proxies { al := proxies[0]
if len(f.selected) == 0 { for i := len(proxies) - 1; i > -1; i-- {
if proxy.Alive() { proxy := proxies[i]
return proxy if proxy.Name() == f.selected && proxy.Alive() {
} return proxy
} else { }
if proxy.Name() == f.selected { if proxy.Alive() {
if proxy.Alive() { al = proxy
return proxy
} else {
f.selected = ""
}
}
} }
} }
return al
return proxies[0]
} }
func (f *Fallback) Set(name string) error { func (f *Fallback) Set(name string) error {
@@ -131,7 +126,6 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}, },
option.Filter, option.Filter,
option.ExcludeFilter,
providers, providers,
}), }),
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,

View File

@@ -3,164 +3,102 @@ package outboundgroup
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"go.uber.org/atomic" "go.uber.org/atomic"
"strings"
"sync"
"time"
) )
type GroupBase struct { type GroupBase struct {
*outbound.Base *outbound.Base
filterRegs []*regexp2.Regexp filter *regexp2.Regexp
excludeFilterReg *regexp2.Regexp providers []provider.ProxyProvider
providers []provider.ProxyProvider versions sync.Map // map[string]uint
failedTestMux sync.Mutex proxies sync.Map // map[string][]C.Proxy
failedTimes int failedTestMux sync.Mutex
failedTime time.Time failedTimes int
failedTesting *atomic.Bool failedTime time.Time
proxies [][]C.Proxy failedTesting *atomic.Bool
versions []atomic.Uint32
} }
type GroupBaseOption struct { type GroupBaseOption struct {
outbound.BaseOption outbound.BaseOption
filter string filter string
excludeFilter string providers []provider.ProxyProvider
providers []provider.ProxyProvider
} }
func NewGroupBase(opt GroupBaseOption) *GroupBase { func NewGroupBase(opt GroupBaseOption) *GroupBase {
var excludeFilterReg *regexp2.Regexp var filter *regexp2.Regexp = nil
if opt.excludeFilter != "" {
excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, 0)
}
var filterRegs []*regexp2.Regexp
if opt.filter != "" { if opt.filter != "" {
for _, filter := range strings.Split(opt.filter, "`") { filter = regexp2.MustCompile(opt.filter, 0)
filterReg := regexp2.MustCompile(filter, 0)
filterRegs = append(filterRegs, filterReg)
}
} }
return &GroupBase{
gb := &GroupBase{ Base: outbound.NewBase(opt.BaseOption),
Base: outbound.NewBase(opt.BaseOption), filter: filter,
filterRegs: filterRegs, providers: opt.providers,
excludeFilterReg: excludeFilterReg, failedTesting: atomic.NewBool(false),
providers: opt.providers,
failedTesting: atomic.NewBool(false),
}
gb.proxies = make([][]C.Proxy, len(opt.providers))
gb.versions = make([]atomic.Uint32, len(opt.providers))
return gb
}
func (gb *GroupBase) Touch() {
for _, pd := range gb.providers {
pd.Touch()
} }
} }
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
var proxies []C.Proxy if gb.filter == nil {
if len(gb.filterRegs) == 0 { var proxies []C.Proxy
for _, pd := range gb.providers { for _, pd := range gb.providers {
if touch { if touch {
pd.Touch() pd.Touch()
} }
proxies = append(proxies, pd.Proxies()...) proxies = append(proxies, pd.Proxies()...)
} }
} else { if len(proxies) == 0 {
for i, pd := range gb.providers { return append(proxies, tunnel.Proxies()["COMPATIBLE"])
if touch {
pd.Touch()
}
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)
}
}
}
}
gb.proxies[i] = newProxies
}
}
for _, p := range gb.proxies {
proxies = append(proxies, p...)
} }
return proxies
} }
for _, pd := range gb.providers {
if touch {
pd.Touch()
}
if pd.VehicleType() == types.Compatible {
gb.proxies.Store(pd.Name(), pd.Proxies())
gb.versions.Store(pd.Name(), pd.Version())
continue
}
if version, ok := gb.versions.Load(pd.Name()); !ok || version != pd.Version() {
var (
proxies []C.Proxy
newProxies []C.Proxy
)
proxies = pd.Proxies()
for _, p := range proxies {
if mat, _ := gb.filter.FindStringMatch(p.Name()); mat != nil {
newProxies = append(newProxies, p)
}
}
gb.proxies.Store(pd.Name(), newProxies)
gb.versions.Store(pd.Name(), pd.Version())
}
}
var proxies []C.Proxy
gb.proxies.Range(func(key, value any) bool {
proxies = append(proxies, value.([]C.Proxy)...)
return true
})
if len(proxies) == 0 { if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"]) return append(proxies, tunnel.Proxies()["COMPATIBLE"])
} }
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 {
var newProxies []C.Proxy
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)
}
}
}
}
for _, p := range proxies { // add not matched proxies at the end
name := p.Name()
if _, ok := proxiesSet[name]; !ok {
proxiesSet[name] = struct{}{}
newProxies = append(newProxies, p)
}
}
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 return proxies
} }
@@ -174,11 +112,11 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
wg.Add(1) wg.Add(1)
go func() { go func() {
delay, err := proxy.URLTest(ctx, url) delay, err := proxy.URLTest(ctx, url)
lock.Lock()
if err == nil { if err == nil {
lock.Lock()
mp[proxy.Name()] = delay mp[proxy.Name()] = delay
lock.Unlock()
} }
lock.Unlock()
wg.Done() wg.Done()
}() }()
@@ -192,13 +130,8 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
} }
} }
func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) { func (gb *GroupBase) onDialFailed() {
if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass { if gb.failedTesting.Load() {
return
}
if strings.Contains(err.Error(), "connection refused") {
go gb.healthCheck()
return return
} }
@@ -212,40 +145,31 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) {
gb.failedTime = time.Now() gb.failedTime = time.Now()
} else { } else {
if time.Since(gb.failedTime) > gb.failedTimeoutInterval() { if time.Since(gb.failedTime) > gb.failedTimeoutInterval() {
gb.failedTimes = 0
return return
} }
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes() { if gb.failedTimes >= gb.maxFailedTimes() {
gb.failedTesting.Store(true)
log.Warnln("because %s failed multiple times, active health check", gb.Name()) log.Warnln("because %s failed multiple times, active health check", gb.Name())
gb.healthCheck() wg := sync.WaitGroup{}
for _, proxyProvider := range gb.providers {
wg.Add(1)
proxyProvider := proxyProvider
go func() {
defer wg.Done()
proxyProvider.HealthCheck()
}()
}
wg.Wait()
gb.failedTesting.Store(false)
gb.failedTimes = 0
} }
} }
}() }()
} }
func (gb *GroupBase) healthCheck() {
if gb.failedTesting.Load() {
return
}
gb.failedTesting.Store(true)
wg := sync.WaitGroup{}
for _, proxyProvider := range gb.providers {
wg.Add(1)
proxyProvider := proxyProvider
go func() {
defer wg.Done()
proxyProvider.HealthCheck()
}()
}
wg.Wait()
gb.failedTesting.Store(false)
gb.failedTimes = 0
}
func (gb *GroupBase) failedIntervalTime() int64 { func (gb *GroupBase) failedIntervalTime() int64 {
return 5 * time.Second.Milliseconds() return 5 * time.Second.Milliseconds()
} }

View File

@@ -5,16 +5,15 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/common/cache"
"net" "net"
"time" "time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/murmur3" "github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
@@ -29,8 +28,10 @@ type LoadBalance struct {
var errStrategy = errors.New("unsupported strategy") var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]any) string { func parseStrategy(config map[string]any) string {
if strategy, ok := config["strategy"].(string); ok { if elm, ok := config["strategy"]; ok {
return strategy if strategy, ok := elm.(string); ok {
return strategy
}
} }
return "consistent-hashing" return "consistent-hashing"
} }
@@ -82,17 +83,17 @@ func jumpHash(key uint64, buckets int32) int32 {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
proxy := lb.Unwrap(metadata, true)
defer func() { defer func() {
if err == nil { if err == nil {
c.AppendToChains(lb) c.AppendToChains(lb)
lb.onDialSuccess() lb.onDialSuccess()
} else { } else {
lb.onDialFailed(proxy.Type(), err) lb.onDialFailed()
} }
}() }()
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...) c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return return
} }
@@ -105,7 +106,7 @@ func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Meta
} }
}() }()
proxy := lb.Unwrap(metadata, true) proxy := lb.Unwrap(metadata)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...) return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
} }
@@ -143,13 +144,6 @@ func strategyConsistentHashing() strategyFn {
} }
} }
// when availability is poor, traverse the entire list to get the available nodes
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
return proxies[0] return proxies[0]
} }
} }
@@ -157,7 +151,7 @@ func strategyConsistentHashing() strategyFn {
func strategyStickySessions() strategyFn { func strategyStickySessions() strategyFn {
ttl := time.Minute * 10 ttl := time.Minute * 10
maxRetry := 5 maxRetry := 5
lruCache := cache.New[uint64, int]( lruCache := cache.NewLRUCache[uint64, int](
cache.WithAge[uint64, int](int64(ttl.Seconds())), cache.WithAge[uint64, int](int64(ttl.Seconds())),
cache.WithSize[uint64, int](1000)) cache.WithSize[uint64, int](1000))
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
@@ -190,8 +184,8 @@ func strategyStickySessions() strategyFn {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.GetProxies(touch) proxies := lb.GetProxies(true)
return lb.strategyFn(proxies, metadata) return lb.strategyFn(proxies, metadata)
} }
@@ -228,7 +222,6 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}, },
option.Filter, option.Filter,
option.ExcludeFilter,
providers, providers,
}), }),
strategyFn: strategyFn, strategyFn: strategyFn,

View File

@@ -21,16 +21,15 @@ var (
type GroupCommonOption struct { type GroupCommonOption struct {
outbound.BasicOption outbound.BasicOption
Name string `group:"name"` Name string `group:"name"`
Type string `group:"type"` Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"` Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"` Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"` URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"` Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"` Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,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) { func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {

View File

@@ -3,12 +3,9 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"net" "fmt"
"net/netip"
"strings"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
@@ -18,36 +15,6 @@ type Relay struct {
*GroupBase *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 // DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxies, chainProxies := r.proxies(metadata, true) proxies, chainProxies := r.proxies(metadata, true)
@@ -58,19 +25,37 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
case 1: case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
} }
var d C.Dialer
d = dialer.NewDialer(r.Base.DialOptions(opts...)...) first := proxies[0]
for _, proxy := range proxies[:len(proxies)-1] {
d = proxyDialer{
proxy: proxy,
dialer: d,
}
}
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
conn, err := last.DialContextWithDialer(ctx, d, metadata)
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err 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)
}
first = proxy
}
c, err = last.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
conn := outbound.NewConn(c, last)
for i := len(chainProxies) - 2; i >= 0; i-- { for i := len(chainProxies) - 2; i >= 0; i-- {
conn.AppendToChains(chainProxies[i]) conn.AppendToChains(chainProxies[i])
@@ -92,18 +77,39 @@ func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...) return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
} }
var d C.Dialer first := proxies[0]
d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
for _, proxy := range proxies[:len(proxies)-1] {
d = proxyDialer{
proxy: proxy,
dialer: d,
}
}
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
pc, err := last.ListenPacketWithDialer(ctx, d, metadata)
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err 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)
}
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)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
} }
for i := len(chainProxies) - 2; i >= 0; i-- { for i := len(chainProxies) - 2; i >= 0; i-- {
@@ -121,19 +127,8 @@ func (r *Relay) SupportUDP() bool {
if len(proxies) == 0 { // C.Direct if len(proxies) == 0 { // C.Direct
return true return true
} }
for i := len(proxies) - 1; i >= 0; i-- { last := proxies[len(proxies)-1]
proxy := proxies[i] return last.SupportUDP() && last.SupportUOT()
if !proxy.SupportUDP() {
return false
}
if proxy.SupportUOT() {
return true
}
if !proxy.SupportWithDialer() {
return false
}
}
return true
} }
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
@@ -158,11 +153,11 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy)
for n, proxy := range rawProxies { for n, proxy := range rawProxies {
proxies = append(proxies, proxy) proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy) chainProxies = append(chainProxies, proxy)
subproxy := proxy.Unwrap(metadata, touch) subproxy := proxy.Unwrap(metadata)
for subproxy != nil { for subproxy != nil {
chainProxies = append(chainProxies, subproxy) chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata, touch) subproxy = subproxy.Unwrap(metadata)
} }
} }
@@ -190,7 +185,6 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}, },
"", "",
"",
providers, providers,
}), }),
} }

View File

@@ -74,8 +74,8 @@ func (s *Selector) Set(name string) error {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (s *Selector) Unwrap(*C.Metadata) C.Proxy {
return s.selectedProxy(touch) return s.selectedProxy(true)
} }
func (s *Selector) selectedProxy(touch bool) C.Proxy { func (s *Selector) selectedProxy(touch bool) C.Proxy {
@@ -99,7 +99,6 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider)
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}, },
option.Filter, option.Filter,
option.ExcludeFilter,
providers, providers,
}), }),
selected: "COMPATIBLE", selected: "COMPATIBLE",

View File

@@ -34,13 +34,12 @@ func (u *URLTest) Now() string {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
proxy := u.fast(true) c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
u.onDialSuccess() u.onDialSuccess()
} else { } else {
u.onDialFailed(proxy.Type(), err) u.onDialFailed()
} }
return c, err return c, err
} }
@@ -56,12 +55,12 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
return u.fast(touch) return u.fast(true)
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) { elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.GetProxies(touch) proxies := u.GetProxies(touch)
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
@@ -90,9 +89,6 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil return u.fastNode, nil
}) })
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
u.Touch()
}
return elm return elm
} }
@@ -143,7 +139,6 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
}, },
option.Filter, option.Filter,
option.ExcludeFilter,
providers, providers,
}), }),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),

View File

@@ -16,19 +16,32 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
return return
} }
if ip, err := netip.ParseAddr(host); err != nil { ip, err := netip.ParseAddr(host)
if err != nil {
addr = &C.Metadata{ addr = &C.Metadata{
Host: host, AddrType: C.AtypDomainName,
DstPort: port, Host: host,
DstIP: netip.Addr{},
DstPort: port,
} }
} else { err = nil
return
} else if ip.Is4() {
addr = &C.Metadata{ addr = &C.Metadata{
Host: "", AddrType: C.AtypIPv4,
DstIP: ip.Unmap(), Host: "",
DstPort: port, DstIP: ip,
DstPort: port,
} }
return
} }
addr = &C.Metadata{
AddrType: C.AtypIPv6,
Host: "",
DstIP: ip,
DstPort: port,
}
return return
} }

View File

@@ -2,13 +2,14 @@ package adapter
import ( import (
"fmt" "fmt"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
func ParseProxy(mapping map[string]any) (C.Proxy, error) { func ParseProxy(mapping map[string]any) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
if !existType { if !existType {
return nil, fmt.Errorf("missing type") return nil, fmt.Errorf("missing type")
@@ -39,14 +40,14 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
proxy, err = outbound.NewSocks5(*socksOption) proxy = outbound.NewSocks5(*socksOption)
case "http": case "http":
httpOption := &outbound.HttpOption{} httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption) err = decoder.Decode(mapping, httpOption)
if err != nil { if err != nil {
break break
} }
proxy, err = outbound.NewHttp(*httpOption) proxy = outbound.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &outbound.VmessOption{ vmessOption := &outbound.VmessOption{
HTTPOpts: outbound.HTTPOptions{ HTTPOpts: outbound.HTTPOptions{
@@ -87,20 +88,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break break
} }
proxy, err = outbound.NewHysteria(*hyOption) 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: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }

View File

@@ -1,4 +1,4 @@
package resource package provider
import ( import (
"bytes" "bytes"
@@ -16,49 +16,45 @@ var (
dirMode os.FileMode = 0o755 dirMode os.FileMode = 0o755
) )
type Parser[V any] func([]byte) (V, error) type parser[V any] func([]byte) (V, error)
type Fetcher[V any] struct { type fetcher[V any] struct {
resourceType string name string
name string vehicle types.Vehicle
vehicle types.Vehicle updatedAt *time.Time
UpdatedAt *time.Time ticker *time.Ticker
ticker *time.Ticker done chan struct{}
done chan struct{} hash [16]byte
hash [16]byte parser parser[V]
parser Parser[V] interval time.Duration
interval time.Duration onUpdate func(V)
OnUpdate func(V)
} }
func (f *Fetcher[V]) Name() string { func (f *fetcher[V]) Name() string {
return f.name return f.name
} }
func (f *Fetcher[V]) Vehicle() types.Vehicle { func (f *fetcher[V]) VehicleType() types.VehicleType {
return f.vehicle
}
func (f *Fetcher[V]) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
func (f *Fetcher[V]) Initial() (V, error) { func (f *fetcher[V]) Initial() (V, error) {
var ( var (
buf []byte buf []byte
err error err error
isLocal bool isLocal bool
forceUpdate bool
) )
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = os.ReadFile(f.vehicle.Path()) buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime() modTime := stat.ModTime()
f.UpdatedAt = &modTime f.updatedAt = &modTime
isLocal = true isLocal = true
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) { if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
log.Infoln("[Provider] %s not updated for a long time, force refresh", f.Name()) defer func() {
forceUpdate = true log.Infoln("[Provider] %s's proxies not updated for a long time, force refresh", f.Name())
go f.Update()
}()
} }
} else { } else {
buf, err = f.vehicle.Read() buf, err = f.vehicle.Read()
@@ -68,21 +64,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
return getZero[V](), err return getZero[V](), err
} }
var contents V proxies, err := f.parser(buf)
if forceUpdate {
var forceBuf []byte
if forceBuf, err = f.vehicle.Read(); err == nil {
if contents, err = f.parser(forceBuf); err == nil {
isLocal = false
buf = forceBuf
}
}
}
if err != nil || !forceUpdate {
contents, err = f.parser(buf)
}
if err != nil { if err != nil {
if !isLocal { if !isLocal {
return getZero[V](), err return getZero[V](), err
@@ -94,7 +76,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
return getZero[V](), err return getZero[V](), err
} }
contents, err = f.parser(buf) proxies, err = f.parser(buf)
if err != nil { if err != nil {
return getZero[V](), err return getZero[V](), err
} }
@@ -110,15 +92,15 @@ func (f *Fetcher[V]) Initial() (V, error) {
f.hash = md5.Sum(buf) f.hash = md5.Sum(buf)
// pull contents automatically // pull proxies automatically
if f.ticker != nil { if f.ticker != nil {
go f.pullLoop() go f.pullLoop()
} }
return contents, nil return proxies, nil
} }
func (f *Fetcher[V]) Update() (V, bool, error) { func (f *fetcher[V]) Update() (V, bool, error) {
buf, err := f.vehicle.Read() buf, err := f.vehicle.Read()
if err != nil { if err != nil {
return getZero[V](), false, err return getZero[V](), false, err
@@ -127,12 +109,12 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
now := time.Now() now := time.Now()
hash := md5.Sum(buf) hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) { if bytes.Equal(f.hash[:], hash[:]) {
f.UpdatedAt = &now f.updatedAt = &now
_ = os.Chtimes(f.vehicle.Path(), now, now) os.Chtimes(f.vehicle.Path(), now, now)
return getZero[V](), true, nil return getZero[V](), true, nil
} }
contents, err := f.parser(buf) proxies, err := f.parser(buf)
if err != nil { if err != nil {
return getZero[V](), false, err return getZero[V](), false, err
} }
@@ -143,20 +125,20 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
} }
} }
f.UpdatedAt = &now f.updatedAt = &now
f.hash = hash f.hash = hash
return contents, false, nil return proxies, false, nil
} }
func (f *Fetcher[V]) Destroy() error { func (f *fetcher[V]) Destroy() error {
if f.ticker != nil { if f.ticker != nil {
f.done <- struct{}{} f.done <- struct{}{}
} }
return nil return nil
} }
func (f *Fetcher[V]) pullLoop() { func (f *fetcher[V]) pullLoop() {
for { for {
select { select {
case <-f.ticker.C: case <-f.ticker.C:
@@ -167,13 +149,13 @@ func (f *Fetcher[V]) pullLoop() {
} }
if same { if same {
log.Debugln("[Provider] %s's content doesn't change", f.Name()) log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
continue continue
} }
log.Infoln("[Provider] %s's content update", f.Name()) log.Infoln("[Provider] %s's proxies update", f.Name())
if f.OnUpdate != nil { if f.onUpdate != nil {
f.OnUpdate(elm) f.onUpdate(elm)
} }
case <-f.done: case <-f.done:
f.ticker.Stop() f.ticker.Stop()
@@ -194,20 +176,19 @@ func safeWrite(path string, buf []byte) error {
return os.WriteFile(path, buf, fileMode) return os.WriteFile(path, buf, fileMode)
} }
func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] { func newFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser parser[V], onUpdate func(V)) *fetcher[V] {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)
} }
return &Fetcher[V]{ return &fetcher[V]{
name: name, name: name,
ticker: ticker, ticker: ticker,
vehicle: vehicle, vehicle: vehicle,
parser: parser, parser: parser,
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
OnUpdate: onUpdate, onUpdate: onUpdate,
interval: interval,
} }
} }

View File

@@ -5,11 +5,7 @@ import (
"time" "time"
"github.com/Dreamacro/clash/common/batch" "github.com/Dreamacro/clash/common/batch"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/gofrs/uuid"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@@ -29,7 +25,6 @@ type HealthCheck struct {
lazy bool lazy bool
lastTouch *atomic.Int64 lastTouch *atomic.Int64
done chan struct{} done chan struct{}
singleDo *singledo.Single[struct{}]
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
@@ -37,13 +32,16 @@ func (hc *HealthCheck) process() {
go func() { go func() {
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
hc.lazyCheck() hc.check()
}() }()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
hc.lazyCheck() now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
}
case <-hc.done: case <-hc.done:
ticker.Stop() ticker.Stop()
return return
@@ -51,17 +49,6 @@ func (hc *HealthCheck) process() {
} }
} }
func (hc *HealthCheck) lazyCheck() bool {
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
return true
} else {
log.Debugln("Skip once health check because we are lazy")
return false
}
}
func (hc *HealthCheck) setProxy(proxies []C.Proxy) { func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies hc.proxies = proxies
} }
@@ -75,29 +62,17 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
_, _, _ = hc.singleDo.Do(func() (struct{}, error) { b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
id := "" for _, proxy := range hc.proxies {
if uid, err := uuid.NewV4(); err == nil { p := proxy
id = uid.String() b.Go(p.Name(), func() (bool, error) {
} ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
log.Debugln("Start New Health Checking {%s}", id) defer cancel()
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) _, _ = p.URLTest(ctx, hc.url)
for _, proxy := range hc.proxies { return false, nil
p := proxy })
b.Go(p.Name(), func() (bool, error) { }
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) b.Wait()
defer cancel()
log.Debugln("Health Checking %s {%s}", p.Name(), id)
_, _ = p.URLTest(ctx, hc.url)
log.Debugln("Health Checked %s : %t %d ms {%s}", p.Name(), p.Alive(), p.LastDelay(), id)
return false, nil
})
}
b.Wait()
log.Debugln("Finish A Health Checking {%s}", id)
return struct{}{}, nil
})
} }
func (hc *HealthCheck) close() { func (hc *HealthCheck) close() {
@@ -112,6 +87,5 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
lazy: lazy, lazy: lazy,
lastTouch: atomic.NewInt64(0), lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
} }
} }

View File

@@ -3,7 +3,6 @@ package provider
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/component/resource"
"time" "time"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
@@ -21,13 +20,12 @@ type healthCheckSchema struct {
} }
type proxyProviderSchema struct { type proxyProviderSchema struct {
Type string `provider:"type"` Type string `provider:"type"`
Path string `provider:"path"` Path string `provider:"path"`
URL string `provider:"url,omitempty"` URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"` Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"` Filter string `provider:"filter,omitempty"`
ExcludeFilter string `provider:"exclude-filter,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
} }
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
@@ -53,15 +51,14 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
var vehicle types.Vehicle var vehicle types.Vehicle
switch schema.Type { switch schema.Type {
case "file": case "file":
vehicle = resource.NewFileVehicle(path) vehicle = NewFileVehicle(path)
case "http": case "http":
vehicle = resource.NewHTTPVehicle(schema.URL, path) vehicle = NewHTTPVehicle(schema.URL, path)
default: default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
} }
interval := time.Duration(uint(schema.Interval)) * time.Second interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter filter := schema.Filter
excludeFilter := schema.ExcludeFilter return NewProxySetProvider(name, interval, filter, vehicle, hc)
return NewProxySetProvider(name, interval, filter, excludeFilter, vehicle, hc)
} }

View File

@@ -1,24 +1,19 @@
package provider package provider
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/dlclark/regexp2" "math"
"gopkg.in/yaml.v3"
"net/http"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert" "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" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log" "github.com/dlclark/regexp2"
"gopkg.in/yaml.v3"
) )
const ( const (
@@ -35,30 +30,28 @@ type ProxySetProvider struct {
} }
type proxySetProvider struct { type proxySetProvider struct {
*resource.Fetcher[[]C.Proxy] *fetcher[[]C.Proxy]
proxies []C.Proxy proxies []C.Proxy
healthCheck *HealthCheck healthCheck *HealthCheck
version uint32 version uint
subscriptionInfo *SubscriptionInfo
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"name": pp.Name(), "name": pp.Name(),
"type": pp.Type().String(), "type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(), "proxies": pp.Proxies(),
"updatedAt": pp.UpdatedAt, "updatedAt": pp.updatedAt,
"subscriptionInfo": pp.subscriptionInfo,
}) })
} }
func (pp *proxySetProvider) Version() uint32 { func (pp *proxySetProvider) Version() uint {
return pp.version return pp.version
} }
func (pp *proxySetProvider) Name() string { func (pp *proxySetProvider) Name() string {
return pp.Fetcher.Name() return pp.name
} }
func (pp *proxySetProvider) HealthCheck() { func (pp *proxySetProvider) HealthCheck() {
@@ -66,19 +59,19 @@ func (pp *proxySetProvider) HealthCheck() {
} }
func (pp *proxySetProvider) Update() error { func (pp *proxySetProvider) Update() error {
elm, same, err := pp.Fetcher.Update() elm, same, err := pp.fetcher.Update()
if err == nil && !same { if err == nil && !same {
pp.OnUpdate(elm) pp.onUpdate(elm)
} }
return err return err
} }
func (pp *proxySetProvider) Initial() error { func (pp *proxySetProvider) Initial() error {
elm, err := pp.Fetcher.Initial() elm, err := pp.fetcher.Initial()
if err != nil { if err != nil {
return err return err
} }
pp.OnUpdate(elm) pp.onUpdate(elm)
return nil return nil
} }
@@ -98,61 +91,19 @@ func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies pp.proxies = proxies
pp.healthCheck.setProxy(proxies) pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() { if pp.healthCheck.auto() {
defer func() { go pp.healthCheck.lazyCheck() }() defer func() { go pp.healthCheck.check() }()
} }
} }
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) { func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close() pd.healthCheck.close()
_ = pd.Fetcher.Destroy() _ = pd.fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
excludeFilterReg, err := regexp2.Compile(excludeFilter, 0) filterReg, err := regexp2.Compile(filter, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) return nil, fmt.Errorf("invalid filter regex: %w", err)
}
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(filter, "`") {
filterReg, err := regexp2.Compile(filter, 0)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
filterRegs = append(filterRegs, filterReg)
} }
if hc.auto() { if hc.auto() {
@@ -164,10 +115,9 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc
healthCheck: hc, healthCheck: hc,
} }
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, filterRegs, excludeFilterReg), proxiesOnUpdate(pd)) fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg), proxiesOnUpdate(pd))
pd.Fetcher = fetcher pd.fetcher = fetcher
pd.getSubscriptionInfo()
wrapper := &ProxySetProvider{pd} wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider) runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil return wrapper, nil
@@ -182,7 +132,7 @@ type compatibleProvider struct {
name string name string
healthCheck *HealthCheck healthCheck *HealthCheck
proxies []C.Proxy proxies []C.Proxy
version uint32 version uint
} }
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
@@ -194,7 +144,7 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
}) })
} }
func (cp *compatibleProvider) Version() uint32 { func (cp *compatibleProvider) Version() uint {
return cp.version return cp.version
} }
@@ -257,12 +207,15 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) { func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) { return func(elm []C.Proxy) {
pd.setProxies(elm) pd.setProxies(elm)
pd.version += 1 if pd.version == math.MaxUint {
pd.getSubscriptionInfo() pd.version = 0
} else {
pd.version++
}
} }
} }
func proxiesParseAndFilter(filter string, excludeFilter string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp) resource.Parser[[]C.Proxy] { func proxiesParseAndFilter(filter string, filterReg *regexp2.Regexp) parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) { return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
@@ -279,37 +232,17 @@ func proxiesParseAndFilter(filter string, excludeFilter string, filterRegs []*re
} }
proxies := []C.Proxy{} proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{} for idx, mapping := range schema.Proxies {
for _, filterReg := range filterRegs { name, ok := mapping["name"]
for idx, mapping := range schema.Proxies { mat, _ := filterReg.FindStringMatch(name.(string))
mName, ok := mapping["name"] if ok && len(filter) > 0 && mat == nil {
if !ok { continue
continue
}
name, ok := mName.(string)
if !ok {
continue
}
if len(excludeFilter) > 0 {
if mat, _ := excludeFilterReg.FindStringMatch(name); mat != nil {
continue
}
}
if len(filter) > 0 {
if mat, _ := filterReg.FindStringMatch(name); mat == nil {
continue
}
}
if _, ok := proxiesSet[name]; ok {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxiesSet[name] = struct{}{}
proxies = append(proxies, proxy)
} }
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
} }
if len(proxies) == 0 { if len(proxies) == 0 {

View File

@@ -1,57 +0,0 @@
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
}

View File

@@ -1,13 +1,14 @@
package resource package provider
import ( import (
"context" "context"
clashHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
"io" "io"
"net/http" "net/http"
"os" "os"
"time" "time"
netHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
) )
type FileVehicle struct { type FileVehicle struct {
@@ -35,10 +36,6 @@ type HTTPVehicle struct {
path string path string
} }
func (h *HTTPVehicle) Url() string {
return h.url
}
func (h *HTTPVehicle) Type() types.VehicleType { func (h *HTTPVehicle) Type() types.VehicleType {
return types.HTTP return types.HTTP
} }
@@ -50,7 +47,7 @@ func (h *HTTPVehicle) Path() string {
func (h *HTTPVehicle) Read() ([]byte, error) { func (h *HTTPVehicle) Read() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel() defer cancel()
resp, err := clashHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil) resp, err := netHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

106
common/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,106 @@
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
}

72
common/cache/cache_test.go vendored Normal file
View File

@@ -0,0 +1,72 @@
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()
}

View File

@@ -65,8 +65,8 @@ type LruCache[K comparable, V any] struct {
onEvict EvictCallback[K, V] onEvict EvictCallback[K, V]
} }
// New creates an LruCache // NewLRUCache creates an LruCache
func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{ lc := &LruCache[K, V]{
lru: list.New[*entry[K, V]](), lru: list.New[*entry[K, V]](),
cache: make(map[K]*list.Element[*entry[K, V]]), cache: make(map[K]*list.Element[*entry[K, V]]),

View File

@@ -19,7 +19,7 @@ var entries = []struct {
} }
func TestLRUCache(t *testing.T) { func TestLRUCache(t *testing.T) {
c := New[string, string]() c := NewLRUCache[string, string]()
for _, e := range entries { for _, e := range entries {
c.Set(e.key, e.value) c.Set(e.key, e.value)
@@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
} }
func TestLRUMaxAge(t *testing.T) { func TestLRUMaxAge(t *testing.T) {
c := New[string, string](WithAge[string, string](86400)) c := NewLRUCache[string, string](WithAge[string, string](86400))
now := time.Now().Unix() now := time.Now().Unix()
expected := now + 86400 expected := now + 86400
@@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
} }
func TestLRUpdateOnGet(t *testing.T) { func TestLRUpdateOnGet(t *testing.T) {
c := New[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]()) c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
now := time.Now().Unix() now := time.Now().Unix()
expires := now + 86400/2 expires := now + 86400/2
@@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
} }
func TestMaxSize(t *testing.T) { func TestMaxSize(t *testing.T) {
c := New[string, string](WithSize[string, string](2)) c := NewLRUCache[string, string](WithSize[string, string](2))
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
_, ok := c.Get("foo") _, ok := c.Get("foo")
@@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
} }
func TestExist(t *testing.T) { func TestExist(t *testing.T) {
c := New[int, int](WithSize[int, int](1)) c := NewLRUCache[int, int](WithSize[int, int](1))
c.Set(1, 2) c.Set(1, 2)
assert.True(t, c.Exist(1)) assert.True(t, c.Exist(1))
c.Set(2, 3) c.Set(2, 3)
@@ -130,7 +130,7 @@ func TestEvict(t *testing.T) {
temp = key + value temp = key + value
} }
c := New[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
c.Set(1, 2) c.Set(1, 2)
c.Set(2, 3) c.Set(2, 3)
@@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
} }
func TestSetWithExpire(t *testing.T) { func TestSetWithExpire(t *testing.T) {
c := New[int, *struct{}](WithAge[int, *struct{}](1)) c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
now := time.Now().Unix() now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0) tenSecBefore := time.Unix(now-10, 0)
@@ -153,7 +153,7 @@ func TestSetWithExpire(t *testing.T) {
} }
func TestStale(t *testing.T) { func TestStale(t *testing.T) {
c := New[int, int](WithAge[int, int](1), WithStale[int, int](true)) c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true))
now := time.Now().Unix() now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0) tenSecBefore := time.Unix(now-10, 0)
@@ -166,11 +166,11 @@ func TestStale(t *testing.T) {
} }
func TestCloneTo(t *testing.T) { func TestCloneTo(t *testing.T) {
o := New[string, int](WithSize[string, int](10)) o := NewLRUCache[string, int](WithSize[string, int](10))
o.Set("1", 1) o.Set("1", 1)
o.Set("2", 2) o.Set("2", 2)
n := New[string, int](WithSize[string, int](2)) n := NewLRUCache[string, int](WithSize[string, int](2))
n.Set("3", 3) n.Set("3", 3)
n.Set("4", 4) n.Set("4", 4)

View File

@@ -14,7 +14,6 @@ func ExecCmd(cmdStr string) (string, error) {
cmd = exec.Command(args[0]) cmd = exec.Command(args[0])
} else { } else {
cmd = exec.Command(args[0], args[1:]...) cmd = exec.Command(args[0], args[1:]...)
} }
prepareBackgroundCommand(cmd) prepareBackgroundCommand(cmd)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()

View File

@@ -7,5 +7,4 @@ import (
) )
func prepareBackgroundCommand(cmd *exec.Cmd) { func prepareBackgroundCommand(cmd *exec.Cmd) {
} }

View File

@@ -1,45 +0,0 @@
package convert
import (
"encoding/base64"
"strings"
)
var (
encRaw = base64.RawStdEncoding
enc = base64.StdEncoding
)
// DecodeBase64 try to decode content from the given bytes,
// which can be in base64.RawStdEncoding, base64.StdEncoding or just plaintext.
func DecodeBase64(buf []byte) []byte {
result, err := tryDecodeBase64(buf)
if err != nil {
return buf
}
return result
}
func tryDecodeBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, encRaw.DecodedLen(len(buf)))
n, err := encRaw.Decode(dBuf, buf)
if err != nil {
n, err = enc.Decode(dBuf, buf)
if err != nil {
return nil, err
}
}
return dBuf[:n], nil
}
func urlSafe(data string) string {
return strings.NewReplacer("+", "-", "/", "_").Replace(data)
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.RawURLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}

View File

@@ -2,6 +2,7 @@ package convert
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" "net/url"
@@ -9,9 +10,34 @@ import (
"strings" "strings"
) )
var enc = base64.StdEncoding
func DecodeBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, enc.DecodedLen(len(buf)))
n, err := enc.Decode(dBuf, buf)
if err != nil {
return nil, err
}
return dBuf[:n], nil
}
// DecodeBase64StringToString decode base64 string to string
func DecodeBase64StringToString(s string) (string, error) {
dBuf, err := enc.DecodeString(s)
if err != nil {
return "", err
}
return string(dBuf), nil
}
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config // ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
data := DecodeBase64(buf) data, err := DecodeBase64(buf)
if err != nil {
data = buf
}
arr := strings.Split(string(data), "\n") arr := strings.Split(string(data), "\n")
@@ -47,19 +73,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["port"] = urlHysteria.Port() hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer") hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs") hysteria["obfs"] = query.Get("obfs")
hysteria["alpn"] = []string{query.Get("alpn")} hysteria["alpn"] = query.Get("alpn")
hysteria["auth_str"] = query.Get("auth") hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol") hysteria["protocol"] = query.Get("protocol")
up := query.Get("up") hysteria["down_mbps"], _ = strconv.Atoi(query.Get("downmbps"))
down := query.Get("down") hysteria["up_mbps"], _ = strconv.Atoi(query.Get("upmbps"))
if up == "" {
up = query.Get("upmbps")
}
if down == "" {
down = query.Get("downmbps")
}
hysteria["down"] = down
hysteria["up"] = up
hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure")) hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria) proxies = append(proxies, hysteria)
@@ -98,6 +116,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
headers := make(map[string]any) headers := make(map[string]any)
wsOpts := make(map[string]any) wsOpts := make(map[string]any)
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent() headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path") wsOpts["path"] = query.Get("path")
@@ -114,45 +133,94 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
proxies = append(proxies, trojan) proxies = append(proxies, trojan)
case "vless": case "vless":
urlVLess, err := url.Parse(line) urlVless, err := url.Parse(line)
if err != nil { if err != nil {
continue continue
} }
query := urlVLess.Query()
query := urlVless.Query()
name := uniqueName(names, urlVless.Fragment)
vless := make(map[string]any, 20) vless := make(map[string]any, 20)
handleVShareLink(names, urlVLess, scheme, vless)
if flow := query.Get("flow"); flow != "" { vless["name"] = name
vless["flow"] = strings.ToLower(flow) vless["type"] = scheme
vless["server"] = urlVless.Hostname()
vless["port"] = urlVless.Port()
vless["uuid"] = urlVless.User.Username()
vless["udp"] = true
vless["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
vless["servername"] = sni
} }
flow := strings.ToLower(query.Get("flow"))
if flow != "" {
vless["flow"] = flow
}
network := strings.ToLower(query.Get("type"))
if network != "" {
fakeType := strings.ToLower(query.Get("headerType"))
if network == "tcp" && fakeType == "http" {
network = "http"
}
if network == "http" {
network = "h2"
}
vless["network"] = network
}
switch network {
case "http":
headers := make(map[string]any)
httpOpts := make(map[string]any)
if query.Get("method") != "" {
httpOpts["method"] = query.Get("method")
}
if query.Get("path") != "" {
httpOpts["path"] = query.Get("path")
}
headers["User-Agent"] = RandUserAgent()
httpOpts["headers"] = headers
vless["http-opts"] = httpOpts
case "h2":
headers := make(map[string]any)
h2Opts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
h2Opts["path"] = query.Get("path")
h2Opts["headers"] = headers
vless["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
vless["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
vless["grpc-opts"] = grpcOpts
}
proxies = append(proxies, vless) proxies = append(proxies, vless)
case "vmess": case "vmess":
// V2RayN-styled share link dcBuf, err := enc.DecodeString(body)
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
dcBuf, err := tryDecodeBase64([]byte(body))
if err != nil { if err != nil {
// Xray VMessAEAD share link
urlVMess, err := url.Parse(line)
if err != nil {
continue
}
query := urlVMess.Query()
vmess := make(map[string]any, 20)
handleVShareLink(names, urlVMess, scheme, vmess)
vmess["alterId"] = 0
vmess["cipher"] = "auto"
if encryption := query.Get("encryption"); encryption != "" {
vmess["cipher"] = encryption
}
if packetEncoding := query.Get("packetEncoding"); packetEncoding != "" {
switch packetEncoding {
case "packet":
vmess["packet-addr"] = true
case "xudp":
vmess["xudp"] = true
}
}
proxies = append(proxies, vmess)
continue continue
} }
@@ -171,34 +239,26 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
vmess["server"] = values["add"] vmess["server"] = values["add"]
vmess["port"] = values["port"] vmess["port"] = values["port"]
vmess["uuid"] = values["id"] vmess["uuid"] = values["id"]
if alterId, ok := values["aid"]; ok { vmess["alterId"] = values["aid"]
vmess["alterId"] = alterId vmess["cipher"] = "auto"
} else {
vmess["alterId"] = 0
}
vmess["udp"] = true vmess["udp"] = true
vmess["tls"] = false
vmess["skip-cert-verify"] = false vmess["skip-cert-verify"] = false
vmess["cipher"] = "auto" sni := values["sni"]
if cipher, ok := values["scy"]; ok && cipher != "" { if sni != "" {
vmess["cipher"] = cipher vmess["sni"] = sni
}
if sni, ok := values["sni"]; ok && sni != "" {
vmess["servername"] = sni
} }
host := values["host"]
network := strings.ToLower(values["net"].(string)) network := strings.ToLower(values["net"].(string))
if values["type"] == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
vmess["network"] = network vmess["network"] = network
tls := strings.ToLower(values["tls"].(string)) tls := strings.ToLower(values["tls"].(string))
if strings.HasSuffix(tls, "tls") { if tls != "" && tls != "0" && tls != "null" {
if host != nil {
vmess["servername"] = host
}
vmess["tls"] = true vmess["tls"] = true
} }
@@ -206,13 +266,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "http": case "http":
headers := make(map[string]any) headers := make(map[string]any)
httpOpts := make(map[string]any) httpOpts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = []string{host.(string)} // headers["Host"] = RandHost()
} headers["User-Agent"] = RandUserAgent()
httpOpts["path"] = []string{"/"} httpOpts["method"] = values["method"]
if path, ok := values["path"]; ok && path != "" { httpOpts["path"] = values["path"]
httpOpts["path"] = []string{path.(string)}
}
httpOpts["headers"] = headers httpOpts["headers"] = headers
vmess["http-opts"] = httpOpts vmess["http-opts"] = httpOpts
@@ -220,10 +278,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "h2": case "h2":
headers := make(map[string]any) headers := make(map[string]any)
h2Opts := make(map[string]any) h2Opts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = []string{host.(string)}
}
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
h2Opts["path"] = values["path"] h2Opts["path"] = values["path"]
h2Opts["headers"] = headers h2Opts["headers"] = headers
@@ -232,14 +289,15 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "ws": case "ws":
headers := make(map[string]any) headers := make(map[string]any)
wsOpts := make(map[string]any) wsOpts := make(map[string]any)
wsOpts["path"] = []string{"/"}
if host, ok := values["host"]; ok && host != "" { headers["Host"] = RandHost()
headers["Host"] = host.(string) headers["User-Agent"] = RandUserAgent()
}
if path, ok := values["path"]; ok && path != "" { if values["path"] != nil {
wsOpts["path"] = path.(string) wsOpts["path"] = values["path"]
} }
wsOpts["headers"] = headers wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts vmess["ws-opts"] = wsOpts
case "grpc": case "grpc":
@@ -260,7 +318,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
port := urlSS.Port() port := urlSS.Port()
if port == "" { if port == "" {
dcBuf, err := encRaw.DecodeString(urlSS.Host) dcBuf, err := enc.DecodeString(urlSS.Host)
if err != nil { if err != nil {
continue continue
} }
@@ -277,17 +335,18 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
) )
if password, found = urlSS.User.Password(); !found { if password, found = urlSS.User.Password(); !found {
dcBuf, _ := enc.DecodeString(cipher) dcBuf, err := enc.DecodeString(cipher)
if !strings.Contains(string(dcBuf), "2022-blake3") { if err != nil {
dcBuf, _ = encRaw.DecodeString(cipher) continue
} }
cipher, password, found = strings.Cut(string(dcBuf), ":") cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found { if !found {
continue continue
} }
} }
ss := make(map[string]any, 10) ss := make(map[string]any, 20)
ss["name"] = name ss["name"] = name
ss["type"] = scheme ss["type"] = scheme
@@ -295,22 +354,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
ss["port"] = urlSS.Port() ss["port"] = urlSS.Port()
ss["cipher"] = cipher ss["cipher"] = cipher
ss["password"] = password ss["password"] = password
query := urlSS.Query()
ss["udp"] = true 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"
ss["plugin-opts"] = map[string]any{
"host": obfsParams[2][10:],
"mode": obfsParams[1][5:],
}
}
proxies = append(proxies, ss) proxies = append(proxies, ss)
case "ssr": case "ssr":
dcBuf, err := encRaw.DecodeString(body) dcBuf, err := enc.DecodeString(body)
if err != nil { if err != nil {
continue continue
} }
@@ -377,6 +425,18 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
return proxies, nil return proxies, nil
} }
func urlSafe(data string) string {
return strings.ReplaceAll(strings.ReplaceAll(data, "+", "-"), "/", "_")
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.URLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}
func uniqueName(names map[string]int, name string) string { func uniqueName(names map[string]int, name string) string {
if index, ok := names[name]; ok { if index, ok := names[name]; ok {
index++ index++

View File

@@ -1,89 +0,0 @@
package convert
import (
"net/url"
"strings"
)
func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy map[string]any) {
// Xray VMessAEAD / VLESS share link standard
// https://github.com/XTLS/Xray-core/discussions/716
query := url.Query()
proxy["name"] = uniqueName(names, url.Fragment)
proxy["type"] = scheme
proxy["server"] = url.Hostname()
proxy["port"] = url.Port()
proxy["uuid"] = url.User.Username()
proxy["udp"] = true
proxy["skip-cert-verify"] = false
proxy["tls"] = false
tls := strings.ToLower(query.Get("security"))
if strings.HasSuffix(tls, "tls") {
proxy["tls"] = true
}
if sni := query.Get("sni"); sni != "" {
proxy["servername"] = sni
}
network := strings.ToLower(query.Get("type"))
if network == "" {
network = "tcp"
}
fakeType := strings.ToLower(query.Get("headerType"))
if fakeType == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
proxy["network"] = network
switch network {
case "tcp":
if fakeType != "none" {
headers := make(map[string]any)
httpOpts := make(map[string]any)
httpOpts["path"] = []string{"/"}
if host := query.Get("host"); host != "" {
headers["Host"] = []string{host}
}
if method := query.Get("method"); method != "" {
httpOpts["method"] = method
}
if path := query.Get("path"); path != "" {
httpOpts["path"] = []string{path}
}
httpOpts["headers"] = headers
proxy["http-opts"] = httpOpts
}
case "http":
headers := make(map[string]any)
h2Opts := make(map[string]any)
h2Opts["path"] = []string{"/"}
if path := query.Get("path"); path != "" {
h2Opts["path"] = []string{path}
}
if host := query.Get("host"); host != "" {
h2Opts["host"] = []string{host}
}
h2Opts["headers"] = headers
proxy["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
headers["Host"] = query.Get("host")
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
proxy["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
proxy["grpc-opts"] = grpcOpts
}
}

View File

@@ -1,36 +0,0 @@
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,
}
}

View File

@@ -1,100 +0,0 @@
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}
}

View File

@@ -4,6 +4,8 @@ import (
"io" "io"
"net" "net"
"time" "time"
"github.com/Dreamacro/clash/common/pool"
) )
// Relay copies between left and right bidirectionally. // Relay copies between left and right bidirectionally.
@@ -11,14 +13,18 @@ func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error) ch := make(chan error)
go func() { go func() {
buf := pool.Get(pool.RelayBufferSize)
// Wrapping to avoid using *net.TCPConn.(ReadFrom) // Wrapping to avoid using *net.TCPConn.(ReadFrom)
// See also https://github.com/Dreamacro/clash/pull/1209 // See also https://github.com/Dreamacro/clash/pull/1209
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}) _, err := io.CopyBuffer(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}, buf)
pool.Put(buf)
leftConn.SetReadDeadline(time.Now()) leftConn.SetReadDeadline(time.Now())
ch <- err ch <- err
}() }()
_, _ = io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}) buf := pool.Get(pool.RelayBufferSize)
io.CopyBuffer(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}, buf)
pool.Put(buf)
rightConn.SetReadDeadline(time.Now()) rightConn.SetReadDeadline(time.Now())
<-ch <-ch
} }

View File

@@ -1,19 +0,0 @@
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
}

View File

@@ -6,10 +6,16 @@ import (
"errors" "errors"
"math/bits" "math/bits"
"sync" "sync"
"github.com/sagernet/sing/common/buf"
) )
var defaultAllocator = NewAllocator() var defaultAllocator = NewAllocator()
func init() {
buf.DefaultAllocator = defaultAllocator
}
// Allocator for incoming frames, optimized to prevent overwriting after zeroing // Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct { type Allocator struct {
buffers []sync.Pool buffers []sync.Pool

View File

@@ -1,7 +0,0 @@
package pool
import "github.com/sagernet/sing/common/buf"
func init() {
buf.DefaultAllocator = defaultAllocator
}

View File

@@ -3,7 +3,6 @@ package structure
// references: https://github.com/mitchellh/mapstructure // references: https://github.com/mitchellh/mapstructure
import ( import (
"encoding/base64"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
@@ -14,11 +13,8 @@ import (
type Option struct { type Option struct {
TagName string TagName string
WeaklyTypedInput bool WeaklyTypedInput bool
KeyReplacer *strings.Replacer
} }
var DefaultKeyReplacer = strings.NewReplacer("_", "-")
// Decoder is the core of structure // Decoder is the core of structure
type Decoder struct { type Decoder struct {
option *Option option *Option
@@ -53,23 +49,6 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
omitempty := found && omitKey == "omitempty" omitempty := found && omitKey == "omitempty"
value, ok := src[key] 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 !ok || value == nil {
if omitempty { if omitempty {
continue continue
@@ -86,16 +65,9 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
} }
func (d *Decoder) decode(name string, data any, val reflect.Value) error { func (d *Decoder) decode(name string, data any, val reflect.Value) error {
kind := val.Kind() switch val.Kind() {
switch { case reflect.Int:
case isInt(kind):
return d.decodeInt(name, data, val) 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: case reflect.String:
return d.decodeString(name, data, val) return d.decodeString(name, data, val)
case reflect.Bool: case reflect.Bool:
@@ -113,42 +85,13 @@ 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) { func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case isInt(kind): case kind == reflect.Int:
val.SetInt(dataVal.Int()) val.SetInt(dataVal.Int())
case isUint(kind) && d.option.WeaklyTypedInput: case kind == reflect.Float64 && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Uint()))
case isFloat(kind) && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float())) val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput: case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64 var i int64
@@ -167,72 +110,14 @@ func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error
return err 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) { func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case kind == reflect.String: case kind == reflect.String:
val.SetString(dataVal.String()) val.SetString(dataVal.String())
case isInt(kind) && d.option.WeaklyTypedInput: case kind == reflect.Int && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatInt(dataVal.Int(), 10)) 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: default:
err = fmt.Errorf( err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'", "'%s' expected type '%s', got unconvertible type '%s'",
@@ -248,10 +133,8 @@ func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err erro
switch { switch {
case kind == reflect.Bool: case kind == reflect.Bool:
val.SetBool(dataVal.Bool()) val.SetBool(dataVal.Bool())
case isInt(kind) && d.option.WeaklyTypedInput: case kind == reflect.Int && d.option.WeaklyTypedInput:
val.SetBool(dataVal.Int() != 0) val.SetBool(dataVal.Int() != 0)
case isUint(kind) && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatUint(dataVal.Uint(), 10))
default: default:
err = fmt.Errorf( err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'", "'%s' expected type '%s', got unconvertible type '%s'",
@@ -266,17 +149,6 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
valType := val.Type() valType := val.Type()
valElemType := valType.Elem() 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 { if dataVal.Kind() != reflect.Slice {
return fmt.Errorf("'%s' is not a slice", name) return fmt.Errorf("'%s' is not a slice", name)
} }
@@ -481,18 +353,12 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !rawMapVal.IsValid() { if !rawMapVal.IsValid() {
// Do a slower search by iterating over each key and // Do a slower search by iterating over each key and
// doing case-insensitive search. // doing case-insensitive search.
if d.option.KeyReplacer != nil {
fieldName = d.option.KeyReplacer.Replace(fieldName)
}
for dataValKey := range dataValKeys { for dataValKey := range dataValKeys {
mK, ok := dataValKey.Interface().(string) mK, ok := dataValKey.Interface().(string)
if !ok { if !ok {
// Not a string key // Not a string key
continue continue
} }
if d.option.KeyReplacer != nil {
mK = d.option.KeyReplacer.Replace(mK)
}
if strings.EqualFold(mK, fieldName) { if strings.EqualFold(mK, fieldName) {
rawMapKey = dataValKey rawMapKey = dataValKey

View File

@@ -137,45 +137,3 @@ func TestStructure_Nest(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal) 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)
}

View File

@@ -1,9 +1,10 @@
package utils package utils
import ( import (
"github.com/gofrs/uuid"
"reflect" "reflect"
"testing" "testing"
"github.com/gofrs/uuid"
) )
func TestUUIDMap(t *testing.T) { func TestUUIDMap(t *testing.T) {

View File

@@ -8,7 +8,6 @@ import (
"github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
) )

View File

@@ -6,7 +6,6 @@ import (
"syscall" "syscall"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )

View File

@@ -6,13 +6,9 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"runtime"
"strings"
"sync" "sync"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"go.uber.org/atomic"
) )
var ( var (
@@ -21,22 +17,9 @@ var (
actualDualStackDialContext = dualStackDialContext actualDualStackDialContext = dualStackDialContext
tcpConcurrent = false tcpConcurrent = false
DisableIPv6 = false DisableIPv6 = false
ErrorInvalidedNetworkStack = errors.New("invalided network stack")
ErrorDisableIPv6 = errors.New("IPv6 is disabled, dialer cancel")
) )
func ParseNetwork(network string, addr netip.Addr) string { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
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{ opt := &option{
interfaceName: DefaultInterface.Load(), interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()), routingMark: int(DefaultRoutingMark.Load()),
@@ -50,29 +33,13 @@ func applyOptions(options ...Option) *option {
o(opt) 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"
} else {
network = "udp"
}
network = fmt.Sprintf("%s%d", network, opt.network)
}
switch network { switch network {
case "tcp4", "tcp6", "udp4", "udp6": case "tcp4", "tcp6", "udp4", "udp6":
return actualSingleDialContext(ctx, network, address, opt) return actualSingleDialContext(ctx, network, address, opt)
case "tcp", "udp": case "tcp", "udp":
return actualDualStackDialContext(ctx, network, address, opt) return actualDualStackDialContext(ctx, network, address, opt)
default: default:
return nil, ErrorInvalidedNetworkStack return nil, errors.New("network invalid")
} }
} }
@@ -138,7 +105,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
} }
if DisableIPv6 && destination.Is6() { if DisableIPv6 && destination.Is6() {
return nil, ErrorDisableIPv6 return nil, fmt.Errorf("IPv6 is diabled, dialer cancel")
} }
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
@@ -163,7 +130,7 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
results := make(chan dialResult) results := make(chan dialResult)
var primary, fallback dialResult var primary, fallback dialResult
startRacer := func(ctx context.Context, network, host string, r resolver.Resolver, ipv6 bool) { startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) {
result := dialResult{ipv6: ipv6, done: true} result := dialResult{ipv6: ipv6, done: true}
defer func() { defer func() {
select { select {
@@ -177,16 +144,16 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
var ip netip.Addr var ip netip.Addr
if ipv6 { if ipv6 {
if r == nil { if !direct {
ip, result.error = resolver.ResolveIPv6ProxyServerHost(ctx, host) ip, result.error = resolver.ResolveIPv6ProxyServerHost(host)
} else { } else {
ip, result.error = resolver.ResolveIPv6WithResolver(ctx, host, r) ip, result.error = resolver.ResolveIPv6(host)
} }
} else { } else {
if r == nil { if !direct {
ip, result.error = resolver.ResolveIPv4ProxyServerHost(ctx, host) ip, result.error = resolver.ResolveIPv4ProxyServerHost(host)
} else { } else {
ip, result.error = resolver.ResolveIPv4WithResolver(ctx, host, r) ip, result.error = resolver.ResolveIPv4(host)
} }
} }
if result.error != nil { if result.error != nil {
@@ -197,44 +164,32 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
result.Conn, result.error = dialContext(ctx, network, ip, port, opt) result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
} }
go startRacer(ctx, network+"4", host, opt.resolver, false) go startRacer(ctx, network+"4", host, opt.direct, false)
go startRacer(ctx, network+"6", host, opt.resolver, true) go startRacer(ctx, network+"6", host, opt.direct, true)
count := 2 for res := range results {
for i := 0; i < count; i++ { if res.error == nil {
select { return res.Conn, nil
case res := <-results: }
if res.error == nil {
return res.Conn, nil
}
if !res.ipv6 { if !res.ipv6 {
primary = res primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else { } else {
fallback = res return nil, primary.error
} }
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else {
return nil, primary.error
}
}
case <-ctx.Done():
err = ctx.Err()
break
} }
} }
if err == nil { return nil, errors.New("never touched")
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) { func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
@@ -244,14 +199,11 @@ func concurrentDualStackDialContext(ctx context.Context, network, address string
} }
var ips []netip.Addr var ips []netip.Addr
if opt.resolver != nil {
ips, err = resolver.LookupIPWithResolver(ctx, host, opt.resolver)
} else {
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
}
if err != nil { if opt.direct {
return nil, err ips, err = resolver.ResolveAllIP(host)
} else {
ips, err = resolver.ResolveAllIPProxyServerHost(host)
} }
return concurrentDialContext(ctx, network, ips, port, opt) return concurrentDialContext(ctx, network, ips, port, opt)
@@ -265,49 +217,30 @@ func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr
ip netip.Addr ip netip.Addr
net.Conn net.Conn
error error
isPrimary bool resolved bool
done bool
} }
preferCount := atomic.NewInt32(0)
results := make(chan dialResult) results := make(chan dialResult)
tcpRacer := func(ctx context.Context, ip netip.Addr) { tcpRacer := func(ctx context.Context, ip netip.Addr) {
result := dialResult{ip: ip, done: true} result := dialResult{ip: ip}
defer func() { defer func() {
select { select {
case results <- result: case results <- result:
case <-returned: case <-returned:
if result.Conn != nil { if result.Conn != nil {
_ = result.Conn.Close() result.Conn.Close()
} }
} }
}() }()
if strings.Contains(network, "tcp") {
network = "tcp"
} else {
network = "udp"
}
v := "4"
if ip.Is6() { if ip.Is6() {
network += "6" v = "6"
if opt.prefer != 4 {
result.isPrimary = true
}
} }
if ip.Is4() { result.Conn, result.error = dialContext(ctx, network+v, ip, port, opt)
network += "4"
if opt.prefer != 6 {
result.isPrimary = true
}
}
if result.isPrimary {
preferCount.Add(1)
}
result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
} }
for _, ip := range ips { for _, ip := range ips {
@@ -315,57 +248,18 @@ func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr
} }
connCount := len(ips) connCount := len(ips)
var fallback dialResult for res := range results {
var primaryError error connCount--
var finalError error if res.error == nil {
for i := 0; i < connCount; i++ { return res.Conn, nil
select { }
case res := <-results:
if res.error == nil { if connCount == 0 {
if res.isPrimary {
return res.Conn, nil
} else {
if !fallback.done || fallback.error != nil {
fallback = res
}
}
} else {
if res.isPrimary {
primaryError = res.error
preferCount.Add(-1)
if preferCount.Load() == 0 && fallback.done && fallback.error == nil {
return fallback.Conn, nil
}
}
}
case <-ctx.Done():
if fallback.done && fallback.error == nil {
return fallback.Conn, nil
}
finalError = ctx.Err()
break break
} }
} }
if fallback.done && fallback.error == nil { return nil, fmt.Errorf("all ips %v tcp shake hands failed", ips)
return fallback.Conn, nil
}
if primaryError != nil {
return nil, primaryError
}
if fallback.error != nil {
return nil, fallback.error
}
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) { func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
@@ -377,16 +271,16 @@ func singleDialContext(ctx context.Context, network string, address string, opt
var ip netip.Addr var ip netip.Addr
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
if opt.resolver == nil { if !opt.direct {
ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host) ip, err = resolver.ResolveIPv4ProxyServerHost(host)
} else { } else {
ip, err = resolver.ResolveIPv4WithResolver(ctx, host, opt.resolver) ip, err = resolver.ResolveIPv4(host)
} }
default: default:
if opt.resolver == nil { if !opt.direct {
ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host) ip, err = resolver.ResolveIPv6ProxyServerHost(host)
} else { } else {
ip, err = resolver.ResolveIPv6WithResolver(ctx, host, opt.resolver) ip, err = resolver.ResolveIPv6(host)
} }
} }
if err != nil { if err != nil {
@@ -397,25 +291,25 @@ func singleDialContext(ctx context.Context, network string, address string, opt
} }
func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
return concurrentIPv4DialContext(ctx, network, address, opt) if !opt.direct {
ips, err = resolver.ResolveAllIPv4ProxyServerHost(host)
} else {
ips, err = resolver.ResolveAllIPv4(host)
}
default: default:
return concurrentIPv6DialContext(ctx, network, address, opt) if !opt.direct {
} ips, err = resolver.ResolveAllIPv6ProxyServerHost(host)
} } else {
ips, err = resolver.ResolveAllIPv6(host)
func concurrentIPv4DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { }
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
if opt.resolver == nil {
ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPv4WithResolver(ctx, host, opt.resolver)
} }
if err != nil { if err != nil {
@@ -424,40 +318,3 @@ func concurrentIPv4DialContext(ctx context.Context, network, address string, opt
return concurrentDialContext(ctx, network, ips, port, opt) return concurrentDialContext(ctx, network, ips, port, opt)
} }
func concurrentIPv6DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
if opt.resolver == nil {
ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPv6WithResolver(ctx, host, opt.resolver)
}
if err != nil {
return nil, err
}
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}
}

View File

@@ -29,13 +29,13 @@ func bindMarkToControl(mark int, chain controlFn) controlFn {
return return
} }
var innerErr error return c.Control(func(fd uintptr) {
err = c.Control(func(fd uintptr) { switch network {
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark) 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)
}
}) })
if innerErr != nil {
err = innerErr
}
return
} }
} }

View File

@@ -1,10 +1,6 @@
package dialer package dialer
import ( import "go.uber.org/atomic"
"github.com/Dreamacro/clash/component/resolver"
"go.uber.org/atomic"
)
var ( var (
DefaultOptions []Option DefaultOptions []Option
@@ -16,9 +12,7 @@ type option struct {
interfaceName string interfaceName string
addrReuse bool addrReuse bool
routingMark int routingMark int
network int direct bool
prefer int
resolver resolver.Resolver
} }
type Option func(opt *option) type Option func(opt *option)
@@ -41,36 +35,8 @@ func WithRoutingMark(mark int) Option {
} }
} }
func WithResolver(r resolver.Resolver) Option { func WithDirect() Option {
return func(opt *option) { return func(opt *option) {
opt.resolver = r opt.direct = true
}
}
func WithPreferIPv4() Option {
return func(opt *option) {
opt.prefer = 4
}
}
func WithPreferIPv6() Option {
return func(opt *option) {
opt.prefer = 6
}
}
func WithOnlySingleStack(isIPv4 bool) Option {
return func(opt *option) {
if isIPv4 {
opt.network = 4
} else {
opt.network = 6
}
}
}
func WithOption(o option) Option {
return func(opt *option) {
*opt = o
} }
} }

View File

@@ -1,99 +0,0 @@
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __BPF_ENDIAN__
#define __BPF_ENDIAN__
/*
* Isolate byte #n and put it into byte #m, for __u##b type.
* E.g., moving byte #6 (nnnnnnnn) into byte #1 (mmmmmmmm) for __u64:
* 1) xxxxxxxx nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx
* 2) nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 00000000
* 3) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn
* 4) 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 00000000
*/
#define ___bpf_mvb(x, b, n, m) ((__u##b)(x) << (b-(n+1)*8) >> (b-8) << (m*8))
#define ___bpf_swab16(x) ((__u16)( \
___bpf_mvb(x, 16, 0, 1) | \
___bpf_mvb(x, 16, 1, 0)))
#define ___bpf_swab32(x) ((__u32)( \
___bpf_mvb(x, 32, 0, 3) | \
___bpf_mvb(x, 32, 1, 2) | \
___bpf_mvb(x, 32, 2, 1) | \
___bpf_mvb(x, 32, 3, 0)))
#define ___bpf_swab64(x) ((__u64)( \
___bpf_mvb(x, 64, 0, 7) | \
___bpf_mvb(x, 64, 1, 6) | \
___bpf_mvb(x, 64, 2, 5) | \
___bpf_mvb(x, 64, 3, 4) | \
___bpf_mvb(x, 64, 4, 3) | \
___bpf_mvb(x, 64, 5, 2) | \
___bpf_mvb(x, 64, 6, 1) | \
___bpf_mvb(x, 64, 7, 0)))
/* LLVM's BPF target selects the endianness of the CPU
* it compiles on, or the user specifies (bpfel/bpfeb),
* respectively. The used __BYTE_ORDER__ is defined by
* the compiler, we cannot rely on __BYTE_ORDER from
* libc headers, since it doesn't reflect the actual
* requested byte order.
*
* Note, LLVM's BPF target has different __builtin_bswapX()
* semantics. It does map to BPF_ALU | BPF_END | BPF_TO_BE
* in bpfel and bpfeb case, which means below, that we map
* to cpu_to_be16(). We could use it unconditionally in BPF
* case, but better not rely on it, so that this header here
* can be used from application and BPF program side, which
* use different targets.
*/
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define __bpf_ntohs(x) __builtin_bswap16(x)
# define __bpf_htons(x) __builtin_bswap16(x)
# define __bpf_constant_ntohs(x) ___bpf_swab16(x)
# define __bpf_constant_htons(x) ___bpf_swab16(x)
# define __bpf_ntohl(x) __builtin_bswap32(x)
# define __bpf_htonl(x) __builtin_bswap32(x)
# define __bpf_constant_ntohl(x) ___bpf_swab32(x)
# define __bpf_constant_htonl(x) ___bpf_swab32(x)
# define __bpf_be64_to_cpu(x) __builtin_bswap64(x)
# define __bpf_cpu_to_be64(x) __builtin_bswap64(x)
# define __bpf_constant_be64_to_cpu(x) ___bpf_swab64(x)
# define __bpf_constant_cpu_to_be64(x) ___bpf_swab64(x)
#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
# define __bpf_ntohs(x) (x)
# define __bpf_htons(x) (x)
# define __bpf_constant_ntohs(x) (x)
# define __bpf_constant_htons(x) (x)
# define __bpf_ntohl(x) (x)
# define __bpf_htonl(x) (x)
# define __bpf_constant_ntohl(x) (x)
# define __bpf_constant_htonl(x) (x)
# define __bpf_be64_to_cpu(x) (x)
# define __bpf_cpu_to_be64(x) (x)
# define __bpf_constant_be64_to_cpu(x) (x)
# define __bpf_constant_cpu_to_be64(x) (x)
#else
# error "Fix your compiler's __BYTE_ORDER__?!"
#endif
#define bpf_htons(x) \
(__builtin_constant_p(x) ? \
__bpf_constant_htons(x) : __bpf_htons(x))
#define bpf_ntohs(x) \
(__builtin_constant_p(x) ? \
__bpf_constant_ntohs(x) : __bpf_ntohs(x))
#define bpf_htonl(x) \
(__builtin_constant_p(x) ? \
__bpf_constant_htonl(x) : __bpf_htonl(x))
#define bpf_ntohl(x) \
(__builtin_constant_p(x) ? \
__bpf_constant_ntohl(x) : __bpf_ntohl(x))
#define bpf_cpu_to_be64(x) \
(__builtin_constant_p(x) ? \
__bpf_constant_cpu_to_be64(x) : __bpf_cpu_to_be64(x))
#define bpf_be64_to_cpu(x) \
(__builtin_constant_p(x) ? \
__bpf_constant_be64_to_cpu(x) : __bpf_be64_to_cpu(x))
#endif /* __BPF_ENDIAN__ */

File diff suppressed because it is too large Load Diff

View File

@@ -1,262 +0,0 @@
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __BPF_HELPERS__
#define __BPF_HELPERS__
/*
* Note that bpf programs need to include either
* vmlinux.h (auto-generated from BTF) or linux/types.h
* in advance since bpf_helper_defs.h uses such types
* as __u64.
*/
#include "bpf_helper_defs.h"
#define __uint(name, val) int (*name)[val]
#define __type(name, val) typeof(val) *name
#define __array(name, val) typeof(val) *name[]
/*
* Helper macro to place programs, maps, license in
* different sections in elf_bpf file. Section names
* are interpreted by libbpf depending on the context (BPF programs, BPF maps,
* extern variables, etc).
* To allow use of SEC() with externs (e.g., for extern .maps declarations),
* make sure __attribute__((unused)) doesn't trigger compilation warning.
*/
#define SEC(name) \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wignored-attributes\"") \
__attribute__((section(name), used)) \
_Pragma("GCC diagnostic pop") \
/* Avoid 'linux/stddef.h' definition of '__always_inline'. */
#undef __always_inline
#define __always_inline inline __attribute__((always_inline))
#ifndef __noinline
#define __noinline __attribute__((noinline))
#endif
#ifndef __weak
#define __weak __attribute__((weak))
#endif
/*
* Use __hidden attribute to mark a non-static BPF subprogram effectively
* static for BPF verifier's verification algorithm purposes, allowing more
* extensive and permissive BPF verification process, taking into account
* subprogram's caller context.
*/
#define __hidden __attribute__((visibility("hidden")))
/* When utilizing vmlinux.h with BPF CO-RE, user BPF programs can't include
* any system-level headers (such as stddef.h, linux/version.h, etc), and
* commonly-used macros like NULL and KERNEL_VERSION aren't available through
* vmlinux.h. This just adds unnecessary hurdles and forces users to re-define
* them on their own. So as a convenience, provide such definitions here.
*/
#ifndef NULL
#define NULL ((void *)0)
#endif
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c)))
#endif
/*
* Helper macros to manipulate data structures
*/
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((unsigned long)&((TYPE *)0)->MEMBER)
#endif
#ifndef container_of
#define container_of(ptr, type, member) \
({ \
void *__mptr = (void *)(ptr); \
((type *)(__mptr - offsetof(type, member))); \
})
#endif
/*
* Helper macro to throw a compilation error if __bpf_unreachable() gets
* built into the resulting code. This works given BPF back end does not
* implement __builtin_trap(). This is useful to assert that certain paths
* of the program code are never used and hence eliminated by the compiler.
*
* For example, consider a switch statement that covers known cases used by
* the program. __bpf_unreachable() can then reside in the default case. If
* the program gets extended such that a case is not covered in the switch
* statement, then it will throw a build error due to the default case not
* being compiled out.
*/
#ifndef __bpf_unreachable
# define __bpf_unreachable() __builtin_trap()
#endif
/*
* Helper function to perform a tail call with a constant/immediate map slot.
*/
#if __clang_major__ >= 8 && defined(__bpf__)
static __always_inline void
bpf_tail_call_static(void *ctx, const void *map, const __u32 slot)
{
if (!__builtin_constant_p(slot))
__bpf_unreachable();
/*
* Provide a hard guarantee that LLVM won't optimize setting r2 (map
* pointer) and r3 (constant map index) from _different paths_ ending
* up at the _same_ call insn as otherwise we won't be able to use the
* jmpq/nopl retpoline-free patching by the x86-64 JIT in the kernel
* given they mismatch. See also d2e4c1e6c294 ("bpf: Constant map key
* tracking for prog array pokes") for details on verifier tracking.
*
* Note on clobber list: we need to stay in-line with BPF calling
* convention, so even if we don't end up using r0, r4, r5, we need
* to mark them as clobber so that LLVM doesn't end up using them
* before / after the call.
*/
asm volatile("r1 = %[ctx]\n\t"
"r2 = %[map]\n\t"
"r3 = %[slot]\n\t"
"call 12"
:: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot)
: "r0", "r1", "r2", "r3", "r4", "r5");
}
#endif
/*
* Helper structure used by eBPF C program
* to describe BPF map attributes to libbpf loader
*/
struct bpf_map_def {
unsigned int type;
unsigned int key_size;
unsigned int value_size;
unsigned int max_entries;
unsigned int map_flags;
};
enum libbpf_pin_type {
LIBBPF_PIN_NONE,
/* PIN_BY_NAME: pin maps by name (in /sys/fs/bpf by default) */
LIBBPF_PIN_BY_NAME,
};
enum libbpf_tristate {
TRI_NO = 0,
TRI_YES = 1,
TRI_MODULE = 2,
};
#define __kconfig __attribute__((section(".kconfig")))
#define __ksym __attribute__((section(".ksyms")))
#ifndef ___bpf_concat
#define ___bpf_concat(a, b) a ## b
#endif
#ifndef ___bpf_apply
#define ___bpf_apply(fn, n) ___bpf_concat(fn, n)
#endif
#ifndef ___bpf_nth
#define ___bpf_nth(_, _1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, N, ...) N
#endif
#ifndef ___bpf_narg
#define ___bpf_narg(...) \
___bpf_nth(_, ##__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#endif
#define ___bpf_fill0(arr, p, x) do {} while (0)
#define ___bpf_fill1(arr, p, x) arr[p] = x
#define ___bpf_fill2(arr, p, x, args...) arr[p] = x; ___bpf_fill1(arr, p + 1, args)
#define ___bpf_fill3(arr, p, x, args...) arr[p] = x; ___bpf_fill2(arr, p + 1, args)
#define ___bpf_fill4(arr, p, x, args...) arr[p] = x; ___bpf_fill3(arr, p + 1, args)
#define ___bpf_fill5(arr, p, x, args...) arr[p] = x; ___bpf_fill4(arr, p + 1, args)
#define ___bpf_fill6(arr, p, x, args...) arr[p] = x; ___bpf_fill5(arr, p + 1, args)
#define ___bpf_fill7(arr, p, x, args...) arr[p] = x; ___bpf_fill6(arr, p + 1, args)
#define ___bpf_fill8(arr, p, x, args...) arr[p] = x; ___bpf_fill7(arr, p + 1, args)
#define ___bpf_fill9(arr, p, x, args...) arr[p] = x; ___bpf_fill8(arr, p + 1, args)
#define ___bpf_fill10(arr, p, x, args...) arr[p] = x; ___bpf_fill9(arr, p + 1, args)
#define ___bpf_fill11(arr, p, x, args...) arr[p] = x; ___bpf_fill10(arr, p + 1, args)
#define ___bpf_fill12(arr, p, x, args...) arr[p] = x; ___bpf_fill11(arr, p + 1, args)
#define ___bpf_fill(arr, args...) \
___bpf_apply(___bpf_fill, ___bpf_narg(args))(arr, 0, args)
/*
* BPF_SEQ_PRINTF to wrap bpf_seq_printf to-be-printed values
* in a structure.
*/
#define BPF_SEQ_PRINTF(seq, fmt, args...) \
({ \
static const char ___fmt[] = fmt; \
unsigned long long ___param[___bpf_narg(args)]; \
\
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \
___bpf_fill(___param, args); \
_Pragma("GCC diagnostic pop") \
\
bpf_seq_printf(seq, ___fmt, sizeof(___fmt), \
___param, sizeof(___param)); \
})
/*
* BPF_SNPRINTF wraps the bpf_snprintf helper with variadic arguments instead of
* an array of u64.
*/
#define BPF_SNPRINTF(out, out_size, fmt, args...) \
({ \
static const char ___fmt[] = fmt; \
unsigned long long ___param[___bpf_narg(args)]; \
\
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \
___bpf_fill(___param, args); \
_Pragma("GCC diagnostic pop") \
\
bpf_snprintf(out, out_size, ___fmt, \
___param, sizeof(___param)); \
})
#ifdef BPF_NO_GLOBAL_DATA
#define BPF_PRINTK_FMT_MOD
#else
#define BPF_PRINTK_FMT_MOD static const
#endif
#define __bpf_printk(fmt, ...) \
({ \
BPF_PRINTK_FMT_MOD char ____fmt[] = fmt; \
bpf_trace_printk(____fmt, sizeof(____fmt), \
##__VA_ARGS__); \
})
/*
* __bpf_vprintk wraps the bpf_trace_vprintk helper with variadic arguments
* instead of an array of u64.
*/
#define __bpf_vprintk(fmt, args...) \
({ \
static const char ___fmt[] = fmt; \
unsigned long long ___param[___bpf_narg(args)]; \
\
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \
___bpf_fill(___param, args); \
_Pragma("GCC diagnostic pop") \
\
bpf_trace_vprintk(___fmt, sizeof(___fmt), \
___param, sizeof(___param)); \
})
/* Use __bpf_printk when bpf_printk call has 3 or fewer fmt args
* Otherwise use __bpf_vprintk
*/
#define ___bpf_pick_printk(...) \
___bpf_nth(_, ##__VA_ARGS__, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \
__bpf_vprintk, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \
__bpf_vprintk, __bpf_vprintk, __bpf_printk /*3*/, __bpf_printk /*2*/,\
__bpf_printk /*1*/, __bpf_printk /*0*/)
/* Helper macro to print out debug messages */
#define bpf_printk(fmt, args...) ___bpf_pick_printk(args)(fmt, ##args)
#endif

View File

@@ -1,342 +0,0 @@
#include <stdint.h>
#include <stdbool.h>
//#include <linux/types.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
//#include <linux/if_packet.h>
//#include <linux/if_vlan.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/tcp.h>
//#include <linux/udp.h>
#include <linux/pkt_cls.h>
#include "bpf_endian.h"
#include "bpf_helpers.h"
#define IP_CSUM_OFF (ETH_HLEN + offsetof(struct iphdr, check))
#define IP_DST_OFF (ETH_HLEN + offsetof(struct iphdr, daddr))
#define IP_SRC_OFF (ETH_HLEN + offsetof(struct iphdr, saddr))
#define IP_PROTO_OFF (ETH_HLEN + offsetof(struct iphdr, protocol))
#define TCP_CSUM_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, check))
#define TCP_SRC_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, source))
#define TCP_DST_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, dest))
//#define UDP_CSUM_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct udphdr, check))
//#define UDP_SRC_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct udphdr, source))
//#define UDP_DST_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct udphdr, dest))
#define IS_PSEUDO 0x10
struct origin_info {
__be32 ip;
__be16 port;
__u16 pad;
};
struct origin_info *origin_info_unused __attribute__((unused));
struct redir_info {
__be32 sip;
__be32 dip;
__be16 sport;
__be16 dport;
};
struct redir_info *redir_info_unused __attribute__((unused));
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct redir_info);
__type(value, struct origin_info);
__uint(max_entries, 65535);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} pair_original_dst_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, __u32);
__uint(max_entries, 3);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} redir_params_map SEC(".maps");
static __always_inline int rewrite_ip(struct __sk_buff *skb, __be32 new_ip, bool is_dest) {
int ret, off = 0, flags = IS_PSEUDO;
__be32 old_ip;
if (is_dest)
ret = bpf_skb_load_bytes(skb, IP_DST_OFF, &old_ip, 4);
else
ret = bpf_skb_load_bytes(skb, IP_SRC_OFF, &old_ip, 4);
if (ret < 0) {
return ret;
}
off = TCP_CSUM_OFF;
// __u8 proto;
//
// ret = bpf_skb_load_bytes(skb, IP_PROTO_OFF, &proto, 1);
// if (ret < 0) {
// return BPF_DROP;
// }
//
// switch (proto) {
// case IPPROTO_TCP:
// off = TCP_CSUM_OFF;
// break;
//
// case IPPROTO_UDP:
// off = UDP_CSUM_OFF;
// flags |= BPF_F_MARK_MANGLED_0;
// break;
//
// case IPPROTO_ICMPV6:
// off = offsetof(struct icmp6hdr, icmp6_cksum);
// break;
// }
//
// if (off) {
ret = bpf_l4_csum_replace(skb, off, old_ip, new_ip, flags | sizeof(new_ip));
if (ret < 0) {
return ret;
}
// }
ret = bpf_l3_csum_replace(skb, IP_CSUM_OFF, old_ip, new_ip, sizeof(new_ip));
if (ret < 0) {
return ret;
}
if (is_dest)
ret = bpf_skb_store_bytes(skb, IP_DST_OFF, &new_ip, sizeof(new_ip), 0);
else
ret = bpf_skb_store_bytes(skb, IP_SRC_OFF, &new_ip, sizeof(new_ip), 0);
if (ret < 0) {
return ret;
}
return 1;
}
static __always_inline int rewrite_port(struct __sk_buff *skb, __be16 new_port, bool is_dest) {
int ret, off = 0;
__be16 old_port;
if (is_dest)
ret = bpf_skb_load_bytes(skb, TCP_DST_OFF, &old_port, 2);
else
ret = bpf_skb_load_bytes(skb, TCP_SRC_OFF, &old_port, 2);
if (ret < 0) {
return ret;
}
off = TCP_CSUM_OFF;
ret = bpf_l4_csum_replace(skb, off, old_port, new_port, sizeof(new_port));
if (ret < 0) {
return ret;
}
if (is_dest)
ret = bpf_skb_store_bytes(skb, TCP_DST_OFF, &new_port, sizeof(new_port), 0);
else
ret = bpf_skb_store_bytes(skb, TCP_SRC_OFF, &new_port, sizeof(new_port), 0);
if (ret < 0) {
return ret;
}
return 1;
}
static __always_inline bool is_lan_ip(__be32 addr) {
if (addr == 0xffffffff)
return true;
__u8 fist = (__u8)(addr & 0xff);
if (fist == 127 || fist == 10)
return true;
__u8 second = (__u8)((addr >> 8) & 0xff);
if (fist == 172 && second >= 16 && second <= 31)
return true;
if (fist == 192 && second == 168)
return true;
return false;
}
SEC("tc_clash_auto_redir_ingress")
int tc_redir_ingress_func(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *iph = (struct iphdr *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return TC_ACT_OK;
__u32 key = 0, *route_index, *redir_ip, *redir_port;
route_index = bpf_map_lookup_elem(&redir_params_map, &key);
if (!route_index)
return TC_ACT_OK;
if (iph->protocol == IPPROTO_ICMP && *route_index != 0)
return bpf_redirect(*route_index, 0);
if (iph->protocol != IPPROTO_TCP)
return TC_ACT_OK;
struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
if ((void *)(tcph + 1) > data_end)
return TC_ACT_SHOT;
key = 1;
redir_ip = bpf_map_lookup_elem(&redir_params_map, &key);
if (!redir_ip)
return TC_ACT_OK;
key = 2;
redir_port = bpf_map_lookup_elem(&redir_params_map, &key);
if (!redir_port)
return TC_ACT_OK;
__be32 new_ip = bpf_htonl(*redir_ip);
__be16 new_port = bpf_htonl(*redir_port) >> 16;
__be32 old_ip = iph->daddr;
__be16 old_port = tcph->dest;
if (old_ip == new_ip || is_lan_ip(old_ip) || bpf_ntohs(old_port) == 53) {
return TC_ACT_OK;
}
struct redir_info p_key = {
.sip = iph->saddr,
.sport = tcph->source,
.dip = new_ip,
.dport = new_port,
};
if (tcph->syn && !tcph->ack) {
struct origin_info origin = {
.ip = old_ip,
.port = old_port,
};
bpf_map_update_elem(&pair_original_dst_map, &p_key, &origin, BPF_NOEXIST);
if (rewrite_ip(skb, new_ip, true) < 0) {
return TC_ACT_SHOT;
}
if (rewrite_port(skb, new_port, true) < 0) {
return TC_ACT_SHOT;
}
} else {
struct origin_info *origin = bpf_map_lookup_elem(&pair_original_dst_map, &p_key);
if (!origin) {
return TC_ACT_OK;
}
if (rewrite_ip(skb, new_ip, true) < 0) {
return TC_ACT_SHOT;
}
if (rewrite_port(skb, new_port, true) < 0) {
return TC_ACT_SHOT;
}
}
return TC_ACT_OK;
}
SEC("tc_clash_auto_redir_egress")
int tc_redir_egress_func(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
__u32 key = 0, *redir_ip, *redir_port; // *clash_mark
// clash_mark = bpf_map_lookup_elem(&redir_params_map, &key);
// if (clash_mark && *clash_mark != 0 && *clash_mark == skb->mark)
// return TC_ACT_OK;
struct iphdr *iph = (struct iphdr *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return TC_ACT_OK;
if (iph->protocol != IPPROTO_TCP)
return TC_ACT_OK;
struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
if ((void *)(tcph + 1) > data_end)
return TC_ACT_SHOT;
key = 1;
redir_ip = bpf_map_lookup_elem(&redir_params_map, &key);
if (!redir_ip)
return TC_ACT_OK;
key = 2;
redir_port = bpf_map_lookup_elem(&redir_params_map, &key);
if (!redir_port)
return TC_ACT_OK;
__be32 new_ip = bpf_htonl(*redir_ip);
__be16 new_port = bpf_htonl(*redir_port) >> 16;
__be32 old_ip = iph->saddr;
__be16 old_port = tcph->source;
if (old_ip != new_ip || old_port != new_port) {
return TC_ACT_OK;
}
struct redir_info p_key = {
.sip = iph->daddr,
.sport = tcph->dest,
.dip = iph->saddr,
.dport = tcph->source,
};
struct origin_info *origin = bpf_map_lookup_elem(&pair_original_dst_map, &p_key);
if (!origin) {
return TC_ACT_OK;
}
if (tcph->fin && tcph->ack) {
bpf_map_delete_elem(&pair_original_dst_map, &p_key);
}
if (rewrite_ip(skb, origin->ip, false) < 0) {
return TC_ACT_SHOT;
}
if (rewrite_port(skb, origin->port, false) < 0) {
return TC_ACT_SHOT;
}
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";

View File

@@ -1,103 +0,0 @@
#include <stdbool.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
//#include <linux/tcp.h>
//#include <linux/udp.h>
#include <linux/pkt_cls.h>
#include "bpf_endian.h"
#include "bpf_helpers.h"
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, __u32);
__uint(max_entries, 2);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} tc_params_map SEC(".maps");
static __always_inline bool is_lan_ip(__be32 addr) {
if (addr == 0xffffffff)
return true;
__u8 fist = (__u8)(addr & 0xff);
if (fist == 127 || fist == 10)
return true;
__u8 second = (__u8)((addr >> 8) & 0xff);
if (fist == 172 && second >= 16 && second <= 31)
return true;
if (fist == 192 && second == 168)
return true;
return false;
}
SEC("tc_clash_redirect_to_tun")
int tc_tun_func(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto == bpf_htons(ETH_P_ARP))
return TC_ACT_OK;
__u32 key = 0, *clash_mark, *tun_ifindex;
clash_mark = bpf_map_lookup_elem(&tc_params_map, &key);
if (!clash_mark)
return TC_ACT_OK;
if (skb->mark == *clash_mark)
return TC_ACT_OK;
if (eth->h_proto == bpf_htons(ETH_P_IP)) {
struct iphdr *iph = (struct iphdr *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return TC_ACT_OK;
if (iph->protocol == IPPROTO_ICMP)
return TC_ACT_OK;
__be32 daddr = iph->daddr;
if (is_lan_ip(daddr))
return TC_ACT_OK;
// if (iph->protocol == IPPROTO_TCP) {
// struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
// if ((void *)(tcph + 1) > data_end)
// return TC_ACT_OK;
//
// __u16 source = bpf_ntohs(tcph->source);
// if (source == 22 || source == 80 || source == 443 || source == 8080 || source == 8443 || source == 9090 || (source >= 7890 && source <= 7895))
// return TC_ACT_OK;
// } else if (iph->protocol == IPPROTO_UDP) {
// struct udphdr *udph = (struct udphdr *)(iph + 1);
// if ((void *)(udph + 1) > data_end)
// return TC_ACT_OK;
//
// __u16 source = bpf_ntohs(udph->source);
// if (source == 53 || (source >= 135 && source <= 139))
// return TC_ACT_OK;
// }
}
key = 1;
tun_ifindex = bpf_map_lookup_elem(&tc_params_map, &key);
if (!tun_ifindex)
return TC_ACT_OK;
//return bpf_redirect(*tun_ifindex, BPF_F_INGRESS); // __bpf_rx_skb
return bpf_redirect(*tun_ifindex, 0); // __bpf_tx_skb / __dev_xmit_skb
}
char _license[] SEC("license") = "GPL";

View File

@@ -1,13 +0,0 @@
package byteorder
import (
"net"
)
// NetIPv4ToHost32 converts an net.IP to a uint32 in host byte order. ip
// must be a IPv4 address, otherwise the function will panic.
func NetIPv4ToHost32(ip net.IP) uint32 {
ipv4 := ip.To4()
_ = ipv4[3] // Assert length of ipv4.
return Native.Uint32(ipv4)
}

View File

@@ -1,12 +0,0 @@
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
package byteorder
import "encoding/binary"
var Native binary.ByteOrder = binary.BigEndian
func HostToNetwork16(u uint16) uint16 { return u }
func HostToNetwork32(u uint32) uint32 { return u }
func NetworkToHost16(u uint16) uint16 { return u }
func NetworkToHost32(u uint32) uint32 { return u }

View File

@@ -1,15 +0,0 @@
//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
package byteorder
import (
"encoding/binary"
"math/bits"
)
var Native binary.ByteOrder = binary.LittleEndian
func HostToNetwork16(u uint16) uint16 { return bits.ReverseBytes16(u) }
func HostToNetwork32(u uint32) uint32 { return bits.ReverseBytes32(u) }
func NetworkToHost16(u uint16) uint16 { return bits.ReverseBytes16(u) }
func NetworkToHost32(u uint32) uint32 { return bits.ReverseBytes32(u) }

View File

@@ -1,33 +0,0 @@
package ebpf
import (
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
type TcEBpfProgram struct {
pros []C.EBpf
rawNICs []string
}
func (t *TcEBpfProgram) RawNICs() []string {
return t.rawNICs
}
func (t *TcEBpfProgram) Close() {
for _, p := range t.pros {
p.Close()
}
}
func (t *TcEBpfProgram) Lookup(srcAddrPort netip.AddrPort) (addr socks5.Addr, err error) {
for _, p := range t.pros {
addr, err = p.Lookup(srcAddrPort)
if err == nil {
return
}
}
return
}

View File

@@ -1,137 +0,0 @@
//go:build !android
package ebpf
import (
"fmt"
"net/netip"
"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) {
routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
if err != nil {
return "", err
}
for _, route := range routes {
if route.Dst == nil {
lk, err := netlink.LinkByIndex(route.LinkIndex)
if err != nil {
return "", err
}
if lk.Type() == "tuntap" {
continue
}
return lk.Attrs().Name, nil
}
}
return "", fmt.Errorf("interface not found")
}
// NewTcEBpfProgram new redirect to tun ebpf program
func NewTcEBpfProgram(ifaceNames []string, tunName string) (*TcEBpfProgram, error) {
tunIface, err := netlink.LinkByName(tunName)
if err != nil {
return nil, fmt.Errorf("lookup network iface %q: %w", tunName, err)
}
tunIndex := uint32(tunIface.Attrs().Index)
dialer.DefaultRoutingMark.Store(C.ClashTrafficMark)
ifMark := uint32(dialer.DefaultRoutingMark.Load())
var pros []C.EBpf
for _, ifaceName := range ifaceNames {
iface, err := netlink.LinkByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("lookup network iface %q: %w", ifaceName, err)
}
if iface.Attrs().OperState != netlink.OperUp {
return nil, fmt.Errorf("network iface %q is down", ifaceName)
}
attrs := iface.Attrs()
index := attrs.Index
tcPro := tc.NewEBpfTc(ifaceName, index, ifMark, tunIndex)
if err = tcPro.Start(); err != nil {
return nil, err
}
pros = append(pros, tcPro)
}
systemSetting(ifaceNames...)
return &TcEBpfProgram{pros: pros, rawNICs: ifaceNames}, nil
}
// NewRedirEBpfProgram new auto redirect ebpf program
func NewRedirEBpfProgram(ifaceNames []string, redirPort uint16, defaultRouteInterfaceName string) (*TcEBpfProgram, error) {
defaultRouteInterface, err := netlink.LinkByName(defaultRouteInterfaceName)
if err != nil {
return nil, fmt.Errorf("lookup network iface %q: %w", defaultRouteInterfaceName, err)
}
defaultRouteIndex := uint32(defaultRouteInterface.Attrs().Index)
var pros []C.EBpf
for _, ifaceName := range ifaceNames {
iface, err := netlink.LinkByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("lookup network iface %q: %w", ifaceName, err)
}
attrs := iface.Attrs()
index := attrs.Index
addrs, err := netlink.AddrList(iface, netlink.FAMILY_V4)
if err != nil {
return nil, fmt.Errorf("lookup network iface %q address: %w", ifaceName, err)
}
if len(addrs) == 0 {
return nil, fmt.Errorf("network iface %q does not contain any ipv4 addresses", ifaceName)
}
address, _ := netip.AddrFromSlice(addrs[0].IP)
redirAddrPort := netip.AddrPortFrom(address, redirPort)
redirPro := redir.NewEBpfRedirect(ifaceName, index, 0, defaultRouteIndex, redirAddrPort)
if err = redirPro.Start(); err != nil {
return nil, err
}
pros = append(pros, redirPro)
}
systemSetting(ifaceNames...)
return &TcEBpfProgram{pros: pros, rawNICs: ifaceNames}, nil
}
func systemSetting(ifaceNames ...string) {
_, _ = cmd.ExecCmd("sysctl -w net.ipv4.ip_forward=1")
_, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.forwarding=1")
_, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.accept_local=1")
_, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.accept_redirects=1")
_, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.rp_filter=0")
for _, ifaceName := range ifaceNames {
_, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.forwarding=1", ifaceName))
_, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.accept_local=1", ifaceName))
_, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.accept_redirects=1", ifaceName))
_, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.rp_filter=0", ifaceName))
}
}

View File

@@ -1,21 +0,0 @@
//go:build !linux || android
package ebpf
import (
"fmt"
)
// NewTcEBpfProgram new ebpf tc program
func NewTcEBpfProgram(_ []string, _ string) (*TcEBpfProgram, error) {
return nil, fmt.Errorf("system not supported")
}
// NewRedirEBpfProgram new ebpf redirect program
func NewRedirEBpfProgram(_ []string, _ uint16, _ string) (*TcEBpfProgram, error) {
return nil, fmt.Errorf("system not supported")
}
func GetAutoDetectInterface() (string, error) {
return "", fmt.Errorf("system not supported")
}

View File

@@ -1,216 +0,0 @@
//go:build linux
package redir
import (
"encoding/binary"
"fmt"
"io"
"net"
"net/netip"
"os"
"path/filepath"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/rlimit"
"github.com/sagernet/netlink"
"golang.org/x/sys/unix"
"github.com/Dreamacro/clash/component/ebpf/byteorder"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf ../bpf/redir.c
const (
mapKey1 uint32 = 0
mapKey2 uint32 = 1
mapKey3 uint32 = 2
)
type EBpfRedirect struct {
objs io.Closer
originMap *ebpf.Map
qdisc netlink.Qdisc
filter netlink.Filter
filterEgress netlink.Filter
ifName string
ifIndex int
ifMark uint32
rtIndex uint32
redirIp uint32
redirPort uint16
bpfPath string
}
func NewEBpfRedirect(ifName string, ifIndex int, ifMark uint32, routeIndex uint32, redirAddrPort netip.AddrPort) *EBpfRedirect {
return &EBpfRedirect{
ifName: ifName,
ifIndex: ifIndex,
ifMark: ifMark,
rtIndex: routeIndex,
redirIp: binary.BigEndian.Uint32(redirAddrPort.Addr().AsSlice()),
redirPort: redirAddrPort.Port(),
}
}
func (e *EBpfRedirect) Start() error {
if err := rlimit.RemoveMemlock(); err != nil {
return fmt.Errorf("remove memory lock: %w", err)
}
e.bpfPath = filepath.Join(C.BpfFSPath, e.ifName)
if err := os.MkdirAll(e.bpfPath, os.ModePerm); err != nil {
return fmt.Errorf("failed to create bpf fs subpath: %w", err)
}
var objs bpfObjects
if err := loadBpfObjects(&objs, &ebpf.CollectionOptions{
Maps: ebpf.MapOptions{
PinPath: e.bpfPath,
},
}); err != nil {
e.Close()
return fmt.Errorf("loading objects: %w", err)
}
e.objs = &objs
e.originMap = objs.bpfMaps.PairOriginalDstMap
if err := objs.bpfMaps.RedirParamsMap.Update(mapKey1, e.rtIndex, ebpf.UpdateAny); err != nil {
e.Close()
return fmt.Errorf("storing objects: %w", err)
}
if err := objs.bpfMaps.RedirParamsMap.Update(mapKey2, e.redirIp, ebpf.UpdateAny); err != nil {
e.Close()
return fmt.Errorf("storing objects: %w", err)
}
if err := objs.bpfMaps.RedirParamsMap.Update(mapKey3, uint32(e.redirPort), ebpf.UpdateAny); err != nil {
e.Close()
return fmt.Errorf("storing objects: %w", err)
}
attrs := netlink.QdiscAttrs{
LinkIndex: e.ifIndex,
Handle: netlink.MakeHandle(0xffff, 0),
Parent: netlink.HANDLE_CLSACT,
}
qdisc := &netlink.GenericQdisc{
QdiscAttrs: attrs,
QdiscType: "clsact",
}
e.qdisc = qdisc
if err := netlink.QdiscAdd(qdisc); err != nil {
if os.IsExist(err) {
_ = netlink.QdiscDel(qdisc)
err = netlink.QdiscAdd(qdisc)
}
if err != nil {
e.Close()
return fmt.Errorf("cannot add clsact qdisc: %w", err)
}
}
filterAttrs := netlink.FilterAttrs{
LinkIndex: e.ifIndex,
Parent: netlink.HANDLE_MIN_INGRESS,
Handle: netlink.MakeHandle(0, 1),
Protocol: unix.ETH_P_IP,
Priority: 0,
}
filter := &netlink.BpfFilter{
FilterAttrs: filterAttrs,
Fd: objs.bpfPrograms.TcRedirIngressFunc.FD(),
Name: "clash-redir-ingress-" + e.ifName,
DirectAction: true,
}
if err := netlink.FilterAdd(filter); err != nil {
e.Close()
return fmt.Errorf("cannot attach ebpf object to filter ingress: %w", err)
}
e.filter = filter
filterAttrsEgress := netlink.FilterAttrs{
LinkIndex: e.ifIndex,
Parent: netlink.HANDLE_MIN_EGRESS,
Handle: netlink.MakeHandle(0, 1),
Protocol: unix.ETH_P_IP,
Priority: 0,
}
filterEgress := &netlink.BpfFilter{
FilterAttrs: filterAttrsEgress,
Fd: objs.bpfPrograms.TcRedirEgressFunc.FD(),
Name: "clash-redir-egress-" + e.ifName,
DirectAction: true,
}
if err := netlink.FilterAdd(filterEgress); err != nil {
e.Close()
return fmt.Errorf("cannot attach ebpf object to filter egress: %w", err)
}
e.filterEgress = filterEgress
return nil
}
func (e *EBpfRedirect) Close() {
if e.filter != nil {
_ = netlink.FilterDel(e.filter)
}
if e.filterEgress != nil {
_ = netlink.FilterDel(e.filterEgress)
}
if e.qdisc != nil {
_ = netlink.QdiscDel(e.qdisc)
}
if e.objs != nil {
_ = e.objs.Close()
}
_ = os.Remove(filepath.Join(e.bpfPath, "redir_params_map"))
_ = os.Remove(filepath.Join(e.bpfPath, "pair_original_dst_map"))
}
func (e *EBpfRedirect) Lookup(srcAddrPort netip.AddrPort) (socks5.Addr, error) {
rAddr := srcAddrPort.Addr().Unmap()
if rAddr.Is6() {
return nil, fmt.Errorf("remote address is ipv6")
}
srcIp := binary.BigEndian.Uint32(rAddr.AsSlice())
scrPort := srcAddrPort.Port()
key := bpfRedirInfo{
Sip: byteorder.HostToNetwork32(srcIp),
Sport: byteorder.HostToNetwork16(scrPort),
Dip: byteorder.HostToNetwork32(e.redirIp),
Dport: byteorder.HostToNetwork16(e.redirPort),
}
origin := bpfOriginInfo{}
err := e.originMap.Lookup(key, &origin)
if err != nil {
return nil, err
}
addr := make([]byte, net.IPv4len+3)
addr[0] = socks5.AtypIPv4
binary.BigEndian.PutUint32(addr[1:1+net.IPv4len], byteorder.NetworkToHost32(origin.Ip)) // big end
binary.BigEndian.PutUint16(addr[1+net.IPv4len:3+net.IPv4len], byteorder.NetworkToHost16(origin.Port)) // big end
return addr, nil
}

View File

@@ -1,138 +0,0 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
package redir
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
type bpfOriginInfo struct {
Ip uint32
Port uint16
Pad uint16
}
type bpfRedirInfo struct {
Sip uint32
Dip uint32
Sport uint16
Dport uint16
}
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
TcRedirEgressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_egress_func"`
TcRedirIngressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_ingress_func"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
PairOriginalDstMap *ebpf.MapSpec `ebpf:"pair_original_dst_map"`
RedirParamsMap *ebpf.MapSpec `ebpf:"redir_params_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
PairOriginalDstMap *ebpf.Map `ebpf:"pair_original_dst_map"`
RedirParamsMap *ebpf.Map `ebpf:"redir_params_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.PairOriginalDstMap,
m.RedirParamsMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
TcRedirEgressFunc *ebpf.Program `ebpf:"tc_redir_egress_func"`
TcRedirIngressFunc *ebpf.Program `ebpf:"tc_redir_ingress_func"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.TcRedirEgressFunc,
p.TcRedirIngressFunc,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//go:embed bpf_bpfeb.o
var _BpfBytes []byte

Binary file not shown.

View File

@@ -1,138 +0,0 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
package redir
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
type bpfOriginInfo struct {
Ip uint32
Port uint16
Pad uint16
}
type bpfRedirInfo struct {
Sip uint32
Dip uint32
Sport uint16
Dport uint16
}
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
TcRedirEgressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_egress_func"`
TcRedirIngressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_ingress_func"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
PairOriginalDstMap *ebpf.MapSpec `ebpf:"pair_original_dst_map"`
RedirParamsMap *ebpf.MapSpec `ebpf:"redir_params_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
PairOriginalDstMap *ebpf.Map `ebpf:"pair_original_dst_map"`
RedirParamsMap *ebpf.Map `ebpf:"redir_params_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.PairOriginalDstMap,
m.RedirParamsMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
TcRedirEgressFunc *ebpf.Program `ebpf:"tc_redir_egress_func"`
TcRedirIngressFunc *ebpf.Program `ebpf:"tc_redir_ingress_func"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.TcRedirEgressFunc,
p.TcRedirIngressFunc,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//go:embed bpf_bpfel.o
var _BpfBytes []byte

Binary file not shown.

View File

@@ -1,119 +0,0 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
package tc
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
TcTunFunc *ebpf.ProgramSpec `ebpf:"tc_tun_func"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
TcParamsMap *ebpf.MapSpec `ebpf:"tc_params_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
TcParamsMap *ebpf.Map `ebpf:"tc_params_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.TcParamsMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
TcTunFunc *ebpf.Program `ebpf:"tc_tun_func"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.TcTunFunc,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//go:embed bpf_bpfeb.o
var _BpfBytes []byte

Binary file not shown.

View File

@@ -1,119 +0,0 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
package tc
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
TcTunFunc *ebpf.ProgramSpec `ebpf:"tc_tun_func"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
TcParamsMap *ebpf.MapSpec `ebpf:"tc_params_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
TcParamsMap *ebpf.Map `ebpf:"tc_params_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.TcParamsMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
TcTunFunc *ebpf.Program `ebpf:"tc_tun_func"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.TcTunFunc,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//go:embed bpf_bpfel.o
var _BpfBytes []byte

Binary file not shown.

View File

@@ -1,147 +0,0 @@
//go:build linux
package tc
import (
"fmt"
"io"
"net/netip"
"os"
"path/filepath"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/rlimit"
"github.com/sagernet/netlink"
"golang.org/x/sys/unix"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf ../bpf/tc.c
const (
mapKey1 uint32 = 0
mapKey2 uint32 = 1
)
type EBpfTC struct {
objs io.Closer
qdisc netlink.Qdisc
filter netlink.Filter
ifName string
ifIndex int
ifMark uint32
tunIfIndex uint32
bpfPath string
}
func NewEBpfTc(ifName string, ifIndex int, ifMark uint32, tunIfIndex uint32) *EBpfTC {
return &EBpfTC{
ifName: ifName,
ifIndex: ifIndex,
ifMark: ifMark,
tunIfIndex: tunIfIndex,
}
}
func (e *EBpfTC) Start() error {
if err := rlimit.RemoveMemlock(); err != nil {
return fmt.Errorf("remove memory lock: %w", err)
}
e.bpfPath = filepath.Join(C.BpfFSPath, e.ifName)
if err := os.MkdirAll(e.bpfPath, os.ModePerm); err != nil {
return fmt.Errorf("failed to create bpf fs subpath: %w", err)
}
var objs bpfObjects
if err := loadBpfObjects(&objs, &ebpf.CollectionOptions{
Maps: ebpf.MapOptions{
PinPath: e.bpfPath,
},
}); err != nil {
e.Close()
return fmt.Errorf("loading objects: %w", err)
}
e.objs = &objs
if err := objs.bpfMaps.TcParamsMap.Update(mapKey1, e.ifMark, ebpf.UpdateAny); err != nil {
e.Close()
return fmt.Errorf("storing objects: %w", err)
}
if err := objs.bpfMaps.TcParamsMap.Update(mapKey2, e.tunIfIndex, ebpf.UpdateAny); err != nil {
e.Close()
return fmt.Errorf("storing objects: %w", err)
}
attrs := netlink.QdiscAttrs{
LinkIndex: e.ifIndex,
Handle: netlink.MakeHandle(0xffff, 0),
Parent: netlink.HANDLE_CLSACT,
}
qdisc := &netlink.GenericQdisc{
QdiscAttrs: attrs,
QdiscType: "clsact",
}
e.qdisc = qdisc
if err := netlink.QdiscAdd(qdisc); err != nil {
if os.IsExist(err) {
_ = netlink.QdiscDel(qdisc)
err = netlink.QdiscAdd(qdisc)
}
if err != nil {
e.Close()
return fmt.Errorf("cannot add clsact qdisc: %w", err)
}
}
filterAttrs := netlink.FilterAttrs{
LinkIndex: e.ifIndex,
Parent: netlink.HANDLE_MIN_EGRESS,
Handle: netlink.MakeHandle(0, 1),
Protocol: unix.ETH_P_ALL,
Priority: 1,
}
filter := &netlink.BpfFilter{
FilterAttrs: filterAttrs,
Fd: objs.bpfPrograms.TcTunFunc.FD(),
Name: "clash-tc-" + e.ifName,
DirectAction: true,
}
if err := netlink.FilterAdd(filter); err != nil {
e.Close()
return fmt.Errorf("cannot attach ebpf object to filter: %w", err)
}
e.filter = filter
return nil
}
func (e *EBpfTC) Close() {
if e.filter != nil {
_ = netlink.FilterDel(e.filter)
}
if e.qdisc != nil {
_ = netlink.QdiscDel(e.qdisc)
}
if e.objs != nil {
_ = e.objs.Close()
}
_ = os.Remove(filepath.Join(e.bpfPath, "tc_params_map"))
}
func (e *EBpfTC) Lookup(_ netip.AddrPort) (socks5.Addr, error) {
return nil, fmt.Errorf("not supported")
}

View File

@@ -73,7 +73,7 @@ func (m *memoryStore) FlushFakeIP() error {
func newMemoryStore(size int) *memoryStore { func newMemoryStore(size int) *memoryStore {
return &memoryStore{ return &memoryStore{
cacheIP: cache.New[string, netip.Addr](cache.WithSize[string, netip.Addr](size)), cacheIP: cache.NewLRUCache[string, netip.Addr](cache.WithSize[string, netip.Addr](size)),
cacheHost: cache.New[netip.Addr, string](cache.WithSize[netip.Addr, string](size)), cacheHost: cache.NewLRUCache[netip.Addr, string](cache.WithSize[netip.Addr, string](size)),
} }
} }

View File

@@ -3,7 +3,6 @@ package fakeip
import ( import (
"errors" "errors"
"net/netip" "net/netip"
"strings"
"sync" "sync"
"github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/common/nnip"
@@ -27,7 +26,7 @@ type store interface {
FlushFakeIP() error FlushFakeIP() error
} }
// Pool is an implementation about fake ip generator without storage // Pool is a implementation about fake ip generator without storage
type Pool struct { type Pool struct {
gateway netip.Addr gateway netip.Addr
first netip.Addr first netip.Addr
@@ -35,7 +34,7 @@ type Pool struct {
offset netip.Addr offset netip.Addr
cycle bool cycle bool
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie[struct{}] host *trie.DomainTrie[bool]
ipnet *netip.Prefix ipnet *netip.Prefix
store store store store
} }
@@ -44,9 +43,6 @@ type Pool struct {
func (p *Pool) Lookup(host string) netip.Addr { func (p *Pool) Lookup(host string) netip.Addr {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() 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 { if ip, exist := p.store.GetByHost(host); exist {
return ip return ip
} }
@@ -154,7 +150,7 @@ func (p *Pool) restoreState() {
type Options struct { type Options struct {
IPNet *netip.Prefix IPNet *netip.Prefix
Host *trie.DomainTrie[struct{}] Host *trie.DomainTrie[bool]
// Size sets the maximum number of entries in memory // Size sets the maximum number of entries in memory
// and does not work if Persistence is true // and does not work if Persistence is true
@@ -170,7 +166,7 @@ func New(options Options) (*Pool, error) {
var ( var (
hostAddr = options.IPNet.Masked().Addr() hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next() gateway = hostAddr.Next()
first = gateway.Next().Next().Next() // default start with 198.18.0.4 first = gateway.Next().Next()
last = nnip.UnMasked(*options.IPNet) last = nnip.UnMasked(*options.IPNet)
) )

View File

@@ -9,7 +9,6 @@ import (
"github.com/Dreamacro/clash/component/profile/cachefile" "github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
) )
@@ -62,16 +61,16 @@ func TestPool_Basic(t *testing.T) {
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last) bar, exist := pool.LookBack(last)
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, exist) assert.True(t, exist)
assert.Equal(t, bar, "bar.com") assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1})) assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1}))
assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15})) assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15}))
assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5}))) assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 4})))
assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6}))) assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5})))
assert.False(t, pool.Exist(netip.MustParseAddr("::1"))) assert.False(t, pool.Exist(netip.MustParseAddr("::1")))
} }
} }
@@ -90,41 +89,20 @@ func TestPool_BasicV6(t *testing.T) {
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last) bar, exist := pool.LookBack(last)
assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8803"))
assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8803"))
assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")) assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.True(t, exist) assert.True(t, exist)
assert.Equal(t, bar, "bar.com") assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801")) assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff")) assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))) assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")))
assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806"))) assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")))
assert.False(t, pool.Exist(netip.MustParseAddr("127.0.0.1"))) assert.False(t, pool.Exist(netip.MustParseAddr("127.0.0.1")))
} }
} }
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) { func TestPool_CycleUsed(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.16/28") ipnet := netip.MustParsePrefix("192.168.0.16/28")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
@@ -137,7 +115,7 @@ func TestPool_CycleUsed(t *testing.T) {
for _, pool := range pools { for _, pool := range pools {
foo := pool.Lookup("foo.com") foo := pool.Lookup("foo.com")
bar := pool.Lookup("bar.com") bar := pool.Lookup("bar.com")
for i := 0; i < 9; i++ { for i := 0; i < 10; i++ {
pool.Lookup(fmt.Sprintf("%d.com", i)) pool.Lookup(fmt.Sprintf("%d.com", i))
} }
baz := pool.Lookup("baz.com") baz := pool.Lookup("baz.com")
@@ -149,8 +127,8 @@ func TestPool_CycleUsed(t *testing.T) {
func TestPool_Skip(t *testing.T) { func TestPool_Skip(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/29") ipnet := netip.MustParsePrefix("192.168.0.1/29")
tree := trie.New[struct{}]() tree := trie.New[bool]()
tree.Insert("example.com", struct{}{}) tree.Insert("example.com", true)
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: &ipnet, IPNet: &ipnet,
Size: 10, Size: 10,
@@ -219,8 +197,8 @@ func TestPool_Clone(t *testing.T) {
first := pool.Lookup("foo.com") first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
newPool, _ := New(Options{ newPool, _ := New(Options{
IPNet: &ipnet, IPNet: &ipnet,

View File

@@ -3,10 +3,10 @@ package geodata
import ( import (
"errors" "errors"
"fmt" "fmt"
C "github.com/Dreamacro/clash/constant"
"strings" "strings"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
) )

View File

@@ -1,52 +0,0 @@
package geodata
import (
"fmt"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"io"
"net/http"
"os"
)
var initFlag bool
func InitGeoSite() error {
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
log.Infoln("Download GeoSite.dat finish")
}
if !initFlag {
if err := Verify(C.GeositeName); err != nil {
log.Warnln("GeoSite.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
}
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
}
initFlag = true
}
return nil
}
func downloadGeoSite(path string) (err error) {
resp, err := http.Get(C.GeoSiteUrl)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}

View File

@@ -329,7 +329,6 @@ func NewGeoIPMatcher(geoip *GeoIP) (*GeoIPMatcher, error) {
} }
func (m *MultiGeoIPMatcher) ApplyIp(ip net.IP) bool { func (m *MultiGeoIPMatcher) ApplyIp(ip net.IP) bool {
for _, matcher := range m.matchers { for _, matcher := range m.matchers {
if matcher.Match(ip) { if matcher.Match(ip) {
return true return true

View File

@@ -7,10 +7,11 @@
package router package router
import ( import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
) )
const ( const (

View File

@@ -9,7 +9,6 @@ import (
"github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )

View File

@@ -2,6 +2,7 @@ package geodata
import ( import (
"fmt" "fmt"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )

View File

@@ -2,14 +2,15 @@ package http
import ( import (
"context" "context"
"github.com/Dreamacro/clash/component/tls"
"github.com/Dreamacro/clash/listener/inner"
"io" "io"
"net" "net"
"net/http" "net/http"
URL "net/url" URL "net/url"
"strings" "strings"
"time" "time"
"github.com/Dreamacro/clash/listener/inner"
"github.com/Dreamacro/clash/log"
) )
const ( const (
@@ -52,13 +53,12 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
log.Infoln(urlRes.String())
conn := inner.HandleTcp(address, urlRes.Hostname()) conn := inner.HandleTcp(address, urlRes.Hostname())
return conn, nil return conn, nil
}, },
TLSClientConfig: tls.GetDefaultTLSConfig(),
} }
client := http.Client{Transport: transport} client := http.Client{Transport: transport}
return client.Do(req) return client.Do(req)
} }

View File

@@ -1,11 +1,11 @@
package mmdb package mmdb
import ( import (
"github.com/oschwald/geoip2-golang"
"sync" "sync"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/oschwald/geoip2-golang"
) )
var ( var (

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