Compare commits

..
5 Commits
Author SHA1 Message Date
Fangliding fa58459817 Refactor 2026-10-21 13:20:30 +08:00
Fangliding 8d0deebf86 simple clear 2026-10-18 22:30:54 +08:00
Fangliding 0396b34eff test 2026-10-18 22:10:39 +08:00
Roman Rublev 83147cf3a7 SS2022 inbound: Don't end a UDP session when its first source address expires
The relay session context was derived from the inbound connection of the
source address that created the session. After a client changes its source
address (NAT rebinding), the UDP worker of the old address is cleaned up after
~2 minutes of inactivity and cancels that context, which tears down the
dispatched outbound flow; the next packet then creates a brand-new session and
outbound flow (new local port on the server, reset replay window).

Sessions are routed by client session ID (SIP022 3.2.4), so detach the session
context from the originating connection's cancellation and let the session's
own inactivity timer govern its lifetime.
2026-10-18 16:28:31 +03:00
Roman Rublev 8d0a7cf856 SS2022 inbound: Fix data races and duplicate acceptance when decoding one UDP session concurrently
Packets of one client session can be processed by two inbound workers at
the same time (e.g. the client's old and new source address after a NAT
rebinding). The sliding window was checked and updated in two separate
critical sections, and clientBodyCipher and the session's User were read
without the lock, so the same packet ID could be accepted more than once
and go test -race reported data races.

Re-check the window and publish the cached body cipher in one critical
section after the packet is fully validated, and always initialize the
session's User under the lock.
2026-10-18 16:28:31 +03:00
61 changed files with 898 additions and 3353 deletions
+11 -5
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory # Download geodat into a staging directory
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
@@ -40,15 +40,21 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image # Build finally image
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
VOLUME /usr/local/etc/xray VOLUME /usr/local/etc/xray
VOLUME /var/log/xray VOLUME /var/log/xray
+11 -5
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory # Download geodat into a staging directory
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
@@ -40,6 +40,11 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image # Build finally image
# Note on Distroless Base Image and Architecture Support: # Note on Distroless Base Image and Architecture Support:
# - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux: # - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux:
@@ -55,9 +60,10 @@ FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
VOLUME /usr/local/etc/xray VOLUME /usr/local/etc/xray
VOLUME /var/log/xray VOLUME /var/log/xray
@@ -40,7 +40,7 @@ jobs:
max_attempts: 60 max_attempts: 60
command: | command: |
[ -d 'resources' ] || mkdir resources [ -d 'resources' ] || mkdir resources
LIST=('XTLS Xray-rules-dat geoip/geoip-lite geoip' 'XTLS Xray-rules-dat geosite/geosite geosite') LIST=('Loyalsoldier v2ray-rules-dat geoip geoip' 'Loyalsoldier v2ray-rules-dat geosite geosite')
for i in "${LIST[@]}" for i in "${LIST[@]}"
do do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}')) INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}'))
+18 -3
View File
@@ -35,7 +35,24 @@ jobs:
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
run: sleep 90 run: sleep 90
check-format-and-proto: check-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do
if ! cmp -s ref.txt <(head -n 4 "$file"); then
echo "Error: Header mismatch in $file"
head -n 4 "$file"
exit 1
fi
done
check-format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions: permissions:
@@ -51,8 +68,6 @@ jobs:
cache: false cache: false
- name: Check Format - name: Check Format
run: go run ./infra/vformat/main.go -mode check -pwd ./ run: go run ./infra/vformat/main.go -mode check -pwd ./
- name: Check Protobuf
run: go run ./infra/vprotogen/main.go -mode check -pwd ./
test: test:
needs: check-assets needs: check-assets
+4 -4
View File
@@ -128,7 +128,7 @@ func TestDNSScriptGeoIPFallback(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
script := ` script := `
local servers = require("xray.dns").Servers local servers = require("xray.dns").Servers
local ru_ips = require("xray.geodata").BuildIPMatcher("geoip:ru") local us_ips = require("xray.geodata").BuildIPMatcher("geoip:us")
local by_id = {} local by_id = {}
for _, server in ipairs(servers) do for _, server in ipairs(servers) do
@@ -138,7 +138,7 @@ assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are r
function HandleDNSQuery(domain, ipv4, ipv6, fake) function HandleDNSQuery(domain, ipv4, ipv6, fake)
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake) local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
if not err and ru_ips:AnyMatch(ips) then if not err and us_ips:AnyMatch(ips) then
return ips, ttl, nil return ips, ttl, nil
end end
return by_id.fallback:Query(domain, ipv4, ipv6, fake) return by_id.fallback:Query(domain, ipv4, ipv6, fake)
@@ -152,7 +152,7 @@ end
primary := &scriptNameServer{ primary := &scriptNameServer{
name: "primary", name: "primary",
answers: map[string]net.IP{ answers: map[string]net.IP{
"ru.example": net.ParseIP("2a02:6b8::feed:0ff"), "us.example": net.ParseIP("2001:4860:4860::8888"),
"other.example": net.ParseIP("127.0.0.1"), "other.example": net.ParseIP("127.0.0.1"),
}, },
ttl: 30, ttl: 30,
@@ -191,7 +191,7 @@ end
ip net.IP ip net.IP
ttl uint32 ttl uint32
}{ }{
{"Ru.Example.", net.ParseIP("2a02:6b8::feed:0ff"), 30}, {"Us.Example.", net.ParseIP("2001:4860:4860::8888"), 30},
{"other.example", net.ParseIP("9.9.9.9"), 60}, {"other.example", net.ParseIP("9.9.9.9"), 60},
} { } {
ips, ttl, err := server.LookupIP(tc.domain, option) ips, ttl, err := server.LookupIP(tc.domain, option)
-1
View File
@@ -46,7 +46,6 @@ message RoutingContext {
// - attributes: Select connection's additional attributes. // - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both // - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags. // outbound tag and outbound group tags.
//
// * If FieldSelectors is left empty, all fields will be returned. // * If FieldSelectors is left empty, all fields will be returned.
message SubscribeRoutingStatsRequest { message SubscribeRoutingStatsRequest {
repeated string FieldSelectors = 1; repeated string FieldSelectors = 1;
+1 -1
View File
@@ -415,7 +415,7 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
func BenchmarkMultiGeoIPMatcher(b *testing.B) { func BenchmarkMultiGeoIPMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:ir", "geoip:ru"}) rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
common.Must(err) common.Must(err)
matcher, err := NewIPMatcher(rules, MatcherAsType_Target) matcher, err := NewIPMatcher(rules, MatcherAsType_Target)
+7 -7
View File
@@ -400,13 +400,13 @@ func TestIPMatcher4CN(t *testing.T) {
} }
} }
func TestIPMatcher6RU(t *testing.T) { func TestIPMatcher6US(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:ru") matcher := buildIPMatcher("geoip:us")
if !matcher.Match(xnet.ParseAddress("2a02:6b8::feed:0ff").IP()) { if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect RU geoip contain 2a02:6b8::feed:0ff, but actually not") t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
} }
} }
@@ -423,11 +423,11 @@ func BenchmarkIPMatcher4CN(b *testing.B) {
} }
} }
func BenchmarkIPMatcher6RU(b *testing.B) { func BenchmarkIPMatcher6US(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:ru") matcher := buildIPMatcher("geoip:us")
ip := xnet.ParseAddress("2a02:6b8::feed:0ff").IP() ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
b.ResetTimer() b.ResetTimer()
+3 -3
View File
@@ -11,7 +11,7 @@ func TestParseIPRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{ rules := []string{
"geoip:ru", "geoip:us",
"geoip:cn", "geoip:cn",
"!geoip:cn", "!geoip:cn",
"!!geoip:cn", "!!geoip:cn",
@@ -20,9 +20,9 @@ func TestParseIPRules(t *testing.T) {
"!geoip:!cn", "!geoip:!cn",
"ext:geoip.dat:!cn", "ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn", "ext:geoip.dat:!!cn",
"ext:geoip.dat:ir", "ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn", "ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ir", "ext-ip:geoip.dat:!ca",
"192.168.0.0/24", "192.168.0.0/24",
"!192.168.0.0/24", "!192.168.0.0/24",
"!!192.168.0.0/24", "!!192.168.0.0/24",
+16 -33
View File
@@ -26,20 +26,20 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
var procFiles []string var procFile string
switch network { switch network {
case "tcp": case "tcp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFiles = []string{"/proc/net/tcp"} procFile = "/proc/net/tcp"
} else { } else {
procFiles = []string{"/proc/net/tcp6"} procFile = "/proc/net/tcp6"
} }
case "udp": case "udp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFiles = []string{"/proc/net/udp", "/proc/net/udp6"} procFile = "/proc/net/udp"
} else { } else {
procFiles = []string{"/proc/net/udp6", "/proc/net/udp"} procFile = "/proc/net/udp6"
} }
default: default:
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
@@ -50,17 +50,12 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return 0, "", "", errors.New("failed to format address: ", err) return 0, "", "", errors.New("failed to format address: ", err)
} }
portFallback := "" inode, err := findInodeInFile(procFile, targetHexAddr)
if network == "udp" {
portFallback = fmt.Sprintf(":%04X", srcPort)
}
inode, err := findInodeInFiles(procFiles, targetHexAddr, portFallback)
if err != nil { if err != nil {
return 0, "", "", errors.New("could not search in ", strings.Join(procFiles, " and ")).Base(err) return 0, "", "", errors.New("could not search in ", procFile).Base(err)
} }
if inode == "" { if inode == "" {
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", strings.Join(procFiles, " or ")) return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile)
} }
pidStr, err := findPidByInode(inode) pidStr, err := findPidByInode(inode)
@@ -107,43 +102,31 @@ func formatLittleEndianString(addr net.IP, port Port) (string, error) {
return fmt.Sprintf("%s:%s", ipHex, portHex), nil return fmt.Sprintf("%s:%s", ipHex, portHex), nil
} }
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) { func findInodeInFile(filePath, targetHexAddr string) (string, error) {
onlyMatchedPort := ""
for i, filePath := range filePaths {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if i > 0 && os.IsNotExist(err) {
continue
}
return "", err return "", err
} }
defer file.Close() defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
fields := strings.Fields(scanner.Text()) line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 10 { if len(fields) < 10 {
continue continue
} }
localAddress := fields[1] localAddress := fields[1]
if localAddress == targetHexAddr { if localAddress == targetHexAddr {
return fields[9], nil inode := fields[9]
} return inode, nil
if portFallback != "" && strings.HasSuffix(localAddress, portFallback) {
wildcardIP := strings.TrimSuffix(localAddress, portFallback)
if (len(wildcardIP) == 8 || len(wildcardIP) == 32) && strings.Trim(wildcardIP, "0") == "" {
onlyMatchedPort = fields[9]
}
}
}
if err := scanner.Err(); err != nil {
return "", err
} }
} }
return onlyMatchedPort, nil return "", scanner.Err()
} }
func findPidByInode(inode string) (string, error) { func findPidByInode(inode string) (string, error) {
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 10 Version_y byte = 10
Version_z byte = 21 Version_z byte = 17
) )
var ( var (
+2 -2
View File
@@ -1,5 +1,5 @@
package core package core
//go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 //go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
//go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.0 //go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
//go:generate go run ../infra/vprotogen/main.go -pwd ./.. //go:generate go run ../infra/vprotogen/main.go -pwd ./..
+3 -10
View File
@@ -4,13 +4,12 @@ go 1.27
require ( require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/bufbuild/protocompile v0.14.1
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.1 github.com/klauspost/cpuid/v2 v2.4.0
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
github.com/miekg/dns v1.1.73 github.com/miekg/dns v1.1.73
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
@@ -24,7 +23,7 @@ require (
github.com/yuin/gopher-lua v1.1.2 github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.61.0 golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.24.0
@@ -33,7 +32,7 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1 golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0 google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.12
h12.io/socks v1.0.3 h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11 layeh.com/gopher-luar v1.0.11
lukechampine.com/blake3 v1.4.1 lukechampine.com/blake3 v1.4.1
@@ -59,12 +58,6 @@ require (
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.43.0 // indirect
golang.org/x/tools v0.51.0 // indirect golang.org/x/tools v0.51.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
) )
tool (
google.golang.org/grpc/cmd/protoc-gen-go-grpc
google.golang.org/protobuf/cmd/protoc-gen-go
)
+4 -10
View File
@@ -2,8 +2,6 @@ github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -35,8 +33,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -99,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a h1:V5oicZXS0oqZx30gY/apgSVD7YvNBfrIOdcMnlelJY4= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -154,10 +152,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-21
View File
@@ -15,11 +15,9 @@ import (
googleuuid "github.com/google/uuid" googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua" xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment" "github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -76,7 +74,6 @@ var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomTCP) }, "header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) }, "fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) }, "sudoku": func() interface{} { return new(Sudoku) },
@@ -84,7 +81,6 @@ var (
}, "type", "settings") }, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomUDP) }, "header-custom": func() interface{} { return new(HeaderCustomUDP) },
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) }, "mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
"noise": func() interface{} { return new(NoiseMask) }, "noise": func() interface{} { return new(NoiseMask) },
@@ -97,23 +93,6 @@ var (
}, "type", "settings") }, "type", "settings")
) )
type Filter struct {
BlockedSourceIPs []string `json:"blockedSourceIPs"`
AllowedSourceIPs []string `json:"allowedSourceIPs"`
}
func (c *Filter) Build() (proto.Message, error) {
rules, err := geodata.ParseIPRules(c.BlockedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "blockedSourceIPs"`).Base(err)
}
allowed, err := geodata.ParseIPRules(c.AllowedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "allowedSourceIPs"`).Base(err)
}
return &filter.Config{BlockedSourceIps: rules, AllowedSourceIps: allowed}, nil
}
type TCPItem struct { type TCPItem struct {
Delay Int32Range `json:"delay"` Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"` Rand int32 `json:"rand"`
@@ -1,79 +0,0 @@
package conf_test
import (
"encoding/json"
"fmt"
"net"
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
)
func TestFinalMaskFilter(t *testing.T) {
for _, network := range []string{"tcp", "udp"} {
for _, test := range []struct {
name, settings string
blocked, allowed int
}{
{"blacklist", `{"blockedSourceIPs":["192.0.2.0/24"]}`, 1, 0},
{"whitelist", `{"allowedSourceIPs":["192.0.2.0/24"]}`, 0, 1},
{"both", `{"blockedSourceIPs":["192.0.2.0/24"],"allowedSourceIPs":["192.0.2.0/24"]}`, 1, 1},
{"empty_whitelist", `{"allowedSourceIPs":[]}`, 0, 0},
{"empty_blacklist", `{"blockedSourceIPs":[]}`, 0, 0},
} {
t.Run(network+"/"+test.name, func(t *testing.T) {
input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"filter","settings":%s}]}}`, network, test.settings)
var config StreamConfig
if err := json.Unmarshal([]byte(input), &config); err != nil {
t.Fatal(err)
}
built, err := config.Build()
if err != nil {
t.Fatal(err)
}
masks := built.Tcpmasks
if network == "udp" {
masks = built.Udpmasks
}
if len(masks) != 1 {
t.Fatalf("expected one mask, got %d", len(masks))
}
instance, err := masks[0].GetInstance()
if err != nil {
t.Fatal(err)
}
filter, ok := instance.(*filter.Config)
if !ok {
t.Fatalf("unexpected mask type: %T", instance)
}
if len(filter.BlockedSourceIps) != test.blocked || len(filter.AllowedSourceIps) != test.allowed {
t.Fatalf("unexpected filter policy: %v", filter)
}
for _, rules := range [][]*geodata.IPRule{filter.BlockedSourceIps, filter.AllowedSourceIps} {
for _, rule := range rules {
cidr := rule.GetCustom().GetCidr()
if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) {
t.Fatalf("unexpected IP rule: %v", cidr)
}
}
}
if _, err := internet.ToMemoryStreamConfig(built); err != nil {
t.Fatalf("failed to initialize runtime finalmask: %v", err)
}
})
}
}
for _, field := range []string{"blockedSourceIPs", "allowedSourceIPs"} {
var config Filter
if err := json.Unmarshal([]byte(fmt.Sprintf(`{"%s":["192.0.2.0/33"]}`, field)), &config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), field) {
t.Errorf("expected invalid %s, got %v", field, err)
}
}
}
-18
View File
@@ -90,7 +90,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config.ProtocolName = protocol config.ProtocolName = protocol
} }
var xdriveH3 bool
switch strings.ToLower(c.Security) { switch strings.ToLower(c.Security) {
case "", "none": case "", "none":
case "tls": case "tls":
@@ -105,7 +104,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
tm := serial.ToTypedMessage(ts) tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm) config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type config.SecurityType = tm.Type
xdriveH3 = config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"h3"})
if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" { if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" {
// a fingerprint puts its own ALPN into the hello, except Go TLS and the // a fingerprint puts its own ALPN into the hello, except Go TLS and the
// randomized ones, which take it from the config or send none // randomized ones, which take it from the config or send none
@@ -221,7 +219,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}) })
} }
if c.XDRIVESettings != nil { if c.XDRIVESettings != nil {
c.XDRIVESettings.h3 = xdriveH3
xs, err := c.XDRIVESettings.Build() xs, err := c.XDRIVESettings.Build()
if err != nil { if err != nil {
return nil, errors.New("Failed to build XDRIVE config.").Base(err) return nil, errors.New("Failed to build XDRIVE config.").Base(err)
@@ -263,20 +260,5 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
} }
} }
if xdriveH3 && c.FinalMask != nil {
if c.XDRIVESettings != nil && c.XDRIVESettings.Service == "google-drive" {
// noise only sends datagrams of its own ahead of the QUIC ones, the
// other masks change those or need a peer that undoes them
for _, mask := range c.FinalMask.Udp {
if strings.ToLower(mask.Type) != "noise" {
return nil, errors.New(`XDRIVE: over h3 to Google Drive the only "udp" mask that works is "noise", not "`, mask.Type, `"`)
}
}
}
if config.QuicParams != nil && (config.QuicParams.Congestion == "brutal" || config.QuicParams.Congestion == "force-brutal") {
return nil, errors.New(`XDRIVE: brutal is for Hysteria servers, over h3 "congestion" is "bbr" or "reno", not "`, config.QuicParams.Congestion, `"`)
}
}
return config, nil return config, nil
} }
-27
View File
@@ -997,10 +997,6 @@ type XDriveConfig struct {
EagerWindowMs uint32 `json:"eagerWindowMs"` EagerWindowMs uint32 `json:"eagerWindowMs"`
HoleTimeoutMs uint32 `json:"holeTimeoutMs"` HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
Template json.RawMessage `json:"template"` Template json.RawMessage `json:"template"`
Xmux XmuxConfig `json:"xmux"`
// set by StreamConfig when "alpn" is ["h3"]
h3 bool
} }
// Build implements Buildable. // Build implements Buildable.
@@ -1022,21 +1018,6 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
default: default:
return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`) return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`)
} }
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 1
c.Xmux.MaxConnections.To = 1
if c.Service == "google-drive" && !c.h3 {
// Google gives an h2 connection 1 MiB of window, which a few sessions on
// a long rtt would share, an h3 connection has no such limit
c.Xmux.MaxConnections.From = 4
c.Xmux.MaxConnections.To = 4
}
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
}
config := &xdrive.Config{ config := &xdrive.Config{
RemoteFolder: c.RemoteFolder, RemoteFolder: c.RemoteFolder,
Service: c.Service, Service: c.Service,
@@ -1050,14 +1031,6 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
EagerWindowMs: c.EagerWindowMs, EagerWindowMs: c.EagerWindowMs,
HoleTimeoutMs: c.HoleTimeoutMs, HoleTimeoutMs: c.HoleTimeoutMs,
Template: string(c.Template), Template: string(c.Template),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
},
} }
return config, nil return config, nil
} }
-123
View File
@@ -9,7 +9,6 @@ import (
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/xdrive" "github.com/xtls/xray-core/transport/internet/xdrive"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -425,81 +424,6 @@ func TestXDriveHTTP1ALPNWarning(t *testing.T) {
} }
} }
// over h3 Google Drive takes only the noise mask, and no service takes brutal
func TestXDriveH3Checks(t *testing.T) {
const (
drive = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]}`
local = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}`
noise = `{"type": "Noise", "settings": {"noise": [{"rand": "50-100"}]}}`
salamander = `{"type": "salamander", "settings": {"password": "p"}}`
forceBrutal = `{"quicParams": {"congestion": "force-brutal", "brutalUp": "10 mbps"}}`
)
for _, c := range []struct {
alpn, transport, finalmask string
ok bool
}{
{`["h3"]`, drive, `{"udp": [` + noise + `]}`, true},
{`["h3"]`, drive, `{"udp": [` + salamander + `]}`, false},
{`["h3"]`, drive, `{"udp": [` + noise + `, ` + salamander + `]}`, false},
{`["h3"]`, local, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, `"method": "xdrive"`, `{"udp": [` + salamander + `]}`, true},
{`["h2"]`, drive, `{"udp": [` + salamander + `]}`, true},
{`null`, drive, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, drive, `null`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "bbr"}}`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, false},
{`["h3"]`, local, forceBrutal, false},
{`["h3", "h2"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, true},
{`["h3"]`, `"method": "xhttp"`, forceBrutal, true},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
`+c.transport+`,
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`},
"finalmask": `+c.finalmask+`
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); (err == nil) != c.ok {
t.Errorf("alpn %s, %s, finalmask %s: %v", c.alpn, c.transport, c.finalmask, err)
}
}
}
// one h3 connection is not held back by a window like h2, so h3 takes one by default
func TestXDriveH3Connections(t *testing.T) {
for _, c := range []struct {
alpn string
want int32
}{
{`["h3"]`, 1},
{`["h2"]`, 4},
{`null`, 4},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
"method": "xdrive",
"xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]},
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`}
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
xs, err := built.TransportSettings[0].Settings.GetInstance()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
if got := xs.(*xdrive.Config).Xmux.MaxConnections; got.From != c.want || got.To != c.want {
t.Errorf("alpn %s: maxConnections is %d-%d, want %d", c.alpn, got.From, got.To, c.want)
}
}
}
func TestXDriveTemplateNeedsTemplate(t *testing.T) { func TestXDriveTemplateNeedsTemplate(t *testing.T) {
config := new(XDriveConfig) config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
@@ -589,50 +513,3 @@ func TestXDriveInboundConfig(t *testing.T) {
} }
} }
} }
func TestXDriveXmux(t *testing.T) {
defaults := func(connections int32) *splithttp.XmuxConfig {
return &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{},
MaxConnections: &splithttp.RangeConfig{From: connections, To: connections},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{From: 1800, To: 3000},
}
}
for _, c := range []struct {
input string
want *splithttp.XmuxConfig
}{
{`{"service": "google-drive", "secrets": ["a", "b", "c"]}`, defaults(4)},
{`{"service": "template", "template": {}}`, defaults(1)},
{`{"service": "google-drive", "secrets": ["a", "b", "c"], "xmux": {"maxConcurrency": "1-2", "hMaxRequestTimes": 100, "hKeepAlivePeriod": -1}}`, &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 2},
MaxConnections: &splithttp.RangeConfig{},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{From: 100, To: 100},
HMaxReusableSecs: &splithttp.RangeConfig{},
HKeepAlivePeriod: -1,
}},
} {
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(c.input), config); err != nil {
t.Fatalf("%s: Unmarshal: %v", c.input, err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("%s: Build: %v", c.input, err)
}
if got := built.(*xdrive.Config).Xmux; !proto.Equal(got, c.want) {
t.Errorf("%s: xmux is %v, want %v", c.input, got, c.want)
}
}
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"service": "template", "template": {}, "xmux": {"maxConnections": 2, "maxConcurrency": 8}}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); err == nil {
t.Error("Build accepted maxConnections with maxConcurrency")
}
}
+202 -167
View File
@@ -1,87 +1,149 @@
package main package main
import ( import (
"bytes" "errors"
"context"
"flag" "flag"
"fmt" "fmt"
"io/fs" "go/build"
"io"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"runtime"
"strconv"
"strings" "strings"
"github.com/bufbuild/protocompile"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/pluginpb"
) )
var ( var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
mode = flag.String("mode", "generate", "Execution mode. 'generate' (default) compiles and saves changes. 'check' reports out-of-date files and exits with non-zero.")
)
// topoSort sorts file descriptors so each dependency appears before files importing it. // envFile returns the name of the Go environment configuration file.
func topoSort(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto { // Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
fileMap := make(map[string]protoreflect.FileDescriptor) func envFile() (string, error) {
for _, f := range files { if file := os.Getenv("GOENV"); file != "" {
fileMap[f.Path()] = f if file == "off" {
return "", errors.New("GOENV=off")
} }
return file, nil
visited := make(map[string]bool)
var sorted []*descriptorpb.FileDescriptorProto
var visit func(fd protoreflect.FileDescriptor)
visit = func(fd protoreflect.FileDescriptor) {
if visited[fd.Path()] {
return
} }
visited[fd.Path()] = true dir, err := os.UserConfigDir()
imports := fd.Imports()
for i := 0; i < imports.Len(); i++ {
if dep, ok := fileMap[imports.Get(i).Path()]; ok {
visit(dep)
}
}
sorted = append(sorted, protodesc.ToFileDescriptorProto(fd))
}
for _, f := range files {
visit(f)
}
return sorted
}
func runPlugin(pluginName, pwd string, req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) {
reqBytes, err := proto.Marshal(req)
if err != nil { if err != nil {
return nil, err return "", err
}
if dir == "" {
return "", errors.New("missing user-config dir")
}
return filepath.Join(dir, "go", "env"), nil
} }
cmd := exec.Command("go", "tool", pluginName) // GetRuntimeEnv returns the value of runtime environment variable,
cmd.Dir = pwd // that is set by running following command: `go env -w key=value`.
cmd.Stdin = bytes.NewReader(reqBytes) func GetRuntimeEnv(key string) (string, error) {
cmd.Env = os.Environ() file, err := envFile()
var stdout, stderr bytes.Buffer if err != nil {
cmd.Stdout = &stdout return "", err
cmd.Stderr = &stderr }
if file == "" {
if err := cmd.Run(); err != nil { return "", errors.New("missing runtime env file")
return nil, fmt.Errorf("%v: %s", err, stderr.String()) }
var data []byte
var runtimeEnv string
data, readErr := os.ReadFile(file)
if readErr != nil {
return "", readErr
}
envStrings := strings.Split(string(data), "\n")
for _, envItem := range envStrings {
envItem = strings.TrimSuffix(envItem, "\r")
envKeyValue := strings.Split(envItem, "=")
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
runtimeEnv = strings.TrimSpace(envKeyValue[1])
}
}
return runtimeEnv, nil
} }
var resp pluginpb.CodeGeneratorResponse // GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil { func GetGOBIN() string {
return nil, fmt.Errorf("failed to unmarshal response: %w", err) // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
GOBIN := os.Getenv("GOBIN")
if GOBIN == "" {
var err error
// The one set by user by running `go env -w GOBIN=/path`
GOBIN, err = GetRuntimeEnv("GOBIN")
if err != nil {
// The default one that Golang uses
return filepath.Join(build.Default.GOPATH, "bin")
} }
if resp.GetError() != "" { if GOBIN == "" {
return nil, fmt.Errorf("%s", resp.GetError()) return filepath.Join(build.Default.GOPATH, "bin")
} }
return &resp, nil return GOBIN
}
return GOBIN
}
func whichProtoc(suffix, targetedVersion string) (string, error) {
protoc := "protoc" + suffix
path, err := exec.LookPath(protoc)
if err != nil {
return "", fmt.Errorf(`
Command "%s" not found.
Make sure that %s is in your system path or current path.
Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases
`, protoc, protoc, protoc, targetedVersion)
}
return path, nil
}
func getProjectProtocVersion(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", errors.New("can not get the version of protobuf used in xray project")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("can not read from body")
}
versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v\d+\.(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(body))
return matched[1], nil
}
func getInstalledProtocVersion(protocPath string) (string, error) {
cmd := exec.Command(protocPath, "--version")
cmd.Env = append(cmd.Env, os.Environ()...)
output, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return "", cmdErr
}
versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(output))
return matched[1], nil
}
func parseVersion(s string, width int) int64 {
strList := strings.Split(s, ".")
format := fmt.Sprintf("%%s%%0%ds", width)
v := ""
for _, value := range strList {
v = fmt.Sprintf(format, v, value)
}
var result int64
var err error
if result, err = strconv.ParseInt(v, 10, 64); err != nil {
return 0
}
return result
}
func needToUpdate(targetedVersion, installedVersion string) bool {
vt := parseVersion(targetedVersion, 4)
vi := parseVersion(installedVersion, 4)
return vt > vi
} }
func main() { func main() {
@@ -92,133 +154,106 @@ func main() {
flag.Parse() flag.Parse()
if !filepath.IsAbs(*directory) { if !filepath.IsAbs(*directory) {
pwd, err := os.Getwd() pwd, wdErr := os.Getwd()
if err != nil { if wdErr != nil {
fmt.Println("Can not get current working directory.") fmt.Println("Can not get current working directory.")
os.Exit(1) os.Exit(1)
} }
*directory = filepath.Join(pwd, *directory) *directory = filepath.Join(pwd, *directory)
} }
pwd := *directory
isCheck := false pwd := *directory
switch *mode { GOBIN := GetGOBIN()
case "generate", "": binPath := os.Getenv("PATH")
isCheck = false pathSlice := []string{pwd, GOBIN, binPath}
case "check": binPath = strings.Join(pathSlice, string(os.PathListSeparator))
isCheck = true os.Setenv("PATH", binPath)
default:
fmt.Fprintf(os.Stderr, "Unrecognized 'mode': %s. Use 'generate' or 'check'.\n", *mode) suffix := ""
if runtime.GOOS == "windows" {
suffix = ".exe"
}
/*
targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
*/
targetedVersion := ""
protoc, err := whichProtoc(suffix, targetedVersion)
if err != nil {
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
if isCheck { installedVersion, err := getInstalledProtocVersion(protoc)
fmt.Println("Checking for out-of-date Protobuf generated files...") if err != nil {
fmt.Println(err)
os.Exit(1)
} }
var protoFiles []string if needToUpdate(targetedVersion, installedVersion) {
err := filepath.WalkDir(pwd, func(path string, d fs.DirEntry, err error) error { fmt.Printf(`
if err != nil || d.IsDir() { You are using an old protobuf version, please update to v%s or later.
Download it from https://github.com/protocolbuffers/protobuf/releases
* Protobuf version used in xray project: v%s
* Protobuf version you have installed: v%s
`, targetedVersion, targetedVersion, installedVersion)
os.Exit(1)
}
protoFilesMap := make(map[string][]string)
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println(err)
return err return err
} }
if strings.HasSuffix(d.Name(), ".proto") {
rel, _ := filepath.Rel(pwd, path) if info.IsDir() {
protoFiles = append(protoFiles, filepath.ToSlash(rel)) return nil
} }
dir := filepath.Dir(path)
filename := filepath.Base(path)
if strings.HasSuffix(filename, ".proto") {
path = path[len(pwd)+1:]
protoFilesMap[dir] = append(protoFilesMap[dir], path)
}
return nil return nil
}) })
if err != nil { if walkErr != nil {
fmt.Println("Walk error:", err) fmt.Println(walkErr)
os.Exit(1)
}
if len(protoFiles) == 0 {
return
}
compiler := protocompile.Compiler{
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{pwd},
}),
SourceInfoMode: protocompile.SourceInfoStandard,
}
linkedFiles, err := compiler.Compile(context.Background(), protoFiles...)
if err != nil {
fmt.Fprintf(os.Stderr, "Protobuf compilation failed:\n%v\n", err)
os.Exit(1) os.Exit(1)
} }
var fdList []protoreflect.FileDescriptor for _, files := range protoFilesMap {
for _, f := range linkedFiles { for _, relProtoFile := range files {
fdList = append(fdList, f) args := []string{
"--go_out", pwd,
"--go_opt", "paths=source_relative",
"--go-grpc_out", pwd,
"--go-grpc_opt", "paths=source_relative",
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
} }
args = append(args, relProtoFile)
req := &pluginpb.CodeGeneratorRequest{ cmd := exec.Command(protoc, args...)
FileToGenerate: protoFiles, cmd.Env = append(cmd.Env, os.Environ()...)
Parameter: proto.String("paths=source_relative"), cmd.Dir = pwd
ProtoFile: topoSort(fdList), output, cmdErr := cmd.CombinedOutput()
// TODO: replace with unknown in the future if len(output) > 0 {
CompilerVersion: &pluginpb.Version{ fmt.Println(string(output))
Major: proto.Int32(6),
Minor: proto.Int32(33),
Patch: proto.Int32(5),
},
} }
if cmdErr != nil {
var diffCount int fmt.Println(cmdErr)
for _, plugin := range []string{"protoc-gen-go", "protoc-gen-go-grpc"} {
resp, err := runPlugin(plugin, pwd, req)
if err != nil {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", plugin, err)
os.Exit(1)
}
for _, f := range resp.File {
targetPath := filepath.Join(pwd, filepath.FromSlash(f.GetName()))
content := []byte(f.GetContent())
hasDiff := false
existing, err := os.ReadFile(targetPath)
if err != nil {
hasDiff = true
} else {
oldNorm := bytes.ReplaceAll(existing, []byte("\r\n"), []byte("\n"))
newNorm := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
if !bytes.Equal(oldNorm, newNorm) {
hasDiff = true
}
}
if hasDiff {
diffCount++
fmt.Println(targetPath)
if isCheck {
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
os.Exit(1)
}
mode := os.FileMode(0o644)
if stat, err := os.Stat(targetPath); err == nil {
mode = stat.Mode().Perm()
}
if err := os.WriteFile(targetPath, content, mode); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", targetPath, err)
os.Exit(1) os.Exit(1)
} }
} }
} }
} }
if isCheck {
if diffCount > 0 {
fmt.Println("Protobuf generated file problem(s) found.")
fmt.Println("Please run 'go run ./infra/vprotogen' to regenerate them.")
os.Exit(1)
}
fmt.Println("All Protobuf generated file check has been passed.")
}
}
+17 -17
View File
@@ -24,11 +24,11 @@ const (
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"` RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"` RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"` FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -65,13 +65,6 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0} return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0}
} }
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetRewriteAddress() *net.IPOrDomain { func (x *Config) GetRewriteAddress() *net.IPOrDomain {
if x != nil { if x != nil {
return x.RewriteAddress return x.RewriteAddress
@@ -93,6 +86,13 @@ func (x *Config) GetPortMap() map[string]string {
return nil return nil
} }
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetFollowRedirect() bool { func (x *Config) GetFollowRedirect() bool {
if x != nil { if x != nil {
return x.FollowRedirect return x.FollowRedirect
@@ -112,11 +112,11 @@ var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor
const file_proxy_dokodemo_config_proto_rawDesc = "" + const file_proxy_dokodemo_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" + "\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" +
"\x06Config\x12C\n" + "\x06Config\x12D\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12D\n" +
"\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" + "\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" +
"\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" + "\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" +
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12'\n" + "\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12'\n" +
"\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" + "\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" +
"\n" + "\n" +
"user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" + "user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" +
@@ -141,13 +141,13 @@ var file_proxy_dokodemo_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_dokodemo_config_proto_goTypes = []any{ var file_proxy_dokodemo_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.dokodemo.Config (*Config)(nil), // 0: xray.proxy.dokodemo.Config
nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry
(net.Network)(0), // 2: xray.common.net.Network (*net.IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain
(*net.IPOrDomain)(nil), // 3: xray.common.net.IPOrDomain (net.Network)(0), // 3: xray.common.net.Network
} }
var file_proxy_dokodemo_config_proto_depIdxs = []int32{ var file_proxy_dokodemo_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network 2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
3, // 1: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain 1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
1, // 2: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry 3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
3, // [3:3] is the sub-list for method output_type 3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type 3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name 3, // [3:3] is the sub-list for extension type_name
+2 -30
View File
@@ -10,7 +10,6 @@ import (
"net/url" "net/url"
"sync" "sync"
"text/template" "text/template"
"time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
@@ -87,13 +86,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
user := server.User user := server.User
var conn stat.Connection var conn stat.Connection
var mbuf buf.MultiBuffer mbuf, _ := link.Reader.ReadMultiBuffer()
if reader, ok := link.Reader.(buf.TimeoutReader); ok {
// Don't wait forever: server-first protocols (SMTP, FTP, MySQL...) send nothing until the server greets.
mbuf, _ = reader.ReadMultiBufferTimeout(time.Millisecond * 500)
} else {
mbuf, _ = link.Reader.ReadMultiBuffer()
}
len := mbuf.Len() len := mbuf.Len()
firstPayload := bytespool.Alloc(len) firstPayload := bytespool.Alloc(len)
mbuf, _ = buf.SplitBytes(mbuf, firstPayload) mbuf, _ = buf.SplitBytes(mbuf, firstPayload)
@@ -241,8 +234,7 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
return nil, err return nil, err
} }
reader := bufio.NewReader(rawConn) resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
resp, err := http.ReadResponse(reader, req)
if err != nil { if err != nil {
rawConn.Close() rawConn.Close()
return nil, err return nil, err
@@ -253,11 +245,6 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
rawConn.Close() rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
} }
// Tunneled bytes (e.g. a server-first greeting) may arrive in the same read as the 200 response.
if n := reader.Buffered(); n > 0 {
early, _ := reader.Peek(n)
return &earlyDataConn{Conn: rawConn, early: early}, nil
}
return rawConn, nil return rawConn, nil
} }
@@ -388,21 +375,6 @@ func (h *http2Conn) Close() error {
return h.out.Close() return h.out.Close()
} }
// earlyDataConn returns the bytes read ahead together with the CONNECT response before reading from Conn.
type earlyDataConn struct {
net.Conn
early []byte
}
func (c *earlyDataConn) Read(p []byte) (int, error) {
if len(c.early) > 0 {
n := copy(p, c.early)
c.early = c.early[n:]
return n, nil
}
return c.Conn.Read(p)
}
func init() { func init() {
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewClient(ctx, config.(*ClientConfig)) return NewClient(ctx, config.(*ClientConfig))
+8 -14
View File
@@ -53,7 +53,12 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
return nil, err return nil, err
} }
udpCodec, err := NewUDPServerCodec(method, psk, 500*time.Second) user := &protocol.MemoryUser{
Email: config.Email,
Level: uint32(config.Level),
}
udpCodec, err := NewUDPServerCodec(method, psk, user, 500*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,10 +69,7 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
method: method, method: method,
psk: psk, psk: psk,
saltFilter: antireplay.NewMapFilter[[32]byte](60), saltFilter: antireplay.NewMapFilter[[32]byte](60),
user: &protocol.MemoryUser{ user: user,
Email: config.Email,
Level: uint32(config.Level),
},
udpCodec: udpCodec, udpCodec: udpCodec,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}, nil }, nil
@@ -155,20 +157,12 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
} }
for _, b := range mb { for _, b := range mb {
decoded, err := i.udpCodec.DecodePacket(b.Bytes()) sessionItem, decoded, err := i.udpCodec.DecodePacket(b.Bytes())
b.Release() b.Release()
if err != nil || decoded.HeaderType != HeaderTypeClient { if err != nil || decoded.HeaderType != HeaderTypeClient {
continue continue
} }
sessionItem := i.udpCodec.GetSession(decoded.SessionID)
if sessionItem.User == nil {
sessionItem.Lock()
if sessionItem.User == nil {
sessionItem.User = i.user
}
sessionItem.Unlock()
}
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
+12 -62
View File
@@ -2,8 +2,6 @@ package shadowsocks_2022
import ( import (
"context" "context"
"crypto/cipher"
"encoding/binary"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -41,8 +39,7 @@ type MultiUserInbound struct {
usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser] usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser]
userCount atomic.Int64 userCount atomic.Int64
saltFilter *antireplay.ReplayFilter[[32]byte] saltFilter *antireplay.ReplayFilter[[32]byte]
udpSessions *UDPSessionManager udpCodec *UDPMultiServerCodec
udpMasterCipher cipher.Block
policyManager policy.Manager policyManager policy.Manager
} }
@@ -68,7 +65,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
return nil, err return nil, err
} }
masterBlock, err := method.NewBlock(masterPSK) udpCodec, err := NewUDPMultiServerCodec(method, masterPSK, 500*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -81,8 +78,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](), usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](),
usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](), usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](),
saltFilter: antireplay.NewMapFilter[[32]byte](60), saltFilter: antireplay.NewMapFilter[[32]byte](60),
udpSessions: NewUDPSessionManager(500 * time.Second), udpCodec: udpCodec,
udpMasterCipher: masterBlock,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
} }
@@ -281,68 +277,26 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
for _, b := range mb { for _, b := range mb {
// In multi-user UDP: sessionItem, decoded, user, err := i.udpCodec.DecodePacket(b.Bytes(), func(hash [AESBlockSize]byte) (*protocol.MemoryUser, []byte, bool) {
// Packet header is 16 bytes: Encrypted(SessionID + PacketID) u, ok := i.usersByHash.Load(hash)
// Followed by 16 bytes EIH
packetBytes := b.Bytes()
if len(packetBytes) < 32+1+8+2 {
b.Release()
continue
}
var rawHeader [16]byte
i.udpMasterCipher.Decrypt(rawHeader[:], packetBytes[:16])
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := i.udpSessions.GetOrCreate(sessionID)
if !sessionItem.CheckPacketID(packetID) {
b.Release()
continue
}
var userPSK []byte
var currentUser *protocol.MemoryUser
sessionItem.Lock()
currentUser = sessionItem.User
userPSK = sessionItem.UserPSK
sessionItem.Unlock()
if currentUser == nil {
// Decrypt EIH
decryptedHash := DecryptUDPEIH(i.udpMasterCipher, rawHeader[:], packetBytes[16:32])
user, ok := i.usersByHash.Load(decryptedHash)
if !ok { if !ok {
return nil, nil, false
}
return u, u.Account.(*MemoryAccount).Key, true
})
b.Release() b.Release()
if err != nil || decoded.HeaderType != HeaderTypeClient {
continue continue
} }
currentUser = user
userPSK = user.Account.(*MemoryAccount).Key
}
decoded, err := sessionItem.DecryptAESPayload(i.method, userPSK, sessionID, packetID, rawHeader[:], packetBytes[32:])
b.Release()
if err != nil {
continue
}
sessionItem.Lock()
if sessionItem.User == nil {
sessionItem.User = currentUser
sessionItem.UserPSK = userPSK
}
sessionItem.Unlock()
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
} else { } else {
userPSK := user.Account.(*MemoryAccount).Key
var err error var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) { link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload) return i.udpCodec.EncodeServerPacket(decoded.SessionID, userPSK, replyDest, payload)
}) })
if err != nil { if err != nil {
continue continue
@@ -356,7 +310,3 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
} }
} }
func (i *MultiUserInbound) encodeServerUDPPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
return i.udpSessions.EncodeServerPacket(i.method, userPSK, clientSessionID, dest, payload)
}
-2
View File
@@ -243,7 +243,6 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
dest.Network = net.Network_UDP dest.Network = net.Network_UDP
sessionItem := i.udpSessions.GetOrCreate(sessionID) sessionItem := i.udpSessions.GetOrCreate(sessionID)
if sessionItem.User == nil {
sessionItem.Lock() sessionItem.Lock()
if sessionItem.User == nil { if sessionItem.User == nil {
sessionItem.User = &protocol.MemoryUser{ sessionItem.User = &protocol.MemoryUser{
@@ -252,7 +251,6 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
} }
} }
sessionItem.Unlock() sessionItem.Unlock()
}
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
+3 -3
View File
@@ -30,7 +30,7 @@ type Outbound struct {
method *CipherMethod method *CipherMethod
pskList [][]byte pskList [][]byte
finalPSK []byte finalPSK []byte
udpCodec *UDPPacketCodec udpCodec *UDPClientCodec
policyManager policy.Manager policyManager policy.Manager
} }
@@ -50,9 +50,9 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
} }
finalPSK := pskList[len(pskList)-1] finalPSK := pskList[len(pskList)-1]
udpCodec, err := NewUDPPacketCodec(method, pskList) udpCodec, err := NewUDPClientCodec(method, pskList)
if err != nil { if err != nil {
return nil, errors.New("failed to create udp packet codec").Base(err) return nil, errors.New("failed to create udp client codec").Base(err)
} }
v := core.MustFromContext(ctx) v := core.MustFromContext(ctx)
+323 -123
View File
@@ -7,33 +7,78 @@ import (
"io" "io"
"math" "math"
mrand "math/rand/v2" mrand "math/rand/v2"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
) )
type UDPCodec struct { type UDPClientCodec struct {
method *CipherMethod method *CipherMethod
pskList [][]byte pskList [][]byte
psk []byte psk []byte
blockCipher cipher.Block blockCipher cipher.Block
blockCiphers []cipher.Block blockCiphers []cipher.Block
chachaCipher cipher.AEAD chachaCipher cipher.AEAD
}
func NewUDPClientCodec(method *CipherMethod, pskList [][]byte) (*UDPClientCodec, error) {
if len(pskList) == 0 {
return nil, errors.New("empty psk list")
}
if method.IsChaCha && len(pskList) > 1 {
return nil, errors.New("multi-key is not supported for chacha20-poly1305")
}
finalPSK := pskList[len(pskList)-1]
c := &UDPClientCodec{
method: method,
pskList: pskList,
psk: finalPSK,
}
var err error
if method.IsChaCha {
c.chachaCipher, err = method.NewUDPCipher(finalPSK)
} else {
c.blockCipher, err = method.NewBlock(finalPSK)
}
if err != nil {
return nil, err
}
if len(pskList) > 1 {
c.blockCiphers = make([]cipher.Block, len(pskList))
for i, psk := range pskList {
c.blockCiphers[i], err = method.NewBlock(psk)
if err != nil {
return nil, err
}
}
}
return c, nil
}
func NewUDPPacketCodec(method *CipherMethod, pskList [][]byte) (*UDPClientCodec, error) {
return NewUDPClientCodec(method, pskList)
}
type UDPServerCodec struct {
method *CipherMethod
psk []byte
user *protocol.MemoryUser
blockCipher cipher.Block
chachaCipher cipher.AEAD
sessions *UDPSessionManager sessions *UDPSessionManager
} }
type ( func NewUDPServerCodec(method *CipherMethod, psk []byte, user *protocol.MemoryUser, sessionTimeout time.Duration) (*UDPServerCodec, error) {
UDPPacketCodec = UDPCodec c := &UDPServerCodec{
UDPServerCodec = UDPCodec
)
func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
c := &UDPCodec{
method: method, method: method,
psk: psk, psk: psk,
user: user,
sessions: NewUDPSessionManager(sessionTimeout),
} }
var err error var err error
if method.IsChaCha { if method.IsChaCha {
@@ -47,46 +92,129 @@ func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
return c, nil return c, nil
} }
func NewUDPPacketCodec(method *CipherMethod, pskList [][]byte) (*UDPCodec, error) { func (c *UDPServerCodec) Sessions() *UDPSessionManager {
if method.IsChaCha && len(pskList) > 1 {
return nil, errors.New("multi-key is not supported for chacha20-poly1305")
}
finalPSK := pskList[len(pskList)-1]
c, err := newUDPCodec(method, finalPSK)
if err != nil {
return nil, err
}
c.pskList = pskList
if len(pskList) > 1 {
c.blockCiphers = make([]cipher.Block, len(pskList))
for i, psk := range pskList {
c.blockCiphers[i], err = method.NewBlock(psk)
if err != nil {
return nil, err
}
}
}
return c, nil
}
func NewUDPServerCodec(method *CipherMethod, psk []byte, sessionTimeout time.Duration) (*UDPCodec, error) {
c, err := newUDPCodec(method, psk)
if err != nil {
return nil, err
}
c.sessions = NewUDPSessionManager(sessionTimeout)
return c, nil
}
func (c *UDPCodec) Sessions() *UDPSessionManager {
return c.sessions return c.sessions
} }
func (c *UDPCodec) GetSession(sessionID uint64) *ServerUDPSession { func (c *UDPServerCodec) GetSession(sessionID uint64) *ServerUDPSession {
if c.sessions == nil { if c.sessions == nil {
return nil return nil
} }
return c.sessions.GetOrCreate(sessionID) return c.sessions.Get(sessionID)
}
type UDPMultiServerCodec struct {
method *CipherMethod
masterPSK []byte
masterBlock cipher.Block
sessions *UDPSessionManager
}
func NewUDPMultiServerCodec(method *CipherMethod, masterPSK []byte, sessionTimeout time.Duration) (*UDPMultiServerCodec, error) {
if method.IsChaCha {
return nil, errors.New("multi-user is not supported for chacha20-poly1305")
}
masterBlock, err := method.NewBlock(masterPSK)
if err != nil {
return nil, err
}
return &UDPMultiServerCodec{
method: method,
masterPSK: masterPSK,
masterBlock: masterBlock,
sessions: NewUDPSessionManager(sessionTimeout),
}, nil
}
func (c *UDPMultiServerCodec) Sessions() *UDPSessionManager {
return c.sessions
}
func (c *UDPMultiServerCodec) GetSession(sessionID uint64) *ServerUDPSession {
if c.sessions == nil {
return nil
}
return c.sessions.Get(sessionID)
}
type UserLookupFunc func(hash [AESBlockSize]byte) (*protocol.MemoryUser, []byte, bool)
func (c *UDPMultiServerCodec) DecodePacket(data []byte, userLookup UserLookupFunc) (*ServerUDPSession, DecodedUDPPacket, *protocol.MemoryUser, error) {
if len(data) < 2*AESBlockSize+AEADTagSize+RequestHeaderFixedChunkLength {
return nil, DecodedUDPPacket{}, nil, ErrPacketTooShort
}
var rawHeader [16]byte
c.masterBlock.Decrypt(rawHeader[:], data[:16])
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := c.sessions.Get(sessionID)
var currentUser *protocol.MemoryUser
var userPSK []byte
var bodyAead cipher.AEAD
if sessionItem != nil {
sessionItem.Lock()
currentUser = sessionItem.User
userPSK = sessionItem.UserPSK
bodyAead = sessionItem.clientBodyCipher
sessionItem.Unlock()
if currentUser != nil {
if !sessionItem.CheckPacketID(packetID) {
return nil, DecodedUDPPacket{}, nil, ErrPacketIdNotUnique
}
}
}
if currentUser == nil {
decryptedHash := DecryptUDPEIH(c.masterBlock, rawHeader[:], data[16:32])
user, psk, ok := userLookup(decryptedHash)
if !ok {
return nil, DecodedUDPPacket{}, nil, errors.New("user not found")
}
currentUser = user
userPSK = psk
}
if bodyAead == nil {
bodyKey := DeriveSessionSubKey(userPSK, rawHeader[:8], c.method.KeySaltLength)
var err error
bodyAead, err = c.method.NewAEAD(bodyKey)
if err != nil {
return nil, DecodedUDPPacket{}, nil, err
}
}
bodyNonce := rawHeader[4:16]
bodyCipher := data[32:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil {
return nil, DecodedUDPPacket{}, nil, errors.New("failed to decrypt aes udp body").Base(err)
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil {
return nil, DecodedUDPPacket{}, nil, err
}
if decoded.HeaderType != HeaderTypeClient {
return nil, DecodedUDPPacket{}, nil, ErrBadHeaderType
}
if sessionItem == nil {
sessionItem = c.sessions.GetOrCreate(sessionID)
}
if !sessionItem.Commit(packetID, bodyAead, currentUser, userPSK) {
return nil, DecodedUDPPacket{}, nil, ErrPacketIdNotUnique
}
return sessionItem, decoded, currentUser, nil
}
func (c *UDPMultiServerCodec) EncodeServerPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
return c.sessions.EncodeServerPacket(c.method, userPSK, clientSessionID, dest, payload)
} }
type DecodedUDPPacket struct { type DecodedUDPPacket struct {
@@ -193,44 +321,42 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
}, nil }, nil
} }
func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) { func (c *UDPServerCodec) DecodePacket(data []byte) (*ServerUDPSession, DecodedUDPPacket, error) {
if len(data) < PacketMinimalHeaderSize { if len(data) < PacketMinimalHeaderSize {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
if c.method.IsChaCha { if c.method.IsChaCha {
if len(data) < PacketNonceSize+AEADTagSize { if len(data) < PacketNonceSize+AEADTagSize {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
nonce := data[:PacketNonceSize] nonce := data[:PacketNonceSize]
ciphertext := data[PacketNonceSize:] ciphertext := data[PacketNonceSize:]
plain, err := c.chachaCipher.Open(nil, nonce, ciphertext, nil) plain, err := c.chachaCipher.Open(nil, nonce, ciphertext, nil)
if err != nil { if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err) return nil, DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err)
} }
if len(plain) < 16+1+8+2 { if len(plain) < 16+1+8+2 {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
sessionID := binary.BigEndian.Uint64(plain[:8]) sessionID := binary.BigEndian.Uint64(plain[:8])
packetID := binary.BigEndian.Uint64(plain[8:16]) packetID := binary.BigEndian.Uint64(plain[8:16])
sessionItem := c.sessions.GetOrCreate(sessionID)
if !sessionItem.CheckPacketID(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:]) decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:])
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
return DecodedUDPPacket{}, ErrBadHeaderType return nil, DecodedUDPPacket{}, ErrBadHeaderType
} }
sessionItem.AddPacketID(packetID) sessionItem := c.sessions.GetOrCreate(sessionID)
return decoded, nil if !sessionItem.Commit(packetID, nil, c.user, c.psk) {
return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
return sessionItem, decoded, nil
} }
// AES mode // AES mode
@@ -239,49 +365,48 @@ func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) {
sessionID := binary.BigEndian.Uint64(rawHeader[:8]) sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16]) packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := c.sessions.GetOrCreate(sessionID) sessionItem := c.sessions.Get(sessionID)
var bodyAead cipher.AEAD
if sessionItem != nil {
if !sessionItem.CheckPacketID(packetID) { if !sessionItem.CheckPacketID(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
sessionItem.Lock()
bodyAead = sessionItem.clientBodyCipher
sessionItem.Unlock()
} }
return sessionItem.DecryptAESPayload(c.method, c.psk, sessionID, packetID, rawHeader[:], data[16:])
}
func (s *ServerUDPSession) DecryptAESPayload(method *CipherMethod, psk []byte, sessionID, packetID uint64, rawHeader, bodyCipher []byte) (DecodedUDPPacket, error) {
bodyAead := s.clientBodyCipher
isNewCipher := false
if bodyAead == nil { if bodyAead == nil {
bodyKey := DeriveSessionSubKey(psk, rawHeader[:8], method.KeySaltLength) bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
var err error var err error
bodyAead, err = method.NewAEAD(bodyKey) bodyAead, err = c.method.NewAEAD(bodyKey)
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
isNewCipher = true
} }
bodyNonce := rawHeader[4:16] bodyNonce := rawHeader[4:16]
bodyCipher := data[16:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil) bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil { if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err) return nil, DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
} }
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain) decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
return DecodedUDPPacket{}, ErrBadHeaderType return nil, DecodedUDPPacket{}, ErrBadHeaderType
} }
s.AddPacketID(packetID) if sessionItem == nil {
sessionItem = c.sessions.GetOrCreate(sessionID)
if isNewCipher {
s.clientBodyCipher = bodyAead
} }
if !sessionItem.Commit(packetID, bodyAead, c.user, c.psk) {
return decoded, nil return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
return sessionItem, decoded, nil
} }
func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) error { func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) error {
@@ -303,6 +428,9 @@ func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) e
if method.IsChaCha { if method.IsChaCha {
var err error var err error
s.serverChaCha, err = method.NewUDPCipher(psk) s.serverChaCha, err = method.NewUDPCipher(psk)
if err != nil {
s.ServerSessionID = 0
}
return err return err
} }
@@ -387,11 +515,12 @@ func (s *ServerUDPSession) EncodeServerPacket(method *CipherMethod, clientSessio
return res, nil return res, nil
} }
func (c *UDPCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) { func (c *UDPServerCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
return c.sessions.EncodeServerPacket(c.method, c.psk, clientSessionID, dest, payload) return c.sessions.EncodeServerPacket(c.method, c.psk, clientSessionID, dest, payload)
} }
type serverSessionState struct { type serverSessionState struct {
sync.Mutex
sessionID uint64 sessionID uint64
window *SlidingWindow window *SlidingWindow
cipher cipher.AEAD cipher cipher.AEAD
@@ -399,21 +528,20 @@ type serverSessionState struct {
} }
func (st *serverSessionState) check(packetID uint64) bool { func (st *serverSessionState) check(packetID uint64) bool {
if st.window == nil { st.Lock()
st.window = new(SlidingWindow) defer st.Unlock()
}
return st.window.Check(packetID) return st.window.Check(packetID)
} }
func (st *serverSessionState) add(packetID uint64) { func (st *serverSessionState) commit(packetID uint64) bool {
if st.window == nil { st.Lock()
st.window = new(SlidingWindow) defer st.Unlock()
} return st.window.CheckAndAdd(packetID)
st.window.Add(packetID)
} }
type ClientUDPSession struct { type ClientUDPSession struct {
codec *UDPCodec mu sync.Mutex
codec *UDPClientCodec
clientSessionID uint64 clientSessionID uint64
nextPacketID atomic.Uint64 nextPacketID atomic.Uint64
clientBodyCipher cipher.AEAD clientBodyCipher cipher.AEAD
@@ -421,7 +549,7 @@ type ClientUDPSession struct {
old atomic.Pointer[serverSessionState] old atomic.Pointer[serverSessionState]
} }
func (c *UDPCodec) NewClientSession() (*ClientUDPSession, error) { func (c *UDPClientCodec) NewClientSession() (*ClientUDPSession, error) {
var sessID [8]byte var sessID [8]byte
if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil { if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil {
return nil, err return nil, err
@@ -446,7 +574,7 @@ func (c *UDPCodec) NewClientSession() (*ClientUDPSession, error) {
}, nil }, nil
} }
func (s *ClientUDPSession) getServerSession(sessionID uint64, now int64) (*serverSessionState, error) { func (s *ClientUDPSession) getOrTransitionServerSession(sessionID uint64, cipher cipher.AEAD, now int64) (*serverSessionState, error) {
cur := s.current.Load() cur := s.current.Load()
if cur != nil && cur.sessionID == sessionID { if cur != nil && cur.sessionID == sessionID {
return cur, nil return cur, nil
@@ -455,39 +583,42 @@ func (s *ClientUDPSession) getServerSession(sessionID uint64, now int64) (*serve
old := s.old.Load() old := s.old.Load()
if old != nil && old.sessionID == sessionID { if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 { if now-old.lastSeen.Load() > 60 {
s.old.CompareAndSwap(old, nil)
return nil, errors.New("old server session expired") return nil, errors.New("old server session expired")
} }
return old, nil return old, nil
} }
// New server session: s.mu.Lock()
defer s.mu.Unlock()
cur = s.current.Load()
if cur != nil && cur.sessionID == sessionID {
return cur, nil
}
old = s.old.Load()
if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 {
return nil, errors.New("old server session expired")
}
return old, nil
}
// Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old. // Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old.
if old != nil && now-old.lastSeen.Load() < 60 { if old != nil && now-old.lastSeen.Load() < 60 {
return nil, errors.New("newer server session rejected: old session is less than 1 minute old") return nil, errors.New("newer server session rejected: old session is less than 1 minute old")
} }
var bodyAead cipher.AEAD
if !s.codec.method.IsChaCha {
var sessBytes [8]byte
binary.BigEndian.PutUint64(sessBytes[:], sessionID)
bodyKey := DeriveSessionSubKey(s.codec.psk, sessBytes[:], s.codec.method.KeySaltLength)
var err error
bodyAead, err = s.codec.method.NewAEAD(bodyKey)
if err != nil {
return nil, err
}
}
newState := &serverSessionState{ newState := &serverSessionState{
sessionID: sessionID, sessionID: sessionID,
cipher: bodyAead, window: new(SlidingWindow),
cipher: cipher,
} }
newState.lastSeen.Store(now) newState.lastSeen.Store(now)
if cur == nil { if cur == nil {
s.current.CompareAndSwap(nil, newState) s.current.Store(newState)
return s.current.Load(), nil return newState, nil
} }
s.old.Store(cur) s.old.Store(cur)
@@ -637,15 +768,6 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
sessionID := binary.BigEndian.Uint64(plain[:8]) sessionID := binary.BigEndian.Uint64(plain[:8])
packetID := binary.BigEndian.Uint64(plain[8:16]) packetID := binary.BigEndian.Uint64(plain[8:16])
now := time.Now().Unix()
st, err := s.getServerSession(sessionID, now)
if err != nil {
return DecodedUDPPacket{}, err
}
if !st.check(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:]) decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:])
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return DecodedUDPPacket{}, err
@@ -658,7 +780,16 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
return DecodedUDPPacket{}, errors.New("client session ID mismatch") return DecodedUDPPacket{}, errors.New("client session ID mismatch")
} }
st.add(packetID) now := time.Now().Unix()
st, err := s.getOrTransitionServerSession(sessionID, nil, now)
if err != nil {
return DecodedUDPPacket{}, err
}
// Atomically commit after all validations succeed
if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now) st.lastSeen.Store(now)
return decoded, nil return decoded, nil
@@ -671,15 +802,30 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
packetID := binary.BigEndian.Uint64(rawHeader[8:16]) packetID := binary.BigEndian.Uint64(rawHeader[8:16])
now := time.Now().Unix() now := time.Now().Unix()
st, err := s.getServerSession(sessionID, now)
if err != nil { // 1. Fast path for known active session
return DecodedUDPPacket{}, err cur := s.current.Load()
var st *serverSessionState
var bodyAead cipher.AEAD
if cur != nil && cur.sessionID == sessionID {
st = cur
bodyAead = cur.cipher
} else {
old := s.old.Load()
if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 {
return DecodedUDPPacket{}, errors.New("old server session expired")
} }
st = old
bodyAead = old.cipher
}
}
if st != nil {
if !st.check(packetID) { if !st.check(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique return DecodedUDPPacket{}, ErrPacketIdNotUnique
} }
bodyAead := st.cipher
bodyNonce := rawHeader[4:16] bodyNonce := rawHeader[4:16]
bodyCipher := data[16:] bodyCipher := data[16:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil) bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
@@ -699,7 +845,61 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
return DecodedUDPPacket{}, errors.New("client session ID mismatch") return DecodedUDPPacket{}, errors.New("client session ID mismatch")
} }
st.add(packetID) if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now)
return decoded, nil
}
// 2. Unknown session: Trial path without mutating s.current or s.old yet!
// Re-check cur in case another worker concurrently transitioned to this sessionID
cur = s.current.Load()
old := s.old.Load()
if (cur == nil || cur.sessionID != sessionID) && (old == nil || old.sessionID != sessionID) {
// Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old.
if old != nil && now-old.lastSeen.Load() < 60 {
return DecodedUDPPacket{}, errors.New("newer server session rejected: old session is less than 1 minute old")
}
}
var sessBytes [8]byte
binary.BigEndian.PutUint64(sessBytes[:], sessionID)
bodyKey := DeriveSessionSubKey(s.codec.psk, sessBytes[:], s.codec.method.KeySaltLength)
trialAead, err := s.codec.method.NewAEAD(bodyKey)
if err != nil {
return DecodedUDPPacket{}, err
}
bodyNonce := rawHeader[4:16]
bodyCipher := data[16:]
bodyPlain, err := trialAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil {
return DecodedUDPPacket{}, err
}
if decoded.HeaderType != HeaderTypeServer {
return DecodedUDPPacket{}, ErrBadHeaderType
}
if decoded.ClientSessionID != s.clientSessionID {
return DecodedUDPPacket{}, errors.New("client session ID mismatch")
}
// Payload authenticated! Safely commit and transition session under lock
st, err = s.getOrTransitionServerSession(sessionID, trialAead, now)
if err != nil {
return DecodedUDPPacket{}, err
}
if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now) st.lastSeen.Store(now)
return decoded, nil return decoded, nil
+4 -3
View File
@@ -191,7 +191,7 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
// Verify downstream destination can decode both packets // Verify downstream destination can decode both packets
method, err := GetCipherMethod(MethodAES128GCM) method, err := GetCipherMethod(MethodAES128GCM)
common.Must(err) common.Must(err)
destCodec, err := NewUDPServerCodec(method, destKey, 300*time.Second) destCodec, err := NewUDPServerCodec(method, destKey, nil, 300*time.Second)
common.Must(err) common.Must(err)
mu.Lock() mu.Lock()
@@ -202,7 +202,8 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
t.Fatalf("expected 2 received packets at destination, got %d", len(pkts)) t.Fatalf("expected 2 received packets at destination, got %d", len(pkts))
} }
dec1, err := destCodec.DecodePacket(pkts[0]) dec1, dec2 := DecodedUDPPacket{}, DecodedUDPPacket{}
_, dec1, err = destCodec.DecodePacket(pkts[0])
if err != nil { if err != nil {
t.Fatalf("dest failed to decode packet 1: %v", err) t.Fatalf("dest failed to decode packet 1: %v", err)
} }
@@ -210,7 +211,7 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload)) t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload))
} }
dec2, err := destCodec.DecodePacket(pkts[1]) _, dec2, err = destCodec.DecodePacket(pkts[1])
if err != nil { if err != nil {
t.Fatalf("dest failed to decode packet 2: %v", err) t.Fatalf("dest failed to decode packet 2: %v", err)
} }
+23 -7
View File
@@ -100,19 +100,27 @@ type ServerUDPSession struct {
func (s *ServerUDPSession) CheckPacketID(packetID uint64) bool { func (s *ServerUDPSession) CheckPacketID(packetID uint64) bool {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.Window == nil {
s.Window = new(SlidingWindow)
}
return s.Window.Check(packetID) return s.Window.Check(packetID)
} }
func (s *ServerUDPSession) AddPacketID(packetID uint64) { func (s *ServerUDPSession) Commit(packetID uint64, bodyAead cipher.AEAD, user *protocol.MemoryUser, userPSK []byte) bool {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.Window == nil { if s.User != nil && user != nil && s.User != user {
s.Window = new(SlidingWindow) return false
} }
s.Window.Add(packetID) if !s.Window.CheckAndAdd(packetID) {
return false
}
if s.clientBodyCipher == nil && bodyAead != nil {
s.clientBodyCipher = bodyAead
}
if s.User == nil && user != nil {
s.User = user
s.UserPSK = userPSK
}
s.LastActive.Store(time.Now().Unix())
return true
} }
type UDPSessionManager struct { type UDPSessionManager struct {
@@ -128,6 +136,13 @@ func NewUDPSessionManager(timeout time.Duration) *UDPSessionManager {
} }
} }
func (m *UDPSessionManager) Get(sessionID uint64) *ServerUDPSession {
if s, ok := m.sessions.Load(sessionID); ok {
return s
}
return nil
}
func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession { func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
now := time.Now().Unix() now := time.Now().Unix()
if s, ok := m.sessions.Load(sessionID); ok { if s, ok := m.sessions.Load(sessionID); ok {
@@ -137,6 +152,7 @@ func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
s := &ServerUDPSession{ s := &ServerUDPSession{
SessionID: sessionID, SessionID: sessionID,
Window: new(SlidingWindow),
manager: m, manager: m,
} }
s.LastActive.Store(now) s.LastActive.Store(now)
+9 -3
View File
@@ -63,10 +63,16 @@ func (s *ServerUDPSession) EnsureLink(
s.currentConn.Store(conn) s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx) // UDP Session might travel across multiple connections
// don't bind its lifetime to a parent context
sessCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
inbound := session.InboundFromContext(sessCtx) inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil { if inbound != nil {
inbound.User = s.User newInbound := *inbound
if s.User != nil {
newInbound.User = s.User
}
sessCtx = session.ContextWithInbound(sessCtx, &newInbound)
} }
var email string var email string
var level uint32 var level uint32
@@ -265,7 +265,7 @@ func TestUDPCodec(t *testing.T) {
clientCodec, err := NewUDPPacketCodec(method, [][]byte{psk}) clientCodec, err := NewUDPPacketCodec(method, [][]byte{psk})
common.Must(err) common.Must(err)
serverCodec, err := NewUDPServerCodec(method, psk, time.Minute) serverCodec, err := NewUDPServerCodec(method, psk, nil, time.Minute)
common.Must(err) common.Must(err)
session, err := clientCodec.NewClientSession() session, err := clientCodec.NewClientSession()
@@ -277,7 +277,7 @@ func TestUDPCodec(t *testing.T) {
rawCopy := make([]byte, pktBuf.Len()) rawCopy := make([]byte, pktBuf.Len())
copy(rawCopy, pktBuf.Bytes()) copy(rawCopy, pktBuf.Bytes())
decoded, err := serverCodec.DecodePacket(pktBuf.Bytes()) _, decoded, err := serverCodec.DecodePacket(pktBuf.Bytes())
common.Must(err) common.Must(err)
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
@@ -291,7 +291,7 @@ func TestUDPCodec(t *testing.T) {
} }
// Replay same packet wire bytes should fail with ErrPacketIdNotUnique // Replay same packet wire bytes should fail with ErrPacketIdNotUnique
_, err = serverCodec.DecodePacket(rawCopy) _, _, err = serverCodec.DecodePacket(rawCopy)
if err != ErrPacketIdNotUnique { if err != ErrPacketIdNotUnique {
t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err) t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err)
} }
@@ -427,7 +427,7 @@ func TestClientUDPSessionMultiDestination(t *testing.T) {
clientCodec, err := NewUDPPacketCodec(method, [][]byte{rawKey}) clientCodec, err := NewUDPPacketCodec(method, [][]byte{rawKey})
common.Must(err) common.Must(err)
serverCodec, err := NewUDPServerCodec(method, rawKey, time.Minute) serverCodec, err := NewUDPServerCodec(method, rawKey, nil, time.Minute)
common.Must(err) common.Must(err)
session, err := clientCodec.NewClientSession() session, err := clientCodec.NewClientSession()
@@ -448,9 +448,9 @@ func TestClientUDPSessionMultiDestination(t *testing.T) {
defer pkt2.Release() defer pkt2.Release()
// Server decodes both // Server decodes both
dec1, err := serverCodec.DecodePacket(pkt1.Bytes()) _, dec1, err := serverCodec.DecodePacket(pkt1.Bytes())
common.Must(err) common.Must(err)
dec2, err := serverCodec.DecodePacket(pkt2.Bytes()) _, dec2, err := serverCodec.DecodePacket(pkt2.Bytes())
common.Must(err) common.Must(err)
if dec1.SessionID != session.ClientSessionID() || dec2.SessionID != session.ClientSessionID() { if dec1.SessionID != session.ClientSessionID() || dec2.SessionID != session.ClientSessionID() {
+4 -5
View File
@@ -2,10 +2,10 @@ package socks
import ( import (
"context" "context"
"net"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/signal"
) )
@@ -14,9 +14,6 @@ func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, expectedRemote *ne
PacketConn: udpConn, PacketConn: udpConn,
AssociatedTCPConn: tcpConn, AssociatedTCPConn: tcpConn,
} }
t.Timer = signal.CancelAfterInactivity(context.Background(), func() {
t.Close()
}, net.ConnIdleTimeout)
t.ExpectedRemote.Store(expectedRemote) t.ExpectedRemote.Store(expectedRemote)
return t return t
} }
@@ -63,7 +60,9 @@ func (c *TempUDPConn) RemoteAddr() net.Addr {
} }
func (c *TempUDPConn) SetTimeout(d time.Duration) { func (c *TempUDPConn) SetTimeout(d time.Duration) {
c.Timer.SetTimeout(d) c.Timer = signal.CancelAfterInactivity(context.Background(), func() {
c.Close()
}, d)
} }
func (c *TempUDPConn) Close() error { func (c *TempUDPConn) Close() error {
+3 -2
View File
@@ -7,11 +7,12 @@
package tun package tun
import ( import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe" unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
) )
const ( const (
+7 -8
View File
@@ -41,9 +41,8 @@ const (
type packet struct { type packet struct {
proto byte proto byte
src, dst netip.Addr src, dst netip.Addr
// payload is what follows the IP header, without the last rest bytes of it when they are in other buffers // payload is what follows the IP header
payload []byte payload []byte
rest int
// fragment is set if payload is a part of what follows the IP header: the one at offset of the packet // fragment is set if payload is a part of what follows the IP header: the one at offset of the packet
// with the number id, which has more after it or not // with the number id, which has more after it or not
fragment bool fragment bool
@@ -52,8 +51,8 @@ type packet struct {
id uint32 id uint32
} }
// parsePacket reads the IP header of a packet that is b and rest bytes after it // parsePacket reads the IP header of b
func parsePacket(b []byte, rest int) (p packet, ok bool) { func parsePacket(b []byte) (p packet, ok bool) {
if len(b) < ipv4HeaderSize { if len(b) < ipv4HeaderSize {
return p, false return p, false
} }
@@ -61,7 +60,7 @@ func parsePacket(b []byte, rest int) (p packet, ok bool) {
case 4: case 4:
size := int(b[0]&0xf) * 4 size := int(b[0]&0xf) * 4
total := int(binary.BigEndian.Uint16(b[2:])) total := int(binary.BigEndian.Uint16(b[2:]))
if size < ipv4HeaderSize || total < size || total > len(b)+rest { if size < ipv4HeaderSize || total < size || total > len(b) {
return p, false return p, false
} }
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 { if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
@@ -70,19 +69,19 @@ func parsePacket(b []byte, rest int) (p packet, ok bool) {
p.proto = b[9] p.proto = b[9]
p.src = netip.AddrFrom4([4]byte(b[12:16])) p.src = netip.AddrFrom4([4]byte(b[12:16]))
p.dst = netip.AddrFrom4([4]byte(b[16:20])) p.dst = netip.AddrFrom4([4]byte(b[16:20]))
p.payload, p.rest = b[size:min(total, len(b))], max(total-len(b), 0) p.payload = b[size:total]
case 6: case 6:
if len(b) < ipv6HeaderSize { if len(b) < ipv6HeaderSize {
return p, false return p, false
} }
total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:])) total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:]))
if total > len(b)+rest { if total > len(b) {
return p, false return p, false
} }
p.proto = b[6] p.proto = b[6]
p.src = netip.AddrFrom16([16]byte(b[8:24])) p.src = netip.AddrFrom16([16]byte(b[8:24]))
p.dst = netip.AddrFrom16([16]byte(b[24:40])) p.dst = netip.AddrFrom16([16]byte(b[24:40]))
p.payload, p.rest = b[ipv6HeaderSize:min(total, len(b))], max(total-len(b), 0) p.payload = b[ipv6HeaderSize:total]
// hop-by-hop options, routing, destination options // hop-by-hop options, routing, destination options
for p.proto == 0 || p.proto == 43 || p.proto == 60 { for p.proto == 0 || p.proto == 43 || p.proto == 60 {
if len(p.payload) < 8 { if len(p.payload) < 8 {
+8 -88
View File
@@ -71,10 +71,6 @@ type stack struct {
udp *udpConnectionHandler udp *udpConnectionHandler
// addresses are what connections are opened from // addresses are what connections are opened from
addresses []netip.Addr addresses []netip.Addr
// bufs is what read reads a packet into if the tun is a segmenter, one after the other, the headers in the first.
// One that keep took is nil. joined is for a packet that is in more than one and is needed in one piece
bufs buf.MultiBuffer
joined []byte
mu sync.Mutex mu sync.Mutex
conns map[flow]*tcpConn conns map[flow]*tcpConn
@@ -153,66 +149,26 @@ func (s *stack) Close() error {
// read takes the packets of the tun until it is closed // read takes the packets of the tun until it is closed
func (s *stack) read() { func (s *stack) read() {
var err error // a tun that does not split segments gives them as large as a packet can be
if s.segmenter != nil {
err = s.readSegments()
} else {
// as large as a packet can be, whatever the MTU is
b := make([]byte, packetRoom+1<<16) b := make([]byte, packetRoom+1<<16)
for err == nil { for {
var p []byte b, err := s.tun.ReadPacket(b)
if p, err = s.tun.ReadPacket(b); err == nil { if err != nil {
s.Input(p)
}
}
}
s.mu.Lock() s.mu.Lock()
closed := s.closed closed := s.closed
s.mu.Unlock() s.mu.Unlock()
if !closed { if !closed {
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read") errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
} }
return
} }
s.Input(b)
// readSegments is read for a tun that does not split segments and gives them as large as a packet can be.
// They are read into buffers for a connection to keep what is large as it is, see keep
func (s *stack) readSegments() error {
s.bufs = make(buf.MultiBuffer, (1<<16)/buf.Size)
defer buf.ReleaseMulti(s.bufs)
parts := make([][]byte, len(s.bufs))
var r room
for {
for i, b := range s.bufs {
if b == nil {
s.bufs[i] = buf.New()
_, _ = s.bufs[i].ReadFrom(&r)
parts[i] = r.b
} }
} }
n, err := s.segmenter.ReadParts(parts)
if err != nil {
return err
}
s.input(parts[0][:min(n, buf.Size)], max(n-buf.Size, 0))
}
}
// room takes what it is given to read into as read: that is the room of a Buffer as it is, not cleared
type room struct{ b []byte }
func (r *room) Read(b []byte) (int, error) {
r.b = b
return len(b), nil
}
// Input implements Stack. Several tunnels may hand over their packets at once // Input implements Stack. Several tunnels may hand over their packets at once
func (s *stack) Input(b []byte) { func (s *stack) Input(b []byte) {
s.input(b, 0) p, ok := parsePacket(b)
}
// input takes a packet that is b and rest bytes after it, in the bufs that follow the one that b is in
func (s *stack) input(b []byte, rest int) {
p, ok := parsePacket(b, rest)
if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() { if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() {
// nothing comes from many at once, and an address that stands for this machine itself is not one // nothing comes from many at once, and an address that stands for this machine itself is not one
// that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which // that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which
@@ -224,10 +180,6 @@ func (s *stack) input(b []byte, rest int) {
} }
// what was put together may be of two packets with one number, its checksum tells // what was put together may be of two packets with one number, its checksum tells
verify := s.verify || p.fragment verify := s.verify || p.fragment
if p.rest != 0 && (verify || p.proto != protoTCP) {
// only a segment of TCP that is not looked at can stay in pieces
p.payload, p.rest = s.whole(p.payload, p.rest), 0
}
if p.fragment { if p.fragment {
p, ok = s.fragments.add(p) p, ok = s.fragments.add(p)
} }
@@ -244,37 +196,6 @@ func (s *stack) input(b []byte, rest int) {
} }
} }
// whole returns in one piece what is b and rest bytes after it, in the bufs that follow the one that b is in
func (s *stack) whole(b []byte, rest int) []byte {
if rest == 0 {
return b
}
s.joined = append(s.joined[:0], b...)
for i := 1; rest > 0; i++ {
n := min(rest, buf.Size)
s.joined = append(s.joined, s.bufs[i].BytesTo(int32(n))...)
rest -= n
}
return s.joined
}
// keep adds to mb what is b and rest bytes after it, in the bufs that follow the one that b is in: nothing is copied,
// the buffers are taken for it and no longer read into
func (s *stack) keep(mb buf.MultiBuffer, b []byte, rest int) buf.MultiBuffer {
// b ends where what was read into the first one does, or before it
first := s.bufs[0]
from := first.Cap() - int32(cap(b))
first.Resize(from, from+int32(len(b)))
mb, s.bufs[0] = append(mb, first), nil
for i := 1; rest > 0; i++ {
n := min(rest, buf.Size)
s.bufs[i].Resize(0, int32(n))
mb, s.bufs[i] = append(mb, s.bufs[i]), nil
rest -= n
}
return mb
}
// local returns the address that packets to remote are sent from // local returns the address that packets to remote are sent from
func (s *stack) local(remote netip.Addr) (netip.Addr, error) { func (s *stack) local(remote netip.Addr) (netip.Addr, error) {
for _, a := range s.addresses { for _, a := range s.addresses {
@@ -316,7 +237,6 @@ func (s *stack) handleTCP(p packet) {
window: binary.BigEndian.Uint16(t[14:]), window: binary.BigEndian.Uint16(t[14:]),
options: t[tcpHeaderSize:size], options: t[tcpHeaderSize:size],
payload: t[size:], payload: t[size:],
rest: p.rest,
} }
s.mu.Lock() s.mu.Lock()
@@ -363,7 +283,7 @@ func (s *stack) refuse(f flow, seg segment) {
binary.BigEndian.PutUint32(t[4:], seg.ack) binary.BigEndian.PutUint32(t[4:], seg.ack)
t[13] = tcpRst t[13] = tcpRst
} else { } else {
ack := seg.seq + uint32(len(seg.payload)+seg.rest) ack := seg.seq + uint32(len(seg.payload))
if seg.flags&(tcpSyn|tcpFin) != 0 { if seg.flags&(tcpSyn|tcpFin) != 0 {
ack++ ack++
} }
+23 -111
View File
@@ -65,20 +65,6 @@ func (l *link) Segments() bool {
return l.segments return l.segments
} }
// ReadParts implements segmenter
func (l *link) ReadParts(parts [][]byte) (int, error) {
select {
case packet := <-l.in:
n := 0
for _, p := range parts {
n += copy(p, packet[n:])
}
return n, nil
case <-l.done:
return 0, io.EOF
}
}
// WriteSegment implements segmenter as the system does when it sends a segment on: a packet for every mss bytes // WriteSegment implements segmenter as the system does when it sends a segment on: a packet for every mss bytes
func (l *link) WriteSegment(header []byte, payload [][]byte, mss int) error { func (l *link) WriteSegment(header []byte, payload [][]byte, mss int) error {
data := bytes.Join(payload, nil) data := bytes.Join(payload, nil)
@@ -220,7 +206,7 @@ func (p *peer) receive() segment {
func (p *peer) dialed() (segment, []byte) { func (p *peer) dialed() (segment, []byte) {
p.t.Helper() p.t.Helper()
b := p.packet() b := p.packet()
packet, ok := parsePacket(b, 0) packet, ok := parsePacket(b)
if !ok || packet.proto != protoTCP || len(packet.payload) < tcpHeaderSize { if !ok || packet.proto != protoTCP || len(packet.payload) < tcpHeaderSize {
p.t.Fatal("not a segment: ", b) p.t.Fatal("not a segment: ", b)
} }
@@ -237,7 +223,7 @@ func (p *peer) segment(b []byte) segment {
} else if checksum(sum(b[:size], 0)) != 0 { } else if checksum(sum(b[:size], 0)) != 0 {
p.t.Fatal("wrong checksum of the ip header") p.t.Fatal("wrong checksum of the ip header")
} }
packet, ok := parsePacket(b, 0) packet, ok := parsePacket(b)
if !ok || packet.proto != protoTCP || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() { if !ok || packet.proto != protoTCP || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() {
p.t.Fatal("not a segment of the connection: ", b) p.t.Fatal("not a segment of the connection: ", b)
} }
@@ -478,90 +464,30 @@ func TestTCPOutOfOrder(t *testing.T) {
read(t, a.conn, "jklmno") read(t, a.conn, "jklmno")
} }
// A segment is as large as the system makes it when the tun does not split what it reads, and one that is large, // A segment is as large as the system makes it when the tun does not split what it reads.
// the next and all taken stays in the buffers that such a tun reads it into.
func TestTCPLargeSegment(t *testing.T) { func TestTCPLargeSegment(t *testing.T) {
for _, segments := range []bool{false, true} {
conns := make(chan accepted, 1) conns := make(chan accepted, 1)
p, l := newPeer(t, "10.0.0.1:1234", "10.0.0.2:80") p, l := newPeer(t, "10.0.0.1:1234", "10.0.0.2:80")
l.segments = segments newTestStack(t, l, accept(conns))
s := newTestStack(t, l, accept(conns))
sent := make([]byte, 200000)
rand.Read(sent)
// one of no connection is refused as a whole
p.send(p.seq, tcpPsh, nil, sent[:20000])
if r := p.receive(); r.flags != tcpRst|tcpAck || r.ack != p.seq+20000 {
t.Fatal("segments: ", segments, ", not refused: flags ", r.flags, ", ack ", r.ack-p.seq, " after its seq")
}
p.connect(nil) p.connect(nil)
a := take(t, conns) a := take(t, conns)
// one that waits for the one before it, that one, one with a part that came already, small ones around sent := make([]byte, 60000)
// one that fits a buffer, and the rest rand.Read(sent)
for _, part := range [][2]int{{60000, 90000}, {0, 60000}, {80000, 110000}, {110000, 110100}, {110100, 115100}, {115100, 118100}, {118100, 170000}, {170000, 200000}} { p.send(p.seq, tcpAck|tcpPsh, nil, sent)
p.send(p.seq+uint32(part[0]), tcpAck|tcpPsh, nil, sent[part[0]:part[1]])
}
// all of it is taken before anything is read, for the buffers to be as the stack put it in them
for r := p.receive(); r.ack != p.seq+uint32(len(sent)); r = p.receive() {
}
var received []byte var received []byte
for len(received) < len(sent) { for len(received) < len(sent) {
mb, err := a.conn.(buf.Reader).ReadMultiBuffer() mb, err := a.conn.(buf.Reader).ReadMultiBuffer()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// a buffer that was read into starts with the headers, one that was copied to is full
if kept := mb[0].Len() == buf.Size-ipv4HeaderSize-tcpHeaderSize; len(received) == 0 && kept != segments {
t.Fatal("segments: ", segments, ", the first buffer has ", mb[0].Len(), " bytes")
}
for _, b := range mb { for _, b := range mb {
// and a small one is copied to where the one before it ends
if b.Len() < 1000 {
t.Fatal("segments: ", segments, ", a buffer of ", b.Len(), " bytes")
}
received = append(received, b.Bytes()...) received = append(received, b.Bytes()...)
} }
buf.ReleaseMulti(mb) buf.ReleaseMulti(mb)
} }
if !bytes.Equal(received, sent) { if !bytes.Equal(received, sent) {
t.Fatal("segments: ", segments, ", read something else than what was sent") t.Fatal("read something else than what was sent")
}
// no more is taken than the window while nobody reads, of the last one a part,
// and what is taken is acknowledged when a quarter of the window is
p.seq += uint32(len(sent))
sent = make([]byte, (int(s.window)/50000+1)*50000)
rand.Read(sent)
for i := 0; i < len(sent); i += 50000 {
p.send(p.seq+uint32(i), tcpAck|tcpPsh, nil, sent[i:i+50000])
}
for acked := p.seq; ; {
r := p.receive()
if taken := int32(r.ack - p.seq); taken > s.window {
t.Fatal("segments: ", segments, ", ", taken, " bytes taken with a window of ", s.window)
} else if taken == s.window {
break
}
// one that acknowledges nothing more tells that the window opened for what was read before
if r.ack != acked && int32(r.ack-acked) < s.window/4 {
t.Fatal("segments: ", segments, ", ", r.ack-acked, " bytes acknowledged with a window of ", s.window)
}
acked = r.ack
}
received = received[:0]
for len(received) < int(s.window) {
mb, err := a.conn.(buf.Reader).ReadMultiBuffer()
if err != nil {
t.Fatal(err)
}
for _, b := range mb {
received = append(received, b.Bytes()...)
}
buf.ReleaseMulti(mb)
}
if !bytes.Equal(received, sent[:s.window]) {
t.Fatal("segments: ", segments, ", read something else than what the window took")
}
} }
} }
@@ -1156,20 +1082,13 @@ func TestTCPSynAfterClose(t *testing.T) {
} }
// A UDP packet starts a connection, which answers from any address, and an echo request is answered. // A UDP packet starts a connection, which answers from any address, and an echo request is answered.
// Both can be larger than a buffer of a tun that reads into more than one.
func TestUDPAndICMP(t *testing.T) { func TestUDPAndICMP(t *testing.T) {
large := make([]byte, 20000) for _, test := range []struct{ src, dst, other string }{
rand.Read(large) {"10.0.0.1:1234", "10.0.0.2:53", "10.0.0.3:5353"},
for _, test := range []struct { {"[fd00::1]:1234", "[fd00::2]:53", "[fd00::3]:5353"},
src, dst, other string
segments bool
}{
{"10.0.0.1:1234", "10.0.0.2:53", "10.0.0.3:5353", false},
{"[fd00::1]:1234", "[fd00::2]:53", "[fd00::3]:5353", true},
} { } {
conns := make(chan accepted, 1) conns := make(chan accepted, 1)
p, l := newPeer(t, test.src, test.dst) p, l := newPeer(t, test.src, test.dst)
l.segments = test.segments
newTestStack(t, l, accept(conns)) newTestStack(t, l, accept(conns))
var b [ipv6HeaderSize + udpHeaderSize]byte var b [ipv6HeaderSize + udpHeaderSize]byte
@@ -1186,13 +1105,6 @@ func TestUDPAndICMP(t *testing.T) {
t.Fatal("read ", mb.String(), " from ", a.conn.RemoteAddr(), " to ", a.dest, ", error: ", err) t.Fatal("read ", mb.String(), " from ", a.conn.RemoteAddr(), " to ", a.dest, ", error: ", err)
} }
buf.ReleaseMulti(mb) buf.ReleaseMulti(mb)
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(large)))
setIPLength(b[:n], n+udpHeaderSize+len(large))
l.in <- append(b[:n+udpHeaderSize:n+udpHeaderSize], large...)
if mb, err = a.conn.(buf.Reader).ReadMultiBuffer(); err != nil || len(mb) != 1 || !bytes.Equal(mb[0].Bytes(), large) {
t.Fatal("read ", mb.Len(), " bytes of a large packet, error: ", err)
}
buf.ReleaseMulti(mb)
for _, from := range []string{test.dst, test.other} { for _, from := range []string{test.dst, test.other} {
answer := buf.FromBytes([]byte("defg")) answer := buf.FromBytes([]byte("defg"))
@@ -1202,7 +1114,7 @@ func TestUDPAndICMP(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
raw := p.packet() raw := p.packet()
packet, ok := parsePacket(raw, 0) packet, ok := parsePacket(raw)
u := packet.payload u := packet.payload
// the system may have to split it on the way to where it goes // the system may have to split it on the way to where it goes
if !ok || raw[0]>>4 == 4 && raw[6] != 0 || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)).String() != from || if !ok || raw[0]>>4 == 4 && raw[6] != 0 || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)).String() != from ||
@@ -1221,12 +1133,12 @@ func TestUDPAndICMP(t *testing.T) {
proto, request, reply = protoICMPv6, 128, 129 proto, request, reply = protoICMPv6, 128, 129
} }
n = putIPHeader(b[:], proto, p.src.Addr(), p.dst.Addr()) n = putIPHeader(b[:], proto, p.src.Addr(), p.dst.Addr())
setIPLength(b[:n], n+8+len(large)) setIPLength(b[:n], n+12)
l.in <- append(append(b[:n:n], request, 0, 0, 0, 1, 2, 3, 4), large...) l.in <- append(b[:n:n], request, 0, 0, 0, 1, 2, 3, 4, 'a', 'b', 'c', 'd')
packet, ok := parsePacket(p.joined(), 0) packet, ok := parsePacket(p.packet())
m := packet.payload m := packet.payload
if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || string(m[4:8]) != "\x01\x02\x03\x04" || !bytes.Equal(m[8:], large) { if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || string(m[4:]) != "\x01\x02\x03\x04abcd" {
t.Fatal("wrong echo reply of ", len(m), " bytes from ", packet.src, " to ", packet.dst) t.Fatal("wrong echo reply: ", packet)
} }
var ip [ipv6HeaderSize]byte var ip [ipv6HeaderSize]byte
size := putIPHeader(ip[:], proto, packet.src, packet.dst) size := putIPHeader(ip[:], proto, packet.src, packet.dst)
@@ -1461,7 +1373,7 @@ func TestUDPDial(t *testing.T) {
t.Fatal("wrong header of the packet to ", to, ": ", raw[:ipv4HeaderSize]) t.Fatal("wrong header of the packet to ", to, ": ", raw[:ipv4HeaderSize])
} }
numbers[to][binary.BigEndian.Uint16(raw[4:])] = true numbers[to][binary.BigEndian.Uint16(raw[4:])] = true
packet, ok := parsePacket(raw, 0) packet, ok := parsePacket(raw)
u := packet.payload u := packet.payload
if !ok || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != local || if !ok || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != local ||
netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != to || string(u[udpHeaderSize:]) != "abc" { netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != to || string(u[udpHeaderSize:]) != "abc" {
@@ -1483,7 +1395,7 @@ func TestUDPDial(t *testing.T) {
if _, err := conn.WriteTo([]byte("abc"), &net.UDPAddr{IP: across.Addr().AsSlice(), Port: int(across.Port())}); err != nil { if _, err := conn.WriteTo([]byte("abc"), &net.UDPAddr{IP: across.Addr().AsSlice(), Port: int(across.Port())}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.proto != protoUDP || packet.dst != across.Addr() || if packet, ok := parsePacket(p.packet()); !ok || packet.proto != protoUDP || packet.dst != across.Addr() ||
netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(packet.payload)) != acrossLocal { netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(packet.payload)) != acrossLocal {
t.Fatal("wrong packet to ", across, ": ", packet) t.Fatal("wrong packet to ", across, ": ", packet)
} }
@@ -1495,7 +1407,7 @@ func TestUDPDial(t *testing.T) {
// a stack that takes no connections is one host: it answers a ping of its address and of no other // a stack that takes no connections is one host: it answers a ping of its address and of no other
l.in <- ping(p.src.Addr(), netip.MustParseAddrPort(test.other).Addr()) l.in <- ping(p.src.Addr(), netip.MustParseAddrPort(test.other).Addr())
l.in <- ping(p.src.Addr(), local.Addr()) l.in <- ping(p.src.Addr(), local.Addr())
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.src != local.Addr() || packet.dst != p.src.Addr() || packet.payload[0]&^129 != 0 { if packet, ok := parsePacket(p.packet()); !ok || packet.src != local.Addr() || packet.dst != p.src.Addr() || packet.payload[0]&^129 != 0 {
t.Fatal("wrong echo reply: ", packet) t.Fatal("wrong echo reply: ", packet)
} }
p.silent(20 * time.Millisecond) p.silent(20 * time.Millisecond)
@@ -1961,7 +1873,7 @@ func TestFragments(t *testing.T) {
if err := a.conn.(buf.Writer).WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(sent)}); err != nil { if err := a.conn.(buf.Writer).WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(sent)}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
packet, ok := parsePacket(p.joined(), 0) packet, ok := parsePacket(p.joined())
u := packet.payload u := packet.payload
if !ok || packet.fragment || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != p.dst || if !ok || packet.fragment || packet.proto != protoUDP || netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(u)) != p.dst ||
netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != p.src || int(binary.BigEndian.Uint16(u[4:])) != len(u) || !bytes.Equal(u[udpHeaderSize:], sent) { netip.AddrPortFrom(packet.dst, binary.BigEndian.Uint16(u[2:])) != p.src || int(binary.BigEndian.Uint16(u[4:])) != len(u) || !bytes.Equal(u[udpHeaderSize:], sent) {
@@ -1986,7 +1898,7 @@ func TestFragments(t *testing.T) {
mixed := split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{1}, 2000))), 1024, 11) mixed := split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{1}, 2000))), 1024, 11)
l.in <- mixed[0] l.in <- mixed[0]
l.in <- split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{2}, 2000))), 1024, 11)[1] l.in <- split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{2}, 2000))), 1024, 11)[1]
packet, ok = parsePacket(p.joined(), 0) packet, ok = parsePacket(p.joined())
m = packet.payload m = packet.payload
if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || !bytes.Equal(m[8:], sent) { if !ok || packet.proto != proto || packet.src != p.dst.Addr() || packet.dst != p.src.Addr() || m[0] != reply || !bytes.Equal(m[8:], sent) {
t.Fatal("wrong echo reply: ", packet.src, packet.dst, packet.proto, len(m)) t.Fatal("wrong echo reply: ", packet.src, packet.dst, packet.proto, len(m))
@@ -2128,7 +2040,7 @@ func TestChecksumsAndLoopback(t *testing.T) {
l.in <- damaged(echo) l.in <- damaged(echo)
p.silent(50 * time.Millisecond) p.silent(50 * time.Millisecond)
l.in <- echo l.in <- echo
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.proto != proto || string(packet.payload[4:]) != "\x01\x02\x03\x04abcd" { if packet, ok := parsePacket(p.packet()); !ok || packet.proto != proto || string(packet.payload[4:]) != "\x01\x02\x03\x04abcd" {
t.Fatal("wrong echo reply: ", packet) t.Fatal("wrong echo reply: ", packet)
} }
+3 -19
View File
@@ -49,9 +49,6 @@ const (
// tcpParts of how many parts of what was written it is made at most // tcpParts of how many parts of what was written it is made at most
tcpSegment = math.MaxUint16 - ipv6HeaderSize - tcpHeaderSize tcpSegment = math.MaxUint16 - ipv6HeaderSize - tcpHeaderSize
tcpParts = 7 tcpParts = 7
// tcpKept is how large a payload is at least for the buffers that it was read into to be kept. A smaller one
// is copied to where the one before it ends, for a reader to get buffers that are full
tcpKept = buf.Size / 2
// tcpTick is how often a connection that waits for something looks at what it waits for // tcpTick is how often a connection that waits for something looks at what it waits for
tcpTick = 100 * time.Millisecond tcpTick = 100 * time.Millisecond
@@ -90,9 +87,7 @@ type segment struct {
flags byte flags byte
window uint16 window uint16
options []byte options []byte
// payload is without its last rest bytes when those are in other buffers of the stack, see stack.whole
payload []byte payload []byte
rest int
} }
// nextOption returns the first option of o and what follows it, kind is zero when there is none // nextOption returns the first option of o and what follows it, kind is zero when there is none
@@ -754,12 +749,6 @@ func (c *tcpConn) space() uint32 {
// receive takes the payload of a segment. c.mu is held // receive takes the payload of a segment. c.mu is held
func (c *tcpConn) receive(s segment) { func (c *tcpConn) receive(s segment) {
// what is large, the next and all taken stays in the buffers that it was read into
size := len(s.payload) + s.rest
keep := c.stack.bufs != nil && size >= tcpKept && s.seq == c.rcvNxt && size <= int(c.window-c.rcvLen)
if !keep {
s.payload = c.stack.whole(s.payload, s.rest)
}
seq, payload, fin := s.seq, s.payload, s.flags&tcpFin != 0 seq, payload, fin := s.seq, s.payload, s.flags&tcpFin != 0
if len(payload) == 0 && !fin { if len(payload) == 0 && !fin {
if int32(c.rcvNxt-seq) > 0 { if int32(c.rcvNxt-seq) > 0 {
@@ -800,14 +789,9 @@ func (c *tcpConn) receive(s segment) {
return return
} }
if keep {
c.rcvQueue = c.stack.keep(c.rcvQueue, payload, s.rest)
} else {
size = len(payload)
c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload) c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload)
} c.rcvLen += int32(len(payload))
c.rcvLen += int32(size) c.rcvNxt += uint32(len(payload))
c.rcvNxt += uint32(size)
for !fin && len(c.rcvHeld) > 0 && int32(c.rcvNxt-c.rcvHeld[0].seq) >= 0 { for !fin && len(c.rcvHeld) > 0 && int32(c.rcvNxt-c.rcvHeld[0].seq) >= 0 {
h := c.rcvHeld[0] h := c.rcvHeld[0]
if before := int32(c.rcvNxt - h.seq); before < h.data.Len() { if before := int32(c.rcvNxt - h.seq); before < h.data.Len() {
@@ -825,7 +809,7 @@ func (c *tcpConn) receive(s segment) {
c.readable.Signal() c.readable.Signal()
// the reader acknowledges what it takes, with one segment for all that came by then. // the reader acknowledges what it takes, with one segment for all that came by then.
// It is not waited for when it does not read for long // It is not waited for when it does not read for long
if fin || now || c.rcvNxt-c.rcvAcked >= uint32(c.window/4) { if fin || now || c.rcvNxt-c.rcvAcked >= tcpWindow/4 {
c.sendAck() c.sendAck()
} else { } else {
c.arm(tcpTick) c.arm(tcpTick)
+2 -5
View File
@@ -16,13 +16,10 @@ type Tun interface {
} }
// segmenter is a tun whose other side takes TCP segments that are larger than the MTU, // segmenter is a tun whose other side takes TCP segments that are larger than the MTU,
// splits them only if it has to send them on, and completes their checksum then. // splits them only if it has to send them on, and completes their checksum then
// What it sends comes as it is too, not split to the MTU
type segmenter interface { type segmenter interface {
// Segments reports whether ReadParts and WriteSegment can be used // Segments reports whether WriteSegment can be used
Segments() bool Segments() bool
// ReadParts waits for a packet, reads it into parts, one after the other, and returns its size
ReadParts(parts [][]byte) (int, error)
// WriteSegment writes a packet with a TCP segment whose payload is in parts, to be sent on with mss bytes // WriteSegment writes a packet with a TCP segment whose payload is in parts, to be sent on with mss bytes
// of it at most in one. Its checksum is that of the pseudo-header only, not inverted // of it at most in one. Its checksum is that of the pseudo-header only, not inverted
WriteSegment(header []byte, payload [][]byte, mss int) error WriteSegment(header []byte, payload [][]byte, mss int) error
+1 -43
View File
@@ -6,8 +6,6 @@ import (
"encoding/binary" "encoding/binary"
"os" "os"
"sync" "sync"
"syscall"
"unsafe"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -24,14 +22,6 @@ type fdTun struct {
// vnet is the size of the virtio_net_hdr in front of every packet, zero if the tun has none. // vnet is the size of the virtio_net_hdr in front of every packet, zero if the tun has none.
// With it the system does not split what it sends to the tun and takes what is not split from it // With it the system does not split what it sends to the tun and takes what is not split from it
vnet int vnet int
// raw is file for ReadParts, readv what it calls when there may be a packet, iovecs where that reads to:
// header, which takes the virtio_net_hdr, and the parts. read and readErr are what came of it
raw syscall.RawConn
readv func(fd uintptr) bool
header [packetRoom]byte
iovecs []unix.Iovec
read int
readErr error
writeLock sync.RWMutex writeLock sync.RWMutex
closed bool closed bool
@@ -39,25 +29,7 @@ type fdTun struct {
// newFdTun takes a file descriptor that does not block // newFdTun takes a file descriptor that does not block
func newFdTun(fd, vnet int) *fdTun { func newFdTun(fd, vnet int) *fdTun {
t := &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet} return &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet}
t.raw, _ = t.file.SyscallConn()
t.readv = func(fd uintptr) bool {
for {
n, _, errno := unix.Syscall(unix.SYS_READV, fd, uintptr(unsafe.Pointer(&t.iovecs[0])), uintptr(len(t.iovecs)))
switch errno {
case 0:
t.read, t.readErr = int(n), nil
case unix.EINTR:
continue
case unix.EAGAIN:
return false
default:
t.read, t.readErr = 0, errno
}
return true
}
}
return t
} }
func (t *fdTun) ReadPacket(b []byte) ([]byte, error) { func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
@@ -66,20 +38,6 @@ func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
return b[min(t.vnet, n):n], err return b[min(t.vnet, n):n], err
} }
// ReadParts implements segmenter
func (t *fdTun) ReadParts(parts [][]byte) (int, error) {
t.iovecs = append(t.iovecs[:0], unix.Iovec{Base: &t.header[0]})
t.iovecs[0].SetLen(t.vnet)
for i, p := range parts {
t.iovecs = append(t.iovecs, unix.Iovec{Base: &p[0]})
t.iovecs[i+1].SetLen(len(p))
}
if err := t.raw.Read(t.readv); err != nil {
return 0, err
}
return max(t.read-t.vnet, 0), t.readErr
}
func (t *fdTun) WritePacket(header, payload []byte) error { func (t *fdTun) WritePacket(header, payload []byte) error {
// a virtio_net_hdr of zeros leaves nothing to the system // a virtio_net_hdr of zeros leaves nothing to the system
var vnet [packetRoom]byte var vnet [packetRoom]byte
-66
View File
@@ -1,66 +0,0 @@
//go:build linux
package tun
import (
"bytes"
"crypto/rand"
"testing"
"time"
"golang.org/x/sys/unix"
)
// A tun with a virtio_net_hdr reads one packet at a time into parts, without the header, waits for the next one,
// and stops waiting when it is closed.
func TestReadParts(t *testing.T) {
// what a tun is like: one packet for every read, and a file descriptor that does not block
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
if err != nil {
t.Fatal(err)
}
defer unix.Close(fds[1])
tun := newFdTun(fds[0], vnetSize)
defer tun.closeFile()
large := make([]byte, 20000)
rand.Read(large)
parts := [][]byte{make([]byte, 8192), make([]byte, 8192), make([]byte, 8192)}
send := func(packet []byte) {
if _, err := unix.Write(fds[1], append(bytes.Repeat([]byte{0xee}, vnetSize), packet...)); err != nil {
t.Error(err)
}
}
send(large)
send([]byte("small"))
for _, packet := range [][]byte{large, []byte("small")} {
n, err := tun.ReadParts(parts)
if err != nil || n != len(packet) || !bytes.Equal(bytes.Join(parts, nil)[:n], packet) {
t.Fatal("read ", n, " bytes of ", len(packet), ", error: ", err)
}
}
go func() {
time.Sleep(50 * time.Millisecond)
send([]byte("later"))
}()
if n, err := tun.ReadParts(parts); err != nil || string(parts[0][:n]) != "later" {
t.Fatal("read ", parts[0][:n], " after waiting, error: ", err)
}
read := make(chan error)
go func() {
_, err := tun.ReadParts(parts)
read <- err
}()
time.Sleep(50 * time.Millisecond)
tun.closeFile()
select {
case err := <-read:
if err == nil {
t.Fatal("read after the tun was closed")
}
case <-time.After(5 * time.Second):
t.Fatal("still waits after the tun was closed")
}
}
-87
View File
@@ -16,10 +16,8 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/dokodemo"
"github.com/xtls/xray-core/proxy/freedom" "github.com/xtls/xray-core/proxy/freedom"
v2http "github.com/xtls/xray-core/proxy/http" v2http "github.com/xtls/xray-core/proxy/http"
v2httptest "github.com/xtls/xray-core/testing/servers/http" v2httptest "github.com/xtls/xray-core/testing/servers/http"
@@ -379,88 +377,3 @@ func TestHttpBasicAuth(t *testing.T) {
} }
} }
} }
// Server-first protocols (SMTP, FTP, MySQL) send nothing from the client until the server's
// greeting arrives, so the HTTP outbound must not wait for a first payload before dialing.
func TestHTTPOutboundServerFirst(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
SendFirst: []byte("send first"),
}
dest, err := tcpServer.Start()
common.Must(err)
defer tcpServer.Close()
serverPort := tcp.PickPort()
serverConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
},
},
}
clientPort := tcp.PickPort()
clientConfig := &core.Config{
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{net.Network_TCP},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&v2http.ClientConfig{
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(serverPort),
},
}),
},
},
}
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
common.Must(err)
defer CloseAllServers(servers)
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{
IP: []byte{127, 0, 0, 1},
Port: int(clientPort),
})
common.Must(err)
defer conn.Close()
common.Must(conn.SetReadDeadline(time.Now().Add(5 * time.Second)))
response := make([]byte, 1024)
nBytes, err := conn.Read(response)
if err != nil {
t.Fatal("no server greeting through HTTP outbound: ", err)
}
if string(response[:nBytes]) != "send first" {
t.Fatal("unexpected first response: ", string(response[:nBytes]))
}
common.Must(conn.SetReadDeadline(time.Time{}))
if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil {
t.Error(err)
}
}
@@ -1,63 +0,0 @@
package filter
import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/finalmask"
)
func (c *Config) buildFilter() (*filter, error) {
if len(c.BlockedSourceIps) == 0 && len(c.AllowedSourceIps) == 0 {
return nil, nil
}
f := new(filter)
var err error
if len(c.BlockedSourceIps) > 0 {
f.blocked, err = geodata.IPReg.BuildIPMatcher(c.BlockedSourceIps)
if err != nil {
return nil, err
}
}
if len(c.AllowedSourceIps) > 0 {
f.allowed, err = geodata.IPReg.BuildIPMatcher(c.AllowedSourceIps)
if err != nil {
return nil, err
}
}
return f, nil
}
func (c *Config) Listen(inner net.Listener) (net.Listener, error) {
f, err := c.buildFilter()
if err != nil {
return nil, err
}
if f == nil {
return inner, nil
}
return &listener{Listener: inner, filter: f}, nil
}
func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
return conn, nil
}
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
// TCP filtering is installed by Listen, before per-connection masks.
return conn, nil
}
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
return conn, nil
}
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
f, err := c.buildFilter()
if err != nil {
return nil, err
}
if f == nil {
return conn, nil
}
return &packetConn{PacketConn: conn, filter: f}, nil
}
@@ -1,136 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: transport/internet/finalmask/filter/config.proto
package filter
import (
geodata "github.com/xtls/xray-core/common/geodata"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
BlockedSourceIps []*geodata.IPRule `protobuf:"bytes,1,rep,name=blocked_source_ips,json=blockedSourceIps,proto3" json:"blocked_source_ips,omitempty"`
AllowedSourceIps []*geodata.IPRule `protobuf:"bytes,2,rep,name=allowed_source_ips,json=allowedSourceIps,proto3" json:"allowed_source_ips,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_finalmask_filter_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_finalmask_filter_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_filter_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetBlockedSourceIps() []*geodata.IPRule {
if x != nil {
return x.BlockedSourceIps
}
return nil
}
func (x *Config) GetAllowedSourceIps() []*geodata.IPRule {
if x != nil {
return x.AllowedSourceIps
}
return nil
}
var File_transport_internet_finalmask_filter_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_filter_config_proto_rawDesc = "" +
"\n" +
"0transport/internet/finalmask/filter/config.proto\x12(xray.transport.internet.finalmask.filter\x1a\x1bcommon/geodata/geodat.proto\"\x9e\x01\n" +
"\x06Config\x12I\n" +
"\x12blocked_source_ips\x18\x01 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x10blockedSourceIps\x12I\n" +
"\x12allowed_source_ips\x18\x02 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x10allowedSourceIpsB\x9a\x01\n" +
",com.xray.transport.internet.finalmask.filterP\x01Z=github.com/xtls/xray-core/transport/internet/finalmask/filter\xaa\x02(Xray.Transport.Internet.Finalmask.Filterb\x06proto3"
var (
file_transport_internet_finalmask_filter_config_proto_rawDescOnce sync.Once
file_transport_internet_finalmask_filter_config_proto_rawDescData []byte
)
func file_transport_internet_finalmask_filter_config_proto_rawDescGZIP() []byte {
file_transport_internet_finalmask_filter_config_proto_rawDescOnce.Do(func() {
file_transport_internet_finalmask_filter_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_filter_config_proto_rawDesc), len(file_transport_internet_finalmask_filter_config_proto_rawDesc)))
})
return file_transport_internet_finalmask_filter_config_proto_rawDescData
}
var file_transport_internet_finalmask_filter_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_finalmask_filter_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.finalmask.filter.Config
(*geodata.IPRule)(nil), // 1: xray.common.geodata.IPRule
}
var file_transport_internet_finalmask_filter_config_proto_depIdxs = []int32{
1, // 0: xray.transport.internet.finalmask.filter.Config.blocked_source_ips:type_name -> xray.common.geodata.IPRule
1, // 1: xray.transport.internet.finalmask.filter.Config.allowed_source_ips:type_name -> xray.common.geodata.IPRule
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_transport_internet_finalmask_filter_config_proto_init() }
func file_transport_internet_finalmask_filter_config_proto_init() {
if File_transport_internet_finalmask_filter_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_filter_config_proto_rawDesc), len(file_transport_internet_finalmask_filter_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_finalmask_filter_config_proto_goTypes,
DependencyIndexes: file_transport_internet_finalmask_filter_config_proto_depIdxs,
MessageInfos: file_transport_internet_finalmask_filter_config_proto_msgTypes,
}.Build()
File_transport_internet_finalmask_filter_config_proto = out.File
file_transport_internet_finalmask_filter_config_proto_goTypes = nil
file_transport_internet_finalmask_filter_config_proto_depIdxs = nil
}
@@ -1,14 +0,0 @@
syntax = "proto3";
package xray.transport.internet.finalmask.filter;
option csharp_namespace = "Xray.Transport.Internet.Finalmask.Filter";
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/filter";
option java_package = "com.xray.transport.internet.finalmask.filter";
option java_multiple_files = true;
import "common/geodata/geodat.proto";
message Config {
repeated xray.common.geodata.IPRule blocked_source_ips = 1;
repeated xray.common.geodata.IPRule allowed_source_ips = 2;
}
@@ -1,69 +0,0 @@
package filter
import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
)
type listener struct {
net.Listener
filter *filter
}
// Accept filters connections after the TCP handshake and before higher-level processing.
func (l *listener) Accept() (net.Conn, error) {
for {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
if l.filter.rejects(conn.RemoteAddr()) {
conn.Close()
continue
}
return conn, nil
}
}
type packetConn struct {
net.PacketConn
filter *filter
}
func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) {
for {
n, addr, err := c.PacketConn.ReadFrom(p)
// Reads may return both data and an error; filter the data first.
if c.filter.rejects(addr) {
if err != nil {
return 0, nil, err
}
continue
}
return n, addr, err
}
}
type filter struct {
blocked geodata.IPMatcher
allowed geodata.IPMatcher
}
func (f *filter) rejects(addr net.Addr) bool {
var ip net.IP
switch addr := addr.(type) {
case *net.TCPAddr:
ip = addr.IP
case *net.UDPAddr:
ip = addr.IP
case *net.IPAddr:
ip = addr.IP
default:
// Unknown sources cannot satisfy an allowlist.
return f.allowed != nil
}
if f.blocked != nil && f.blocked.Match(ip) {
return true
}
return f.allowed != nil && !f.allowed.Match(ip)
}
@@ -1,311 +0,0 @@
package filter
import (
"context"
"errors"
"io"
"net"
"testing"
"time"
"github.com/pires/go-proxyproto"
"github.com/xtls/xray-core/common/geodata"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/finalmask"
)
func TestTCPFilterBeforeOtherMasks(t *testing.T) {
for _, policy := range testPolicies(t) {
t.Run(policy.name, func(t *testing.T) {
blocked4 := sourceConn("192.0.2.1")
blocked6 := sourceConn("2001:db8::1")
unknown := &testConn{addr: &net.UnixAddr{Name: "filter.sock", Net: "unix"}}
missing := new(testConn)
allowed := sourceConn("198.51.100.1")
allowed6 := sourceConn("2001:db8:1::1")
raw := &testListener{conns: []net.Conn{blocked4, blocked6, unknown, missing, allowed, allowed6}}
spy := new(recordMask)
fm := finalmask.NewFinalMask([]finalmask.TCPMask{spy, policy.config}, nil, nil,
func(context.Context, net.Addr) (net.Listener, error) { return raw, nil }, nil, nil)
l, err := fm.Listen(context.Background(), &net.TCPAddr{})
if err != nil {
t.Fatal(err)
}
defer l.Close()
whitelist := len(policy.config.AllowedSourceIps) > 0
want := []net.Conn{allowed, allowed6}
if !whitelist {
want = append([]net.Conn{unknown, missing}, want...)
}
for _, expected := range want {
if got, err := l.Accept(); err != nil || got != expected {
t.Fatalf("Accept() = (%v, %v), want %v", got, err, expected)
}
}
// Fake sockets panic on Read or Write. Rejection must not require handshake data.
if !blocked4.closed || !blocked6.closed || unknown.closed != whitelist || missing.closed != whitelist || allowed.closed || allowed6.closed || raw.closed {
t.Fatal("filter did not close only the blocked connections")
}
if len(spy.seen) != len(want) {
t.Fatal("blocked connections reached the next mask")
}
for i, conn := range spy.seen {
if conn != want[i] {
t.Fatal("unexpected connection reached the next mask")
}
}
if conn, err := l.Accept(); conn != nil || !errors.Is(err, io.EOF) {
t.Fatalf("listener error was not propagated: (%v, %v)", conn, err)
}
})
}
}
func TestTCPFilterUsesProxyHeader(t *testing.T) {
for _, test := range []struct {
name, peer, reported string
blocked bool
}{
{"blocked_header", "198.51.100.1", "192.0.2.1", true},
{"allowed_header", "192.0.2.1", "198.51.100.1", false},
} {
t.Run(test.name, func(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
if err := server.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
t.Fatal(err)
}
peer := sourceConn(test.peer)
peer.Conn = server
conn := proxyproto.NewConn(peer)
next := sourceConn("198.51.100.2")
l, err := testConfig(t, "192.0.2.0/24").Listen(&testListener{conns: []net.Conn{conn, next}})
if err != nil {
t.Fatal(err)
}
defer l.Close()
written := make(chan error, 1)
go func() {
_, err := client.Write([]byte("PROXY TCP4 " + test.reported + " 203.0.113.1 1234 443\r\n"))
written <- err
}()
got, err := l.Accept()
if err != nil {
t.Fatal(err)
}
if err := <-written; err != nil {
t.Fatal(err)
}
var want net.Conn = conn
if test.blocked {
want = next
}
if got != want || peer.closed != test.blocked {
t.Fatal("filter did not follow the address supplied by PROXY protocol")
}
})
}
}
func TestUDPFilter(t *testing.T) {
for _, policy := range testPolicies(t) {
t.Run(policy.name, func(t *testing.T) {
allowed := &net.UDPAddr{IP: net.ParseIP("198.51.100.1"), Port: 1234}
allowed6 := &net.UDPAddr{IP: net.ParseIP("2001:db8:1::1"), Port: 1234}
unknown := datagram{data: []byte("unknown"), addr: &net.UnixAddr{Name: "filter.sock", Net: "unixgram"}}
missing := datagram{data: []byte("missing address")}
raw := &testPacketConn{reads: []datagram{
{data: []byte("blocked"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}},
{addr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1234}},
unknown,
missing,
{addr: allowed},
{data: []byte("allowed"), addr: allowed},
{data: []byte("allowed6"), addr: allowed6},
{data: []byte("blocked again"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.2"), Port: 1234}},
}}
fm := finalmask.NewFinalMask(nil, []finalmask.UDPMask{policy.config}, nil, nil, nil,
func(context.Context, net.Addr) (net.PacketConn, error) { return raw, nil })
conn, err := fm.ListenPacket(context.Background(), &net.UDPAddr{})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
buf := make([]byte, 64)
if len(policy.config.AllowedSourceIps) == 0 {
for _, expected := range []datagram{unknown, missing} {
n, addr, err := conn.ReadFrom(buf)
if err != nil || addr != expected.addr || string(buf[:n]) != string(expected.data) {
t.Fatalf("unknown source was not allowed by blacklist: (%q, %v, %v)", buf[:n], addr, err)
}
}
}
if n, addr, err := conn.ReadFrom(buf); n != 0 || addr != allowed || err != nil {
t.Fatalf("zero-length allowed datagram was lost: (%d, %v, %v)", n, addr, err)
}
n, addr, err := conn.ReadFrom(buf)
if err != nil || addr != allowed || string(buf[:n]) != "allowed" {
t.Fatalf("ReadFrom() = (%q, %v, %v)", buf[:n], addr, err)
}
n, addr, err = conn.ReadFrom(buf)
if err != nil || addr != allowed6 || string(buf[:n]) != "allowed6" {
t.Fatalf("IPv6 ReadFrom() = (%q, %v, %v)", buf[:n], addr, err)
}
if _, _, err := conn.ReadFrom(buf); !errors.Is(err, io.EOF) {
t.Fatalf("underlying read error was not propagated: %v", err)
}
if raw.closed {
t.Fatal("dropping a packet closed the shared UDP socket")
}
})
}
}
func TestEmptyFilter(t *testing.T) {
for _, empty := range []*Config{{}, {AllowedSourceIps: []*geodata.IPRule{}}} {
rawListener := new(testListener)
rawPacket := new(testPacketConn)
if l, err := empty.Listen(rawListener); err != nil || l != rawListener {
t.Fatal("an empty TCP filter should preserve the listener")
}
if conn, err := empty.WrapPacketConnServer(rawPacket, nil, nil); err != nil || conn != rawPacket {
t.Fatal("an empty UDP filter should preserve the socket")
}
}
}
func TestUDPFilterReadError(t *testing.T) {
for _, policy := range testPolicies(t) {
t.Run(policy.name, func(t *testing.T) {
for _, test := range []struct {
name string
source net.Addr
blocked bool
}{
{"blocked", &net.UDPAddr{IP: net.ParseIP("192.0.2.1")}, true},
{"allowed", &net.UDPAddr{IP: net.ParseIP("198.51.100.1")}, false},
{"missing address", nil, len(policy.config.AllowedSourceIps) > 0},
{"unknown address", &net.UnixAddr{Name: "filter.sock", Net: "unixgram"}, len(policy.config.AllowedSourceIps) > 0},
} {
t.Run(test.name, func(t *testing.T) {
source := test.source
raw := &testPacketConn{reads: []datagram{{data: []byte("partial"), addr: source, err: io.ErrShortBuffer}}}
conn, err := policy.config.WrapPacketConnServer(raw, nil, nil)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 32)
n, addr, err := conn.ReadFrom(buf)
if !errors.Is(err, io.ErrShortBuffer) {
t.Fatalf("read error was not preserved: %v", err)
}
if test.blocked {
if n != 0 || addr != nil {
t.Fatal("partial data from a blocked source was returned with an error")
}
} else if n != 7 || addr != source || string(buf[:n]) != "partial" {
t.Fatal("partial data from an unblocked source was changed")
}
})
}
})
}
}
func testPolicies(t *testing.T) []struct {
name string
config *Config
} {
t.Helper()
blocked := testConfig(t, "192.0.2.0/24", "2001:db8::/48").BlockedSourceIps
allowed := testConfig(t, "198.51.100.0/24", "2001:db8:1::/48").BlockedSourceIps
broader := testConfig(t, "192.0.2.0/24", "198.51.100.0/24", "2001:db8::/32").BlockedSourceIps
return []struct {
name string
config *Config
}{
{"blacklist", &Config{BlockedSourceIps: blocked}},
{"whitelist", &Config{AllowedSourceIps: allowed}},
{"both", &Config{BlockedSourceIps: blocked, AllowedSourceIps: broader}},
}
}
func testConfig(t *testing.T, entries ...string) *Config {
t.Helper()
rules, err := geodata.ParseIPRules(entries)
if err != nil {
t.Fatal(err)
}
return &Config{BlockedSourceIps: rules}
}
type testConn struct {
net.Conn
addr net.Addr
closed bool
}
func sourceConn(ip string) *testConn {
return &testConn{addr: &net.TCPAddr{IP: net.ParseIP(ip), Port: 1234}}
}
func (c *testConn) RemoteAddr() net.Addr { return c.addr }
func (c *testConn) Close() error {
c.closed = true
if c.Conn != nil {
return c.Conn.Close()
}
return nil
}
type testListener struct {
net.Listener
conns []net.Conn
closed bool
}
func (l *testListener) Accept() (net.Conn, error) {
if len(l.conns) == 0 {
return nil, io.EOF
}
conn := l.conns[0]
l.conns = l.conns[1:]
return conn, nil
}
func (l *testListener) Close() error { l.closed = true; return nil }
type datagram struct {
data []byte
addr net.Addr
err error
}
type testPacketConn struct {
net.PacketConn
reads []datagram
closed bool
}
func (c *testPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
if len(c.reads) == 0 {
return 0, nil, io.EOF
}
packet := c.reads[0]
c.reads = c.reads[1:]
return copy(p, packet.data), packet.addr, packet.err
}
func (c *testPacketConn) Close() error { c.closed = true; return nil }
type recordMask struct{ seen []net.Conn }
func (m *recordMask) WrapConnClient(c net.Conn, _ *xnet.Destination, _ *finalmask.Dialer) (net.Conn, error) {
return c, nil
}
func (m *recordMask) WrapConnServer(c net.Conn) (net.Conn, error) {
m.seen = append(m.seen, c)
return c, nil
}
+1 -1
View File
@@ -68,7 +68,7 @@ func KeyLogWriterFromConfig(c *Config) io.Writer {
return nil return nil
} }
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600) writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644)
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log")
} }
+2 -4
View File
@@ -69,10 +69,9 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
} }
var lc net.ListenConfig var lc net.ListenConfig
if sockopt != nil || len(Controllers) > 0 { if sockopt != nil || len(Controllers) > 0 {
lc.Control = func(network, _ string, c syscall.RawConn) error { lc.Control = func(network, address string, c syscall.RawConn) error {
// ListenPacket supplies a local address; dialer controllers need the destination.
for _, ctl := range Controllers { for _, ctl := range Controllers {
if err := ctl(network, destAddr.String(), c); err != nil { if err := ctl(network, address, c); err != nil {
return errors.New("failed to apply external controller").Base(err) return errors.New("failed to apply external controller").Base(err)
} }
} }
@@ -198,7 +197,6 @@ func UseAlternativeSystemDialer(dialer SystemDialer) {
// RegisterDialerController adds a controller to the effective system dialer. // RegisterDialerController adds a controller to the effective system dialer.
// The controller can be used to operate on file descriptors before they are put into use. // The controller can be used to operate on file descriptors before they are put into use.
// It receives the network and resolved remote address, as in net.Dialer.Control.
// It only works when effective dialer is the default dialer. // It only works when effective dialer is the default dialer.
// //
// xray:api:beta // xray:api:beta
+1 -1
View File
@@ -466,7 +466,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
} }
if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" { if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" {
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600) writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644)
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log")
} else { } else {
+12 -270
View File
@@ -2,25 +2,14 @@ package xdrive
import ( import (
"context" "context"
gotls "crypto/tls"
"io" "io"
"net/http" "net/http"
"reflect"
"runtime"
"slices"
"sync" "sync"
"time" "time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
) )
@@ -50,10 +39,6 @@ func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
cancel(nil) cancel(nil)
} }
r = r.WithContext(ctx) r = r.WithContext(ctx)
// the headers of chrome as with XHTTP and DoH, net/http and http3 would send
// their own user agent after a hello of chrome
r.Header = r.Header.Clone()
utils.TryDefaultHeadersWith(r.Header, "fetch")
if r.Body != nil && r.Body != http.NoBody { if r.Body != nil && r.Body != http.NoBody {
r.Body = &progressBody{ReadCloser: r.Body, alive: alive} r.Body = &progressBody{ReadCloser: r.Body, alive: alive}
} }
@@ -119,40 +104,25 @@ func (b *progressBody) Close() error {
} }
// newServiceClient returns a client whose requests end after stall without // newServiceClient returns a client whose requests end after stall without
// progress, see serviceTransport.RoundTrip, and whose connections close when // progress, see serviceTransport.RoundTrip
// connCtx is done. Clients that share h1Hosts share which hosts answer over h1 func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
var ( var (
tlsConfig *tls.Config tlsConfig *tls.Config
sockopt *internet.SocketConfig sockopt *internet.SocketConfig
fronting *net.Destination fronting *net.Destination
keepAlive = net.ChromeH2KeepAlivePeriod
// h3 keeps alive only when asked to, see below
h3KeepAlive time.Duration
) )
if streamSettings != nil { if streamSettings != nil {
tlsConfig = tls.ConfigFromStreamSettings(streamSettings) tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
sockopt = streamSettings.SocketSettings sockopt = streamSettings.SocketSettings
fronting = streamSettings.Destination fronting = streamSettings.Destination
if config, ok := streamSettings.ProtocolSettings.(*Config); ok && config.Xmux != nil {
switch period := config.Xmux.HKeepAlivePeriod; {
case period > 0:
keepAlive = time.Duration(period) * time.Second
h3KeepAlive = keepAlive
case period < 0:
keepAlive = 0
}
}
} }
if tlsConfig == nil { if tlsConfig == nil {
tlsConfig = &tls.Config{} tlsConfig = &tls.Config{}
} }
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint) fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
// fronting takes only the address and port, the network stays that of the dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
// dial, which is UDP for h3 though the destination is built as TCP host, err := net.ParseDestination("tcp:" + addr)
dial := func(ctx context.Context, network, addr string) (net.Conn, net.Destination, error) {
host, err := net.ParseDestination(network + ":" + addr)
if err != nil { if err != nil {
return nil, host, errors.New("bad address: ", addr).Base(err) return nil, host, errors.New("bad address: ", addr).Base(err)
} }
@@ -166,36 +136,25 @@ func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings
} }
var conn net.Conn var conn net.Conn
switch { if streamSettings.FinalMask != nil {
case streamSettings.FinalMask == nil:
conn, err = internet.DialSystem(ctx, target, sockopt)
case target.Network == net.Network_UDP:
conn, err = streamSettings.FinalMask.DialUDP(ctx, target)
default:
conn, err = streamSettings.FinalMask.DialTCP(ctx, target) conn, err = streamSettings.FinalMask.DialTCP(ctx, target)
} else {
conn, err = internet.DialSystem(ctx, target, sockopt)
} }
if err != nil { if err != nil {
return nil, host, errors.New("failed to dial to dest").Base(err) return nil, host, errors.New("failed to dial to dest").Base(err)
} }
// h3 closes its QUIC connection instead, as the server never hears of a UDP
// socket that closes
if network == "udp" {
return conn, host, nil return conn, host, nil
} }
// CloseIdleConnections leaves an h2 connection open while a stream just
// canceled has not left it yet
stop := context.AfterFunc(connCtx, func() { conn.Close() })
return &ctxConn{Conn: conn, stop: stop}, host, nil
}
dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) { dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, _, err := dial(ctx, "tcp", addr) conn, _, err := dial(ctx, addr)
return conn, err return conn, err
} }
// http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn // http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) { dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, host, err := dial(ctx, "tcp", addr) conn, host, err := dial(ctx, addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -226,86 +185,7 @@ func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings
MaxIdleConns: maxConns, MaxIdleConns: maxConns,
MaxIdleConnsPerHost: maxConns, MaxIdleConnsPerHost: maxConns,
MaxConnsPerHost: connsPerHost, MaxConnsPerHost: connsPerHost,
HTTP2: &http.HTTP2Config{SendPingTimeout: keepAlive}, HTTP2: &http.HTTP2Config{SendPingTimeout: net.ChromeH2KeepAlivePeriod},
}
}
var https http.RoundTripper = &secureTransport{h2: secure(1), h1: secure(maxConns), h1Hosts: h1Hosts}
// no server offers h3 by ALPN over TCP, so it is used only when asked for alone
if slices.Equal(tlsConfig.NextProtocol, []string{"h3"}) {
quicParams := streamSettings.QuicParams
if quicParams == nil {
quicParams = &internet.QuicParams{BbrProfile: string(bbr.ProfileStandard)}
}
// the defaults are those of quic-go, with no keepalive unless asked for, as
// in DoH: http3 never closes a connection that is idle or that it dropped
// after an error, so only the idle timeout or the end of connCtx ends it
quicConfig := &quic.Config{
InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow,
MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow,
MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second,
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
MaxIncomingStreams: quicParams.MaxIncomingStreams,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
ChromeParrot: !quicParams.DisableChromeParrot,
}
if quicConfig.KeepAlivePeriod == 0 {
quicConfig.KeepAlivePeriod = h3KeepAlive
}
https = &http3.Transport{
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, _ *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
switch quicParams.Congestion {
case "", "bbr", "reno":
default:
// brutal is for Hysteria servers, not services, and a panic here, in
// a goroutine of http3, would end the process
return nil, errors.New("unsupported congestion control over h3: ", quicParams.Congestion)
}
conn, host, err := dial(ctx, "udp", addr)
if err != nil {
return nil, err
}
var pktConn net.PacketConn
var udpAddr net.Addr
switch c := conn.(type) {
case *net.PacketConnWrapper:
pktConn = c.PacketConn
udpAddr = c.RemoteAddr()
case *cnc.Connection:
pktConn = &internet.FakePacketConn{Conn: c}
udpAddr = &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
default:
conn.Close()
return nil, errors.New("failed to dial to dest: unexpected ", reflect.TypeOf(c))
}
// built for each dial with the host as SNI, as for h2: http3 sets the
// SNI on a copy, which a pinned CA and the ECH lookup do not see
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
// it is for servers, and ChromeParrot refuses a config that has it
gotlsConfig.GetCertificate = nil
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
if !quicParams.DisableChromeParrot {
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
}
qconn, err := tr.Dial(ctx, udpAddr, gotlsConfig, cfg)
if err != nil {
tr.Close()
pktConn.Close()
return nil, err
}
stop := context.AfterFunc(connCtx, func() { qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "") })
context.AfterFunc(qconn.Context(), func() { stop(); tr.Close(); pktConn.Close() })
if quicParams.Congestion != "reno" {
congestion.UseBBR(qconn, bbr.Profile(quicParams.BbrProfile))
}
return qconn, nil
},
} }
} }
@@ -318,30 +198,18 @@ func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings
MaxIdleConnsPerHost: maxConns, MaxIdleConnsPerHost: maxConns,
MaxConnsPerHost: maxConns, MaxConnsPerHost: maxConns,
}, },
secure: https, secure: &secureTransport{h2: secure(1), h1: secure(maxConns)},
stall: stall, stall: stall,
}, },
} }
} }
// ctxConn is a connection that connCtx no longer closes once it is closed,
// so that one that ends on its own leaves nothing behind in connCtx
type ctxConn struct {
net.Conn
stop func() bool
}
func (c *ctxConn) Close() error {
c.stop()
return c.Conn.Close()
}
// secureTransport keeps a host to one connection, all that h2 needs, as without // secureTransport keeps a host to one connection, all that h2 needs, as without
// a limit parallel requests would each dial their own. A host that answers over // a limit parallel requests would each dial their own. A host that answers over
// h1 moves to a transport with more, since h1 carries one request at a time // h1 moves to a transport with more, since h1 carries one request at a time
type secureTransport struct { type secureTransport struct {
h2, h1 http.RoundTripper h2, h1 http.RoundTripper
h1Hosts *sync.Map h1Hosts sync.Map
} }
func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) { func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
@@ -354,129 +222,3 @@ func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
} }
return resp, err return resp, err
} }
var errSessionClosed = errors.New("the session is closed")
// connPool hands the sessions of a storage the connections of XMUX, each a
// client of its own made by newClient
type connPool struct {
mu sync.Mutex
xmux *splithttp.XmuxManager
}
func newConnPool(config *splithttp.XmuxConfig, newClient func(connCtx context.Context) *http.Client) *connPool {
var xmuxConfig splithttp.XmuxConfig
if config != nil {
xmuxConfig = *config
}
return &connPool{xmux: splithttp.NewXmuxManager(xmuxConfig, func() splithttp.XmuxConn {
ctx, cancel := context.WithCancel(context.Background())
return &poolConn{next: newClient(ctx).Transport, cancel: cancel}
})}
}
// poolConn is a connection of XMUX, which closes once XMUX drops it and the
// requests on it end
type poolConn struct {
next http.RoundTripper
cancel context.CancelFunc
mu sync.Mutex
inflight int
closing bool
}
func (c *poolConn) IsClosed() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.closing
}
func (c *poolConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.closing = true
if c.inflight == 0 {
c.cancel()
}
return nil
}
func (c *poolConn) done() {
c.mu.Lock()
defer c.mu.Unlock()
c.inflight--
if c.closing && c.inflight == 0 {
c.cancel()
}
}
// roundTrip sends a request counted in inflight by the caller
func (c *poolConn) roundTrip(r *http.Request) (*http.Response, error) {
resp, err := c.next.RoundTrip(r)
if err != nil {
c.done()
return nil, err
}
resp.Body = &doneBody{ReadCloser: resp.Body, done: sync.OnceFunc(c.done)}
return resp, nil
}
type doneBody struct {
io.ReadCloser
done func()
}
func (b *doneBody) Close() error {
err := b.ReadCloser.Close()
b.done()
return err
}
// sessionTransport sends the requests of one session over a connection of
// XMUX, and over another once that one has no requests or time left
type sessionTransport struct {
pool *connPool
mu sync.Mutex
client *splithttp.XmuxClient
closed bool
}
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return nil, errSessionClosed
}
client := t.client
if client == nil || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) {
t.pool.mu.Lock()
t.client = t.pool.xmux.GetXmuxClient(r.Context())
t.pool.mu.Unlock()
t.client.AddRunning()
t.client.LeftRequests.Add(-1)
if client != nil {
client.DoneRunning()
}
client = t.client
}
// counted under mu, or another request of the session that moves on could
// close the connection before this one is sent
conn := client.XmuxConn.(*poolConn)
conn.mu.Lock()
conn.inflight++
conn.mu.Unlock()
t.mu.Unlock()
return conn.roundTrip(r)
}
func (t *sessionTransport) Close() {
t.mu.Lock()
defer t.mu.Unlock()
t.closed = true
if t.client != nil {
t.client.DoneRunning()
t.client = nil
}
}
+12 -493
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
gotls "crypto/tls" gotls "crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"errors" "errors"
"io" "io"
@@ -12,22 +11,13 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"slices" "slices"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
xnet "github.com/xtls/xray-core/common/net" xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/protocol/tls/cert"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
) )
@@ -85,7 +75,7 @@ func sniForSettings(t *testing.T, serverName string) string {
driveFilesURL = "https://www.googleapis.com/drive/v3/files" driveFilesURL = "https://www.googleapis.com/drive/v3/files"
defer func() { driveFilesURL = prev }() defer func() { driveFilesURL = prev }()
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8) client := newServiceClient(settings, 5*time.Second, 8)
req, err := http.NewRequest(http.MethodGet, driveFilesURL, nil) req, err := http.NewRequest(http.MethodGet, driveFilesURL, nil)
if err != nil { if err != nil {
t.Fatalf("request: %v", err) t.Fatalf("request: %v", err)
@@ -155,7 +145,7 @@ func TestServiceClientEndsOnlyStalledRequests(t *testing.T) {
} }
})) }))
defer server.Close() defer server.Close()
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, stall, 8) client := newServiceClient(&internet.MemoryStreamConfig{}, stall, 8)
fetch := func(path string, body io.Reader) (int, error) { fetch := func(path string, body io.Reader) (int, error) {
req, err := http.NewRequest(http.MethodPost, server.URL+path, body) req, err := http.NewRequest(http.MethodPost, server.URL+path, body)
@@ -227,7 +217,7 @@ func TestServiceClientStallOverTLS(t *testing.T) {
} }
time.Sleep(5 * stall) time.Sleep(5 * stall)
}) })
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), stall, 8) client := newServiceClient(server.settings(&tls.Config{}), stall, 8)
for _, path := range []string{"/stuck", "/silent"} { for _, path := range []string{"/stuck", "/silent"} {
start := time.Now() start := time.Now()
@@ -250,13 +240,12 @@ func TestServiceClientStallOverTLS(t *testing.T) {
} }
// tlsServer is a TLS server that keeps the last hello it got and counts its // tlsServer is a TLS server that keeps the last hello it got and counts its
// connections, and those closed // connections
type tlsServer struct { type tlsServer struct {
*httptest.Server *httptest.Server
mu sync.Mutex mu sync.Mutex
hello *gotls.ClientHelloInfo hello *gotls.ClientHelloInfo
conns atomic.Int32 conns atomic.Int32
closed atomic.Int32
} }
func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc) *tlsServer { func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc) *tlsServer {
@@ -271,11 +260,8 @@ func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc)
} }
s.TLS = config s.TLS = config
s.Config.ConnState = func(_ net.Conn, state http.ConnState) { s.Config.ConnState = func(_ net.Conn, state http.ConnState) {
switch state { if state == http.StateNew {
case http.StateNew:
s.conns.Add(1) s.conns.Add(1)
case http.StateClosed:
s.closed.Add(1)
} }
} }
s.StartTLS() s.StartTLS()
@@ -312,7 +298,7 @@ func TestServiceClientSpeaksWhatServerPicks(t *testing.T) {
for _, protos := range [][]string{chromeALPN, {"http/1.1"}} { for _, protos := range [][]string{chromeALPN, {"http/1.1"}} {
server := newTLSServer(t, &gotls.Config{NextProtos: protos}, func(w http.ResponseWriter, r *http.Request) {}) server := newTLSServer(t, &gotls.Config{NextProtos: protos}, func(w http.ResponseWriter, r *http.Request) {})
for _, config := range []*tls.Config{{}, {NextProtocol: []string{"h2"}}, {NextProtocol: []string{"http/1.1"}}, {Fingerprint: "unsafe"}} { for _, config := range []*tls.Config{{}, {NextProtocol: []string{"h2"}}, {NextProtocol: []string{"http/1.1"}}, {Fingerprint: "unsafe"}} {
client := newServiceClient(context.Background(), new(sync.Map), server.settings(config), 5*time.Second, 8) client := newServiceClient(server.settings(config), 5*time.Second, 8)
resp, err := client.Get(server.URL) resp, err := client.Get(server.URL)
if err != nil { if err != nil {
t.Fatalf("server %v, alpn %v, fingerprint %q: %v", protos, config.NextProtocol, config.Fingerprint, err) t.Fatalf("server %v, alpn %v, fingerprint %q: %v", protos, config.NextProtocol, config.Fingerprint, err)
@@ -332,7 +318,7 @@ func TestServiceClientSpeaksWhatServerPicks(t *testing.T) {
// offers certificate compression // offers certificate compression
func TestServiceClientDefaultsToChrome(t *testing.T) { func TestServiceClientDefaultsToChrome(t *testing.T) {
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {}) server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {})
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8) client := newServiceClient(&internet.MemoryStreamConfig{}, 5*time.Second, 8)
// the test certificate is not trusted, the hello is all that is needed // the test certificate is not trusted, the hello is all that is needed
if resp, err := client.Get(server.URL); err == nil { if resp, err := client.Get(server.URL); err == nil {
resp.Body.Close() resp.Body.Close()
@@ -346,33 +332,6 @@ func TestServiceClientDefaultsToChrome(t *testing.T) {
} }
} }
// a request says it is chrome, as the hello does, unless it says what it is
func TestServiceClientUserAgent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.UserAgent())
}))
defer server.Close()
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8)
for ua, want := range map[string]string{"": utils.ChromeUA, "firefox": utils.FirefoxUA, "custom/1.0": "custom/1.0"} {
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
if ua != "" {
req.Header.Set("User-Agent", ua)
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
got, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if string(got) != want {
t.Errorf("with %q the server got %q, not %q", ua, got, want)
}
if req.Header.Get("User-Agent") != ua || len(req.Header) > min(len(ua), 1) {
t.Errorf("with %q the request was changed to %v", ua, req.Header)
}
}
}
// parallel requests share one h2 connection instead of dialing one each, the // parallel requests share one h2 connection instead of dialing one each, the
// first ones as the later ones, and next to a host that answers over h1 // first ones as the later ones, and next to a host that answers over h1
func TestServiceClientOneConnection(t *testing.T) { func TestServiceClientOneConnection(t *testing.T) {
@@ -380,7 +339,7 @@ func TestServiceClientOneConnection(t *testing.T) {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
}) })
h1 := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, func(w http.ResponseWriter, r *http.Request) {}) h1 := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, func(w http.ResponseWriter, r *http.Request) {})
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), 5*time.Second, driveMaxInflight) client := newServiceClient(server.settings(&tls.Config{}), 5*time.Second, driveMaxInflight)
get := func(url string) { get := func(url string) {
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
@@ -436,7 +395,7 @@ func TestServiceClientHTTP1(t *testing.T) {
server := newTLSServer(t, &gotls.Config{NextProtos: c.protos, EncryptedClientHelloKeys: echKeys}, func(w http.ResponseWriter, r *http.Request) { server := newTLSServer(t, &gotls.Config{NextProtos: c.protos, EncryptedClientHelloKeys: echKeys}, func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
}) })
client := newServiceClient(context.Background(), new(sync.Map), server.settings(c.config), 5*time.Second, 8) client := newServiceClient(server.settings(c.config), 5*time.Second, 8)
get := func() { get := func() {
resp, err := client.Get(server.URL) resp, err := client.Get(server.URL)
if err != nil { if err != nil {
@@ -467,7 +426,7 @@ func TestServiceClientHTTP1(t *testing.T) {
// without the keys the server gets the outer hello, which offers the ALPN of // without the keys the server gets the outer hello, which offers the ALPN of
// Chrome // Chrome
outer := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {}) outer := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {})
if resp, err := newServiceClient(context.Background(), new(sync.Map), outer.settings(ech), 5*time.Second, 8).Get(outer.URL); err == nil { if resp, err := newServiceClient(outer.settings(ech), 5*time.Second, 8).Get(outer.URL); err == nil {
resp.Body.Close() resp.Body.Close()
} }
if hello := outer.lastHello(); hello == nil { if hello := outer.lastHello(); hello == nil {
@@ -503,7 +462,7 @@ func TestServiceClientHungHandshake(t *testing.T) {
server.Listener = &muteFirst{Listener: server.Listener} server.Listener = &muteFirst{Listener: server.Listener}
server.StartTLS() server.StartTLS()
t.Cleanup(server.Close) t.Cleanup(server.Close)
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{Fingerprint: fingerprint}), stall, 8) client := newServiceClient(server.settings(&tls.Config{Fingerprint: fingerprint}), stall, 8)
if _, err := client.Get(server.URL); err == nil { if _, err := client.Get(server.URL); err == nil {
t.Fatalf("fingerprint %q: a request got through a handshake that hung", fingerprint) t.Fatalf("fingerprint %q: a request got through a handshake that hung", fingerprint)
@@ -529,7 +488,7 @@ func TestServiceClientClosesFailedHandshake(t *testing.T) {
for _, fingerprint := range []string{"", "unsafe"} { for _, fingerprint := range []string{"", "unsafe"} {
settings := &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: &tls.Config{Fingerprint: fingerprint}} settings := &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: &tls.Config{Fingerprint: fingerprint}}
go newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8).Get("https://" + ln.Addr().String()) go newServiceClient(settings, 5*time.Second, 8).Get("https://" + ln.Addr().String())
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -544,446 +503,6 @@ func TestServiceClientClosesFailedHandshake(t *testing.T) {
} }
} }
// afterFuncs counts the functions that context.AfterFunc registers on it and
// that are not stopped yet
type afterFuncs struct {
context.Context
live atomic.Int32
}
// Value hides the cancelCtx below, which context.AfterFunc would use instead
func (c *afterFuncs) Value(any) any { return nil }
func (c *afterFuncs) AfterFunc(func()) func() bool {
c.live.Add(1)
return func() bool {
c.live.Add(-1)
return true
}
}
// a connection that ends on its own, here after Connection: close, leaves
// nothing on the context that closes the connections of the client
func TestServiceClientForgetsClosedConnections(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "close")
}))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
connCtx := &afterFuncs{Context: ctx}
client := newServiceClient(connCtx, new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8)
for range 4 {
resp, err := client.Get(server.URL)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
}
waitFor(t, "the closed connections to leave the context", func() bool { return connCtx.live.Load() == 0 })
}
type roundTrip func(*http.Request) (*http.Response, error) type roundTrip func(*http.Request) (*http.Response, error)
func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
// "hKeepAlivePeriod" of XMUX sets when an idle h2 connection pings, -1 never,
// and h3 keeps alive only when it is set
func TestServiceClientKeepAlive(t *testing.T) {
for period, want := range map[int64]time.Duration{0: xnet.ChromeH2KeepAlivePeriod, 20: 20 * time.Second, -1: 0} {
settings := &internet.MemoryStreamConfig{ProtocolSettings: &Config{Xmux: &splithttp.XmuxConfig{HKeepAlivePeriod: period}}}
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
h2 := client.Transport.(*serviceTransport).secure.(*secureTransport).h2.(*http.Transport)
if got := h2.HTTP2.SendPingTimeout; got != want {
t.Errorf("hKeepAlivePeriod %d pings after %v, want %v", period, got, want)
}
}
for period, want := range map[int64]time.Duration{0: 0, 20: 20 * time.Second, -1: 0} {
settings := &internet.MemoryStreamConfig{
ProtocolSettings: &Config{Xmux: &splithttp.XmuxConfig{HKeepAlivePeriod: period}},
SecuritySettings: &tls.Config{NextProtocol: []string{"h3"}},
}
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
if got := client.Transport.(*serviceTransport).secure.(*http3.Transport).QUICConfig.KeepAlivePeriod; got != want {
t.Errorf("hKeepAlivePeriod %d keeps h3 alive every %v, want %v", period, got, want)
}
}
}
// h3 is used when "alpn" asks for it alone, and only then
func TestServiceClientH3Selection(t *testing.T) {
for _, c := range []struct {
alpn []string
h3 bool
}{
{nil, false},
{[]string{"h2"}, false},
{[]string{"http/1.1"}, false},
{[]string{"h3", "h2"}, false},
{[]string{"h2", "h3"}, false},
{[]string{"h3"}, true},
} {
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{SecuritySettings: &tls.Config{NextProtocol: c.alpn}}, 5*time.Second, 8)
if _, h3 := client.Transport.(*serviceTransport).secure.(*http3.Transport); h3 != c.h3 {
t.Errorf("alpn %v: h3 %v, want %v", c.alpn, h3, c.h3)
}
}
if _, h3 := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, 5*time.Second, 8).Transport.(*serviceTransport).secure.(*http3.Transport); h3 {
t.Error("h3 without tlsSettings")
}
}
// h3Server is an http3 server that keeps the SNI of each connection, and counts
// its connections and those closed
type h3Server struct {
port xnet.Port
hash, caHash [32]byte
mu sync.Mutex
snis []string
conns atomic.Int32
closed atomic.Int32
}
func newH3Server(t *testing.T, handler http.Handler) *h3Server {
t.Helper()
// ECDSA, as ChromeParrot offers no Ed25519, from a CA of its own
ca, caHash := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))
certificate, hash := cert.MustGenerate(ca, cert.DNSNames("www.googleapis.com", "oauth2.googleapis.com"))
key, err := x509.ParsePKCS8PrivateKey(certificate.PrivateKey)
if err != nil {
t.Fatal(err)
}
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatal(err)
}
s := &h3Server{port: xnet.Port(conn.LocalAddr().(*net.UDPAddr).Port), hash: hash, caHash: caHash}
server := &http3.Server{
Handler: handler,
TLSConfig: http3.ConfigureTLSConfig(&gotls.Config{
Certificates: []gotls.Certificate{{Certificate: [][]byte{certificate.Certificate, ca.Certificate}, PrivateKey: key}},
GetConfigForClient: func(hello *gotls.ClientHelloInfo) (*gotls.Config, error) {
s.mu.Lock()
s.snis = append(s.snis, hello.ServerName)
s.mu.Unlock()
return nil, nil
},
}),
ConnContext: func(ctx context.Context, c *quic.Conn) context.Context {
s.conns.Add(1)
context.AfterFunc(c.Context(), func() { s.closed.Add(1) })
return ctx
},
}
go server.Serve(conn)
t.Cleanup(func() {
server.Close()
conn.Close()
})
return s
}
// settings sends every host to the server over h3, through a destination built
// as TCP the way conf builds it, and pins the certificate of the server in config
func (s *h3Server) settings(config *tls.Config) *internet.MemoryStreamConfig {
config.NextProtocol = []string{"h3"}
config.PinnedPeerCertSha256 = [][]byte{s.hash[:]}
return &internet.MemoryStreamConfig{
ProtocolName: protocolName,
Destination: &xnet.Destination{Address: xnet.LocalHostIP, Port: s.port, Network: xnet.Network_TCP},
SecuritySettings: config,
}
}
func (s *h3Server) seenSNIs() []string {
s.mu.Lock()
defer s.mu.Unlock()
return slices.Sorted(slices.Values(s.snis))
}
// one client serves two hosts, and each gets its own SNI unless serverName
// says otherwise, which is also the name a pinned CA checks the certificate for
func TestServiceClientH3SNI(t *testing.T) {
for _, c := range []struct {
serverName string
pinCA bool
want []string
}{
{"", false, []string{"oauth2.googleapis.com", "www.googleapis.com"}},
{"www.google.com", false, []string{"www.google.com", "www.google.com"}},
{"", true, []string{"oauth2.googleapis.com", "www.googleapis.com"}},
} {
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
settings := server.settings(&tls.Config{ServerName: c.serverName})
if c.pinCA {
settings.SecuritySettings.(*tls.Config).PinnedPeerCertSha256 = [][]byte{server.caHash[:]}
}
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
for _, url := range []string{"https://www.googleapis.com/drive/v3/files", "https://oauth2.googleapis.com/token"} {
resp, err := client.Get(url)
if err != nil {
t.Fatalf("serverName %q, pinned CA %v, %s: %v", c.serverName, c.pinCA, url, err)
}
resp.Body.Close()
if resp.Proto != "HTTP/3.0" {
t.Errorf("serverName %q, %s: went over %s", c.serverName, url, resp.Proto)
}
}
if snis := server.seenSNIs(); !slices.Equal(snis, c.want) {
t.Errorf("serverName %q: SNIs %v, want %v", c.serverName, snis, c.want)
}
}
}
// a request that stands still ends over h3 too, and says so
func TestServiceClientStallOverH3(t *testing.T) {
const stall = 200 * time.Millisecond
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/stuck" {
w.Write([]byte("x"))
w.(http.Flusher).Flush()
}
time.Sleep(5 * stall)
}))
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), stall, 8)
for _, path := range []string{"/stuck", "/silent"} {
start := time.Now()
resp, err := client.Get("https://www.googleapis.com" + path)
if err == nil {
if resp.Proto != "HTTP/3.0" {
t.Fatalf("%s went over %s", path, resp.Proto)
}
_, err = io.ReadAll(resp.Body)
resp.Body.Close()
}
if !errors.Is(err, errStalled) {
t.Errorf("%s gave %v, want %v", path, err, errStalled)
}
if took := time.Since(start); took > 4*stall {
t.Errorf("%s took %v to give up", path, took)
}
}
}
// brutal and force-brutal, which are for Hysteria servers, give an error and
// no panic in a goroutine of http3, where XHTTP and DoH panic on brutal
func TestServiceClientH3Congestion(t *testing.T) {
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
for _, cc := range []string{"", "bbr", "reno", "brutal", "force-brutal"} {
settings := server.settings(&tls.Config{})
settings.QuicParams = &internet.QuicParams{Congestion: cc, BrutalUp: 1 << 20}
resp, err := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8).Get("https://www.googleapis.com/")
if err == nil {
resp.Body.Close()
}
if brutal := strings.HasSuffix(cc, "brutal"); brutal != (err != nil) || brutal && !strings.Contains(err.Error(), cc) {
t.Errorf("congestion %q gave %v", cc, err)
}
}
}
// recordConn keeps the datagrams sent through it
type recordConn struct {
net.PacketConn
mu sync.Mutex
sent [][]byte
closed atomic.Bool
}
func (c *recordConn) WriteTo(p []byte, addr net.Addr) (int, error) {
c.mu.Lock()
c.sent = append(c.sent, bytes.Clone(p))
c.mu.Unlock()
return c.PacketConn.WriteTo(p, addr)
}
func (c *recordConn) Close() error {
c.closed.Store(true)
return c.PacketConn.Close()
}
// recorder dials UDP through recordConns and keeps them
type recorder struct {
mu sync.Mutex
conns []*recordConn
}
func (r *recorder) dialUDP(ctx context.Context, dest xnet.Destination) (net.PacketConn, net.Addr, error) {
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, nil, err
}
rec := &recordConn{PacketConn: conn.(*xnet.PacketConnWrapper).PacketConn}
r.mu.Lock()
r.conns = append(r.conns, rec)
r.mu.Unlock()
return rec, conn.RemoteAddr(), nil
}
// a connection ends with its socket once it is idle for its idle timeout, as
// keepalive is off unless asked for, and so does one whose handshake failed,
// and neither leaves anything on the context that closes the connections
func TestServiceClientH3Closes(t *testing.T) {
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for _, pin := range [][]byte{server.hash[:], make([]byte, 32)} {
rec := new(recorder)
settings := server.settings(&tls.Config{})
settings.SecuritySettings.(*tls.Config).PinnedPeerCertSha256 = [][]byte{pin}
// ChromeParrot would keep the idle timeout at 30 s
settings.QuicParams = &internet.QuicParams{MaxIdleTimeout: 1, DisableChromeParrot: true}
settings.FinalMask = finalmask.NewFinalMask(nil, nil, nil, nil, rec.dialUDP, nil)
connCtx := &afterFuncs{Context: ctx}
resp, err := newServiceClient(connCtx, new(sync.Map), settings, 5*time.Second, 8).Get("https://www.googleapis.com/")
if err == nil {
resp.Body.Close()
}
if bytes.Equal(pin, server.hash[:]) != (err == nil) {
t.Fatalf("pin %x: %v", pin[:4], err)
}
closed := func() bool {
rec.mu.Lock()
defer rec.mu.Unlock()
return len(rec.conns) == 1 && rec.conns[0].closed.Load()
}
for deadline := time.Now().Add(5 * time.Second); !closed() && time.Now().Before(deadline); {
time.Sleep(10 * time.Millisecond)
}
if !closed() {
t.Errorf("pin %x: the socket was left open", pin[:4])
}
waitFor(t, "the closed connection to leave the context", func() bool { return connCtx.live.Load() == 0 })
}
}
// cncDialer gives UDP as a stream conn, the way dialerProxy does
type cncDialer struct{}
func (cncDialer) Dial(ctx context.Context, src xnet.Address, dest xnet.Destination, sockopt *internet.SocketConfig) (net.Conn, error) {
conn, err := net.Dial("udp", dest.NetAddr())
if err != nil {
return nil, err
}
return cnc.NewConnection(cnc.ConnectionInput(conn), cnc.ConnectionOutput(conn), cnc.ConnectionOnClose(conn)), nil
}
func (cncDialer) DestIpAddress() net.IP { return nil }
// h3 goes over the stream conn of a dialerProxy too
func TestServiceClientH3DialerProxy(t *testing.T) {
internet.UseAlternativeSystemDialer(cncDialer{})
defer internet.UseAlternativeSystemDialer(nil)
server := newH3Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
resp, err := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{}), 5*time.Second, 8).Get("https://www.googleapis.com/")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.Proto != "HTTP/3.0" {
t.Errorf("went over %s", resp.Proto)
}
}
// xdrive moves data over h3 to a fake Drive, with the noise of a UDP mask sent
// ahead of the Initial of each connection
func TestDriveOverH3(t *testing.T) {
drive := newFakeDrive(t)
server := newH3Server(t, drive.server.Config.Handler)
driveTokenURL = "https://oauth2.googleapis.com/token"
driveFilesURL = "https://www.googleapis.com/files"
driveUploadURL = "https://www.googleapis.com/upload"
rec := new(recorder)
settings := server.settings(&tls.Config{})
settings.ProtocolSettings = driveSettings().ProtocolSettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{MaxConnections: &splithttp.RangeConfig{From: 4, To: 4}}
masks := []finalmask.UDPMask{&noise.Config{Items: []*noise.Item{{Packet: []byte("noise")}}}}
settings.FinalMask = finalmask.NewFinalMask(nil, masks, nil, nil, rec.dialUDP, nil)
client, srv, cleanup := pairWith(t, settings)
defer cleanup()
if _, err := client.Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, srv, "ping")
if _, err := srv.Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, client, "pong")
payload := make([]byte, 300000)
for i := range payload {
payload[i] = byte(i % 251)
}
go client.Write(payload)
if err := srv.SetReadDeadline(time.Now().Add(testPatience)); err != nil {
t.Fatalf("SetReadDeadline: %v", err)
}
got := make([]byte, len(payload))
if _, err := io.ReadFull(srv, got); err != nil {
t.Fatalf("ReadFull: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatal("payload mismatch")
}
rec.mu.Lock()
defer rec.mu.Unlock()
// one for the listener to www.googleapis.com, one for the token to
// oauth2.googleapis.com and one of XMUX for each end of the session
if len(rec.conns) != 4 {
t.Errorf("%d connections went through the mask, want 4", len(rec.conns))
}
for i, conn := range rec.conns {
conn.mu.Lock()
sent := conn.sent
conn.mu.Unlock()
if len(sent) < 2 {
t.Errorf("connection %d sent %d datagrams", i, len(sent))
continue
}
// the noise, then a QUIC v1 Initial of 1250 bytes with no source
// connection ID, as ChromeParrot sends it
first, initial := sent[0], sent[1]
if string(first) != "noise" || len(initial) != 1250 || initial[0]&0xf0 != 0xc0 || !bytes.Equal(initial[1:5], []byte{0, 0, 0, 1}) || initial[6+int(initial[5])] != 0 {
t.Errorf("connection %d began with %q and %d bytes %x", i, first, len(initial), initial[:min(len(initial), 24)])
}
}
}
// over h3 a connection of XMUX is a QUIC connection, which closes once XMUX
// drops it, so that the server hears of it
func TestDriveXmuxOverH3(t *testing.T) {
drive := newFakeDrive(t)
server := newH3Server(t, drive.server.Config.Handler)
driveTokenURL = "https://www.googleapis.com/token"
driveFilesURL = "https://www.googleapis.com/files"
driveUploadURL = "https://www.googleapis.com/upload"
settings := server.settings(&tls.Config{})
settings.ProtocolSettings = driveSettings().ProtocolSettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{HMaxRequestTimes: &splithttp.RangeConfig{From: 3, To: 3}}
storage, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
session := sessionStorage(storage)
defer session.Close()
for _, name := range []string{"a", "b", "c", "d"} {
if err := session.Put(context.Background(), "streams/abc/c2s/"+name, []byte("x")); err != nil {
t.Fatalf("Put: %v", err)
}
}
// one for the token, the first of XMUX for 3 requests and one more
if n := server.conns.Load(); n != 3 {
t.Errorf("the token and 4 requests with 3 to a connection took %d connections", n)
}
waitFor(t, "the first connection of XMUX to close", func() bool { return server.closed.Load() == 1 })
}
+7 -19
View File
@@ -7,7 +7,6 @@
package xdrive package xdrive
import ( import (
splithttp "github.com/xtls/xray-core/transport/internet/splithttp"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
@@ -36,7 +35,6 @@ type Config struct {
EagerWindowMs uint32 `protobuf:"varint,10,opt,name=eager_window_ms,json=eagerWindowMs,proto3" json:"eager_window_ms,omitempty"` EagerWindowMs uint32 `protobuf:"varint,10,opt,name=eager_window_ms,json=eagerWindowMs,proto3" json:"eager_window_ms,omitempty"`
HoleTimeoutMs uint32 `protobuf:"varint,11,opt,name=hole_timeout_ms,json=holeTimeoutMs,proto3" json:"hole_timeout_ms,omitempty"` HoleTimeoutMs uint32 `protobuf:"varint,11,opt,name=hole_timeout_ms,json=holeTimeoutMs,proto3" json:"hole_timeout_ms,omitempty"`
Template string `protobuf:"bytes,12,opt,name=template,proto3" json:"template,omitempty"` Template string `protobuf:"bytes,12,opt,name=template,proto3" json:"template,omitempty"`
Xmux *splithttp.XmuxConfig `protobuf:"bytes,13,opt,name=xmux,proto3" json:"xmux,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -155,18 +153,11 @@ func (x *Config) GetTemplate() string {
return "" return ""
} }
func (x *Config) GetXmux() *splithttp.XmuxConfig {
if x != nil {
return x.Xmux
}
return nil
}
var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor
const file_transport_internet_xdrive_config_proto_rawDesc = "" + const file_transport_internet_xdrive_config_proto_rawDesc = "" +
"\n" + "\n" +
"&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\x1a)transport/internet/splithttp/config.proto\"\x8e\x04\n" + "&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\"\xcb\x03\n" +
"\x06Config\x12#\n" + "\x06Config\x12#\n" +
"\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" + "\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" +
"\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" + "\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" +
@@ -180,8 +171,7 @@ const file_transport_internet_xdrive_config_proto_rawDesc = "" +
"\x0feager_window_ms\x18\n" + "\x0feager_window_ms\x18\n" +
" \x01(\rR\reagerWindowMs\x12&\n" + " \x01(\rR\reagerWindowMs\x12&\n" +
"\x0fhole_timeout_ms\x18\v \x01(\rR\rholeTimeoutMs\x12\x1a\n" + "\x0fhole_timeout_ms\x18\v \x01(\rR\rholeTimeoutMs\x12\x1a\n" +
"\btemplate\x18\f \x01(\tR\btemplate\x12A\n" + "\btemplate\x18\f \x01(\tR\btemplateB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
"\x04xmux\x18\r \x01(\v2-.xray.transport.internet.splithttp.XmuxConfigR\x04xmuxB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
var ( var (
file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once
@@ -198,15 +188,13 @@ func file_transport_internet_xdrive_config_proto_rawDescGZIP() []byte {
var file_transport_internet_xdrive_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_transport_internet_xdrive_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_xdrive_config_proto_goTypes = []any{ var file_transport_internet_xdrive_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.xdrive.Config (*Config)(nil), // 0: xray.transport.internet.xdrive.Config
(*splithttp.XmuxConfig)(nil), // 1: xray.transport.internet.splithttp.XmuxConfig
} }
var file_transport_internet_xdrive_config_proto_depIdxs = []int32{ var file_transport_internet_xdrive_config_proto_depIdxs = []int32{
1, // 0: xray.transport.internet.xdrive.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig 0, // [0:0] is the sub-list for method output_type
1, // [1:1] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type
1, // [1:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee
1, // [1:1] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name
0, // [0:1] is the sub-list for field type_name
} }
func init() { file_transport_internet_xdrive_config_proto_init() } func init() { file_transport_internet_xdrive_config_proto_init() }
-3
View File
@@ -3,8 +3,6 @@ syntax = "proto3";
package xray.transport.internet.xdrive; package xray.transport.internet.xdrive;
option go_package = "github.com/xtls/xray-core/transport/internet/xdrive"; option go_package = "github.com/xtls/xray-core/transport/internet/xdrive";
import "transport/internet/splithttp/config.proto";
message Config { message Config {
string remote_folder = 1; string remote_folder = 1;
string service = 2; string service = 2;
@@ -18,5 +16,4 @@ message Config {
uint32 eager_window_ms = 10; uint32 eager_window_ms = 10;
uint32 hole_timeout_ms = 11; uint32 hole_timeout_ms = 11;
string template = 12; string template = 12;
xray.transport.internet.splithttp.XmuxConfig xmux = 13;
} }
+6 -37
View File
@@ -39,21 +39,11 @@ var (
) )
type driveStorage struct { type driveStorage struct {
*driveShared
// a session has a client and inflight of its own, see session
client *http.Client
inflight chan struct{}
release func()
}
// driveShared is what the sessions of a storage have in common
type driveShared struct {
conns *connPool
folder string folder string
clientID string clientID string
clientSecret string clientSecret string
refreshToken string refreshToken string
tokenClient *http.Client client *http.Client
tokenURL string tokenURL string
filesURL string filesURL string
uploadURL string uploadURL string
@@ -63,6 +53,8 @@ type driveShared struct {
token string token string
tokenExpiry time.Time tokenExpiry time.Time
inflight chan struct{}
idMu sync.Mutex idMu sync.Mutex
ids map[string]string ids map[string]string
} }
@@ -80,41 +72,21 @@ func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config
} }
} }
h1Hosts := new(sync.Map)
client := newServiceClient(context.Background(), h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
return &driveStorage{ return &driveStorage{
driveShared: &driveShared{
conns: newConnPool(config.Xmux, func(connCtx context.Context) *http.Client {
return newServiceClient(connCtx, h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
}),
folder: config.RemoteFolder, folder: config.RemoteFolder,
clientID: config.Secrets[0], clientID: config.Secrets[0],
clientSecret: config.Secrets[1], clientSecret: config.Secrets[1],
refreshToken: config.Secrets[2], refreshToken: config.Secrets[2],
tokenClient: client, client: newServiceClient(streamSettings, driveTimeout, driveMaxInflight),
tokenURL: driveTokenURL, tokenURL: driveTokenURL,
filesURL: driveFilesURL, filesURL: driveFilesURL,
uploadURL: driveUploadURL, uploadURL: driveUploadURL,
backoff: driveInitialBackoff, backoff: driveInitialBackoff,
inflight: make(chan struct{}, driveMaxInflight),
ids: make(map[string]string), ids: make(map[string]string),
},
client: client,
inflight: make(chan struct{}, driveMaxInflight),
}, nil }, nil
} }
// session returns a storage for one session, over the connections of XMUX
// until Close, with the token and ids of s
func (s *driveStorage) session() Storage {
transport := &sessionTransport{pool: s.conns}
return &driveStorage{
driveShared: s.driveShared,
client: &http.Client{Transport: transport},
inflight: make(chan struct{}, driveMaxInflight),
release: transport.Close,
}
}
func flatten(name string) string { func flatten(name string) string {
return strings.ReplaceAll(name, "/", flatSeparator) return strings.ReplaceAll(name, "/", flatSeparator)
} }
@@ -143,7 +115,7 @@ func (s *driveStorage) accessToken(ctx context.Context) (string, error) {
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.tokenClient.Do(req) resp, err := s.client.Do(req)
if err != nil { if err != nil {
return "", errors.New("failed to refresh the access token").Base(err) return "", errors.New("failed to refresh the access token").Base(err)
} }
@@ -600,8 +572,5 @@ func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error)
} }
func (s *driveStorage) Close() error { func (s *driveStorage) Close() error {
if s.release != nil {
s.release()
}
return nil return nil
} }
-6
View File
@@ -562,12 +562,6 @@ func TestDriveSharedClient(t *testing.T) {
if first != second { if first != second {
t.Fatal("same settings did not share one storage") t.Fatal("same settings did not share one storage")
} }
// a session has an inflight of its own, as large
session := sessionStorage(first).(*driveStorage)
defer session.Close()
if session.inflight == first.(*driveStorage).inflight || cap(session.inflight) != driveMaxInflight {
t.Fatalf("a session shares inflight or has cap %d", cap(session.inflight))
}
} }
func TestDriveOldServiceName(t *testing.T) { func TestDriveOldServiceName(t *testing.T) {
-13
View File
@@ -48,19 +48,6 @@ func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
} }
} }
// sessionStorage returns the storage of one session, which takes connections
// of XMUX until Close, with the token and cache of storage. Local storage has
// no connections
func sessionStorage(storage Storage) Storage {
switch s := storage.(type) {
case *driveStorage:
return s.session()
case *templateStorage:
return s.session()
}
return storage
}
var ( var (
sharedMu sync.Mutex sharedMu sync.Mutex
shared = make(map[string]Storage) shared = make(map[string]Storage)
+12 -76
View File
@@ -33,14 +33,6 @@ type opTemplate struct {
Headers map[string]string `json:"headers"` Headers map[string]string `json:"headers"`
Body string `json:"body"` Body string `json:"body"`
NamesRegex string `json:"namesRegex"` NamesRegex string `json:"namesRegex"`
// HrefPath makes the operation two-step, as in the Yandex Disk REST API: the first request
// answers with a JSON body, the URL found at HrefPath is then called with HrefMethod (GET by
// default), carrying the payload. The URL is used as given, with neither auth nor headers.
HrefPath string `json:"hrefPath"`
HrefMethod string `json:"hrefMethod"`
presigned bool
} }
type retryTemplate struct { type retryTemplate struct {
@@ -62,21 +54,13 @@ type storageTemplate struct {
} }
type templateStorage struct { type templateStorage struct {
*templateShared
// a session has a client and inflight of its own, see session
client *http.Client
inflight chan struct{}
release func()
}
// templateShared is what the sessions of a storage have in common
type templateShared struct {
conns *connPool
tmpl *storageTemplate tmpl *storageTemplate
tokenClient *http.Client client *http.Client
folder string folder string
secrets []string secrets []string
inflight chan struct{}
tokenMu sync.Mutex tokenMu sync.Mutex
token string token string
tokenExpiry time.Time tokenExpiry time.Time
@@ -110,35 +94,15 @@ func newTemplateStorage(streamSettings *internet.MemoryStreamConfig, config *Con
conc = maxTemplateConcurrency conc = maxTemplateConcurrency
} }
h1Hosts := new(sync.Map)
client := newServiceClient(context.Background(), h1Hosts, streamSettings, driveTimeout, conc)
return &templateStorage{ return &templateStorage{
templateShared: &templateShared{
conns: newConnPool(config.Xmux, func(connCtx context.Context) *http.Client {
return newServiceClient(connCtx, h1Hosts, streamSettings, driveTimeout, conc)
}),
tmpl: tmpl, tmpl: tmpl,
tokenClient: client, client: newServiceClient(streamSettings, driveTimeout, conc),
folder: config.RemoteFolder, folder: config.RemoteFolder,
secrets: config.Secrets, secrets: config.Secrets,
},
client: client,
inflight: make(chan struct{}, conc), inflight: make(chan struct{}, conc),
}, nil }, nil
} }
// session returns a storage for one session, over the connections of XMUX
// until Close, with the token of s
func (s *templateStorage) session() Storage {
transport := &sessionTransport{pool: s.conns}
return &templateStorage{
templateShared: s.templateShared,
client: &http.Client{Transport: transport},
inflight: make(chan struct{}, cap(s.inflight)),
release: transport.Close,
}
}
const maxTemplateConcurrency = 256 const maxTemplateConcurrency = 256
func (s *templateStorage) baseVars() map[string]string { func (s *templateStorage) baseVars() map[string]string {
@@ -252,7 +216,7 @@ func (s *templateStorage) accessToken(ctx context.Context) (string, error) {
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.tokenClient.Do(req) resp, err := s.client.Do(req)
if err != nil { if err != nil {
return "", errors.New("failed to fetch the token").Base(err) return "", errors.New("failed to fetch the token").Base(err)
} }
@@ -298,11 +262,6 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
backoff := driveInitialBackoff backoff := driveInitialBackoff
var lastErr error var lastErr error
url := op.URL
if !op.presigned {
url = subst(url, vars)
}
for attempt := 0; attempt < driveMaxAttempts; attempt++ { for attempt := 0; attempt < driveMaxAttempts; attempt++ {
if attempt > 0 { if attempt > 0 {
select { select {
@@ -316,15 +275,11 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
} }
} }
var authHeaders map[string]string authHeaders, err := s.authHeaders(ctx, vars)
if !op.presigned {
headers, err := s.authHeaders(ctx, vars)
if err != nil { if err != nil {
lastErr = err lastErr = err
continue continue
} }
authHeaders = headers
}
method := op.Method method := op.Method
if method == "" { if method == "" {
@@ -335,7 +290,7 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
if body != nil { if body != nil {
reader = bytes.NewReader(body) reader = bytes.NewReader(body)
} }
req, err := http.NewRequestWithContext(ctx, method, url, reader) req, err := http.NewRequestWithContext(ctx, method, subst(op.URL, vars), reader)
if err != nil { if err != nil {
return 0, nil, errors.New("failed to build request").Base(err) return 0, nil, errors.New("failed to build request").Base(err)
} }
@@ -370,7 +325,7 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
continue continue
} }
if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" && !op.presigned { if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" {
s.invalidateToken() s.invalidateToken()
lastErr = errors.New("the service rejected the token") lastErr = errors.New("the service rejected the token")
continue continue
@@ -384,22 +339,6 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
return 0, nil, lastErr return 0, nil, lastErr
} }
// run performs op, following its href when the operation is two-step.
func (s *templateStorage) run(ctx context.Context, op *opTemplate, vars map[string]string, body []byte) (int, []byte, error) {
if op.HrefPath == "" {
return s.do(ctx, op, vars, body)
}
status, payload, err := s.do(ctx, op, vars, nil)
if err != nil || status < 200 || status >= 300 {
return status, payload, err
}
href := jsonString(payload, op.HrefPath)
if href == "" {
return 0, nil, errors.New("the response has no href at ", op.HrefPath)
}
return s.do(ctx, &opTemplate{Method: op.HrefMethod, URL: href, presigned: true}, vars, body)
}
func (s *templateStorage) Put(ctx context.Context, name string, data []byte) error { func (s *templateStorage) Put(ctx context.Context, name string, data []byte) error {
vars := s.baseVars() vars := s.baseVars()
vars["name"] = s.storedName(name) vars["name"] = s.storedName(name)
@@ -410,7 +349,7 @@ func (s *templateStorage) Put(ctx context.Context, name string, data []byte) err
body = []byte(subst(s.tmpl.Put.Body, vars)) body = []byte(subst(s.tmpl.Put.Body, vars))
} }
status, payload, err := s.run(ctx, &s.tmpl.Put, vars, body) status, payload, err := s.do(ctx, &s.tmpl.Put, vars, body)
if err != nil { if err != nil {
return err return err
} }
@@ -424,7 +363,7 @@ func (s *templateStorage) Get(ctx context.Context, name string) ([]byte, error)
vars := s.baseVars() vars := s.baseVars()
vars["name"] = s.storedName(name) vars["name"] = s.storedName(name)
status, payload, err := s.run(ctx, &s.tmpl.Get, vars, nil) status, payload, err := s.do(ctx, &s.tmpl.Get, vars, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -442,7 +381,7 @@ func (s *templateStorage) Delete(ctx context.Context, name string) error {
vars := s.baseVars() vars := s.baseVars()
vars["name"] = s.storedName(name) vars["name"] = s.storedName(name)
status, payload, err := s.run(ctx, &s.tmpl.Delete, vars, nil) status, payload, err := s.do(ctx, &s.tmpl.Delete, vars, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -457,7 +396,7 @@ func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, err
flat := s.storedName(prefix) flat := s.storedName(prefix)
vars["prefix"] = flat vars["prefix"] = flat
status, payload, err := s.run(ctx, &s.tmpl.List, vars, nil) status, payload, err := s.do(ctx, &s.tmpl.List, vars, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -502,8 +441,5 @@ func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, err
} }
func (s *templateStorage) Close() error { func (s *templateStorage) Close() error {
if s.release != nil {
s.release()
}
return nil return nil
} }
+1 -117
View File
@@ -113,7 +113,7 @@ func templateSettings(store *fakeStore, auth map[string]interface{}, secrets []s
"delete": map[string]interface{}{"method": "DELETE", "url": base + "/folder/{name}"}, "delete": map[string]interface{}{"method": "DELETE", "url": base + "/folder/{name}"},
"list": map[string]interface{}{ "list": map[string]interface{}{
"method": "PROPFIND", "method": "PROPFIND",
"url": base + "/folder/?prefix={prefix}", "url": base + "/folder/",
"namesRegex": `<d:href>/folder/([^<]+)</d:href>`, "namesRegex": `<d:href>/folder/([^<]+)</d:href>`,
}, },
"retry": map[string]interface{}{"status": []int{429, 500, 502, 503}}, "retry": map[string]interface{}{"status": []int{429, 500, 502, 503}},
@@ -297,12 +297,6 @@ func TestTemplateConcurrency(t *testing.T) {
if cap(storage.inflight) != 4 { if cap(storage.inflight) != 4 {
t.Fatalf("inflight cap is %d, want 4 from the template", cap(storage.inflight)) t.Fatalf("inflight cap is %d, want 4 from the template", cap(storage.inflight))
} }
// and so has a session, of its own
session := storage.session().(*templateStorage)
defer session.Close()
if session.inflight == storage.inflight || cap(session.inflight) != 4 {
t.Fatalf("a session shares inflight or has cap %d, want 4", cap(session.inflight))
}
} }
func TestTemplateConcurrencyDefault(t *testing.T) { func TestTemplateConcurrencyDefault(t *testing.T) {
@@ -312,113 +306,3 @@ func TestTemplateConcurrencyDefault(t *testing.T) {
t.Fatalf("default inflight cap is %d, want %d", cap(storage.inflight), driveMaxInflight) t.Fatalf("default inflight cap is %d, want %d", cap(storage.inflight), driveMaxInflight)
} }
} }
// hrefStore mimics the Yandex Disk REST API: upload and download answer with an href
// to a presigned blob URL, which must be called without the API credentials.
type hrefStore struct {
*fakeStore
blobAuth string
}
func newHrefStore(t *testing.T) *hrefStore {
t.Helper()
store := &hrefStore{fakeStore: &fakeStore{objects: make(map[string][]byte)}}
store.server = httptest.NewServer(http.HandlerFunc(store.handle))
t.Cleanup(store.server.Close)
return store
}
func (s *hrefStore) handle(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/blob/") {
s.mu.Lock()
if auth := r.Header.Get("Authorization"); auth != "" {
s.blobAuth = auth
}
s.mu.Unlock()
r.URL.Path = "/folder/" + strings.TrimPrefix(r.URL.Path, "/blob/")
s.fakeStore.handle(w, r)
return
}
if r.Header.Get("Authorization") != "OAuth tok" {
w.WriteHeader(http.StatusUnauthorized)
return
}
name := r.URL.Query().Get("path")
switch r.URL.Path {
case "/upload":
case "/download":
s.mu.Lock()
_, ok := s.objects[name]
s.mu.Unlock()
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
default:
w.WriteHeader(http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{"href": s.server.URL + "/blob/" + name})
}
func hrefSettings(store *hrefStore) *internet.MemoryStreamConfig {
base := store.server.URL
settings := templateSettings(store.fakeStore, map[string]interface{}{
"type": "static", "header": map[string]interface{}{"Authorization": "OAuth {secret0}"},
}, []string{"tok"})
cfg := settings.ProtocolSettings.(*Config)
var tmpl map[string]interface{}
json.Unmarshal([]byte(cfg.Template), &tmpl)
tmpl["put"] = map[string]interface{}{"url": base + "/upload?path={name}", "hrefPath": "href", "hrefMethod": "PUT"}
tmpl["get"] = map[string]interface{}{"url": base + "/download?path={name}", "hrefPath": "href"}
tmpl["list"] = map[string]interface{}{"method": "PROPFIND", "url": base + "/blob/", "namesRegex": `<d:href>/folder/([^<]+)</d:href>`}
tmpl["delete"] = map[string]interface{}{"method": "DELETE", "url": base + "/blob/{name}"}
raw, _ := json.Marshal(tmpl)
cfg.Template = string(raw)
return settings
}
func TestTemplateTwoStep(t *testing.T) {
store := newHrefStore(t)
settings := hrefSettings(store)
storage, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
ctx := context.Background()
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", []byte("hello")); err != nil {
t.Fatalf("Put: %v", err)
}
data, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
if err != nil {
t.Fatalf("Get: %v", err)
}
if string(data) != "hello" {
t.Fatalf("Get returned %q, want hello", data)
}
if _, err := storage.Get(ctx, "streams/abc/c2s/000000009.seg"); err != errNotFound {
t.Fatalf("Get of a missing object returned %v, want errNotFound", err)
}
if store.blobAuth != "" {
t.Fatalf("the presigned href received credentials %q", store.blobAuth)
}
}
func TestTemplateTwoStepTransport(t *testing.T) {
store := newHrefStore(t)
client, server, cleanup := pairWith(t, hrefSettings(store))
defer cleanup()
if _, err := client.Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, server, "ping")
if _, err := server.Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, client, "pong")
}
+1 -4
View File
@@ -87,7 +87,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
if err != nil { if err != nil {
return nil, err return nil, err
} }
storage = sessionStorage(storage)
session, err := newSessionID() session, err := newSessionID()
if err != nil { if err != nil {
@@ -229,10 +228,8 @@ func (l *Listener) claim(session string) bool {
} }
func (l *Listener) newSessionConn(session string) *Conn { func (l *Listener) newSessionConn(session string) *Conn {
storage := sessionStorage(l.storage) return newConn(l.ctx, l.storage,
return newConn(l.ctx, storage,
downlinkPrefix(session), uplinkPrefix(session), l.params, func() { downlinkPrefix(session), uplinkPrefix(session), l.params, func() {
storage.Close()
l.mu.Lock() l.mu.Lock()
delete(l.active, session) delete(l.active, session)
l.mu.Unlock() l.mu.Unlock()
-390
View File
@@ -4,21 +4,16 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/rand" "crypto/rand"
gotls "crypto/tls"
"io" "io"
"net/http"
"os" "os"
"strings" "strings"
"sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
) )
const testPatience = 30 * time.Second const testPatience = 30 * time.Second
@@ -586,388 +581,3 @@ func TestInlinePayload(t *testing.T) {
t.Fatalf("called Get %d times for an inline payload", got) t.Fatalf("called Get %d times for an inline payload", got)
} }
} }
// runSessions opens n sessions at once, both ends in this process, passes data
// both ways over each and closes them
func runSessions(t *testing.T, streamSettings *internet.MemoryStreamConfig, n int) {
t.Helper()
accepted := make(chan stat.Connection, 1)
listener, err := Serve(context.Background(), net.LocalHostIP, net.Port(0), streamSettings, func(conn stat.Connection) {
accepted <- conn
})
if err != nil {
t.Fatalf("Serve: %v", err)
}
defer listener.Close()
var ends []stat.Connection
defer func() {
for _, end := range ends {
end.Close()
}
}()
for range n {
client, err := Dial(context.Background(), net.Destination{}, streamSettings)
if err != nil {
t.Fatalf("Dial: %v", err)
}
ends = append(ends, client)
select {
case peer := <-accepted:
ends = append(ends, peer)
case <-time.After(testPatience):
t.Fatal("listener did not accept the session")
}
}
for i := 0; i < len(ends); i += 2 {
if _, err := ends[i].Write([]byte("ping")); err != nil {
t.Fatalf("client write: %v", err)
}
expectRead(t, ends[i+1], "ping")
if _, err := ends[i+1].Write([]byte("pong")); err != nil {
t.Fatalf("server write: %v", err)
}
expectRead(t, ends[i], "pong")
}
}
// sessionEnd is the session and end of it that a request to the fake store
// comes from, or "" for the listener and the token
func sessionEnd(r *http.Request) string {
name := strings.TrimPrefix(r.URL.Path, "/folder/")
if r.Method == "PROPFIND" {
name = r.URL.Query().Get("prefix")
}
write := r.Method == http.MethodPut
parts := strings.Split(name, flatSeparator)
switch {
case len(parts) > 2 && parts[0] == streamsDir:
// each end writes its own direction and reads the other
if (parts[2] == uplinkDir) == write {
return parts[1] + " client"
}
return parts[1] + " server"
case len(parts) == 2 && parts[0] == sessionsDir && write:
_, session, _ := strings.Cut(parts[1], "-")
return session + " client"
}
return ""
}
// with "maxConcurrency" 1 every end of a session has a connection of its own,
// with "maxConnections" the ends share that many, and the sessions after them
// take the same ones again. The listener and the token use none of them, and
// the sessions get the token once
func TestXmuxConnections(t *testing.T) {
const n = 3
for _, c := range []struct {
name string
xmux *splithttp.XmuxConfig
conns int
shared bool
}{
{"maxConcurrency", &splithttp.XmuxConfig{MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 1}}, 2 * n, false},
{"maxConnections", &splithttp.XmuxConfig{MaxConnections: &splithttp.RangeConfig{From: 2, To: 2}}, 2, true},
} {
t.Run(c.name, func(t *testing.T) {
resetSharedStorage()
t.Cleanup(resetSharedStorage)
store := &fakeStore{objects: make(map[string][]byte), needAuth: "Bearer tok-fake"}
var (
mu sync.Mutex
conns = make(map[string]map[string]bool)
)
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
end := sessionEnd(r)
mu.Lock()
if conns[end] == nil {
conns[end] = make(map[string]bool)
}
conns[end][r.RemoteAddr] = true
mu.Unlock()
store.handle(w, r)
})
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{
"type": "oauth2",
"tokenUrl": server.URL + "/token",
"form": map[string]interface{}{"grant_type": "refresh_token", "refresh_token": "{secret0}"},
"header": map[string]interface{}{"Authorization": "Bearer {token}"},
}, []string{"refresh"})
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = c.xmux
runSessions(t, settings, n)
mu.Lock()
first := conns
conns = make(map[string]map[string]bool)
mu.Unlock()
dialed := server.conns.Load()
runSessions(t, settings, n)
if got := server.conns.Load(); got != dialed {
t.Errorf("the next %d sessions dialed %d connections", n, got-dialed)
}
owner := make(map[string]string)
for end, addrs := range first {
if end == "" {
continue
}
if len(addrs) != 1 {
t.Errorf("%s used %d connections", end, len(addrs))
}
for addr := range addrs {
if owner[addr] != "" && !c.shared {
t.Errorf("%s and %s shared a connection", end, owner[addr])
}
owner[addr] = end
}
}
if len(owner) != c.conns {
t.Errorf("%d sessions had %d connections, want %d", 2*n, len(owner), c.conns)
}
for addr := range first[""] {
if owner[addr] != "" {
t.Errorf("the listener or the token went over the connection of %s", owner[addr])
}
}
store.mu.Lock()
defer store.mu.Unlock()
if store.tokens != 1 {
t.Errorf("the sessions got the token %d times", store.tokens)
}
})
}
}
func TestDriveXmuxConnections(t *testing.T) {
drive := newFakeDrive(t)
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, drive.server.Config.Handler.ServeHTTP)
driveTokenURL = server.URL + "/token"
driveFilesURL = server.URL + "/files"
driveUploadURL = server.URL + "/upload"
settings := driveSettings()
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{MaxConnections: &splithttp.RangeConfig{From: 3, To: 3}}
runSessions(t, settings, 3)
if got := server.conns.Load(); got != 4 {
t.Errorf("6 ends of sessions with 3 connections and the listener took %d connections", got)
}
drive.mu.Lock()
defer drive.mu.Unlock()
if drive.tokens != 1 {
t.Errorf("6 ends of sessions got the token %d times", drive.tokens)
}
}
// a session knows the hosts that answered the storage over h1, the polling of
// the listener for one, so it sends requests in a row over one connection, not
// first over one for h2 that then stays idle, and parallel ones over more
func TestSessionKnowsHTTP1Hosts(t *testing.T) {
check := func(name string, server *tlsServer, storage Storage) {
put := func(s Storage, names ...string) {
for _, n := range names {
if err := s.Put(context.Background(), "streams/abc/c2s/"+n, []byte("x")); err != nil {
t.Errorf("%s: Put: %v", name, err)
}
}
}
put(storage, "a", "b")
before := server.conns.Load()
session := sessionStorage(storage)
defer session.Close()
put(session, "c", "d")
if n := server.conns.Load() - before; n != 1 {
t.Errorf("%s: 2 requests in a row of a session over h1 took %d connections", name, n)
}
before = server.conns.Load()
var wg sync.WaitGroup
for _, n := range []string{"e", "f", "g", "h"} {
wg.Go(func() { put(session, n) })
}
wg.Wait()
if server.conns.Load() == before {
t.Errorf("%s: 4 parallel requests of a session over h1 took one connection", name)
}
}
slow := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond)
next(w, r)
}
}
store := &fakeStore{objects: make(map[string][]byte)}
server := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, slow(store.handle))
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
template, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
check("template", server, template)
drive := newFakeDrive(t)
server = newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, slow(drive.server.Config.Handler.ServeHTTP))
driveTokenURL = server.URL + "/token"
driveFilesURL = server.URL + "/files"
settings = driveSettings()
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
google, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
check("drive", server, google)
}
// with "hMaxRequestTimes" a session moves on to a new connection after that
// many requests, and the old one closes once the requests on it end, not before
func TestXmuxRequestLimit(t *testing.T) {
store := &fakeStore{objects: make(map[string][]byte)}
arrived := make(chan struct{}, 1)
release := make(chan struct{})
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
arrived <- struct{}{}
<-release
}
store.handle(w, r)
})
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{HMaxRequestTimes: &splithttp.RangeConfig{From: 3, To: 3}}
storage, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
session := sessionStorage(storage)
defer session.Close()
put := func(names ...string) {
for _, name := range names {
if err := session.Put(context.Background(), "streams/abc/c2s/"+name, []byte("x")); err != nil {
t.Fatalf("Put: %v", err)
}
}
}
put("a")
got := make(chan error, 1)
go func() {
_, err := session.Get(context.Background(), "streams/abc/c2s/a")
got <- err
}()
<-arrived
put("b", "c")
if n := server.conns.Load(); n != 2 {
t.Errorf("4 requests with 3 to a connection took %d connections", n)
}
if server.closed.Load() != 0 {
t.Error("the first connection closed under a request")
}
close(release)
if err := <-got; err != nil {
t.Errorf("Get on the first connection: %v", err)
}
waitFor(t, "the first connection to close", func() bool { return server.closed.Load() == 1 })
put("d", "e", "f")
if n := server.conns.Load(); n != 3 {
t.Errorf("7 requests with 3 to a connection took %d connections", n)
}
waitFor(t, "the second connection to close", func() bool { return server.closed.Load() == 2 })
}
// a session that fails to announce itself lets go of its connection, which the
// next one takes with "maxConcurrency" 1
func TestFailedDialReleasesConnection(t *testing.T) {
resetSharedStorage()
t.Cleanup(resetSharedStorage)
store := &fakeStore{objects: make(map[string][]byte)}
var refuse atomic.Bool
refuse.Store(true)
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
if refuse.Load() {
w.WriteHeader(http.StatusUnauthorized)
return
}
store.handle(w, r)
})
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
settings.ProtocolSettings.(*Config).Xmux = &splithttp.XmuxConfig{MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 1}}
if _, err := Dial(context.Background(), net.Destination{}, settings); err == nil {
t.Fatal("Dial announced a session that the service refused")
}
refuse.Store(false)
conn, err := Dial(context.Background(), net.Destination{}, settings)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer conn.Close()
if n := server.conns.Load(); n != 1 {
t.Errorf("a failed Dial and the next took %d connections", n)
}
}
// a session gets the token over a connection of the storage, not over one of
// XMUX
func TestSessionTokenConnection(t *testing.T) {
check := func(name string, server *tlsServer, storage Storage) {
session := sessionStorage(storage)
defer session.Close()
if err := session.Put(context.Background(), "sessions/abc", nil); err != nil {
t.Fatalf("%s: Put: %v", name, err)
}
if n := server.conns.Load(); n != 2 {
t.Errorf("%s: the token and a Put of a session took %d connections, want 2", name, n)
}
}
store := &fakeStore{objects: make(map[string][]byte), needAuth: "Bearer tok-fake"}
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, store.handle)
store.server = server.Server
settings := templateSettings(store, map[string]interface{}{
"type": "oauth2",
"tokenUrl": server.URL + "/token",
"header": map[string]interface{}{"Authorization": "Bearer {token}"},
}, nil)
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
template, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newTemplateStorage: %v", err)
}
check("template", server, template)
drive := newFakeDrive(t)
server = newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, drive.server.Config.Handler.ServeHTTP)
driveTokenURL = server.URL + "/token"
driveFilesURL = server.URL + "/files"
settings = driveSettings()
settings.SecuritySettings = server.settings(&tls.Config{}).SecuritySettings
google, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
if err != nil {
t.Fatalf("newDriveStorage: %v", err)
}
check("drive", server, google)
}
// local storage has no connections to keep apart
func TestLocalSessionStorage(t *testing.T) {
storage, err := newLocalStorage(t.TempDir())
if err != nil {
t.Fatalf("newLocalStorage: %v", err)
}
if sessionStorage(storage) != Storage(storage) {
t.Fatal("a session of local storage got a storage of its own")
}
}