mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-21 16:54:29 +00:00
Compare commits
21
Commits
tls-update
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b1647cd9d | ||
|
|
557657b576 | ||
|
|
fc5ba8e6a2 | ||
|
|
00c0277787 | ||
|
|
3653dcb4fc | ||
|
|
061c2df291 | ||
|
|
23e2292344 | ||
|
|
41797c234d | ||
|
|
9424810830 | ||
|
|
12b7f85de3 | ||
|
|
de1bbbff88 | ||
|
|
ec73b73f5e | ||
|
|
6307fa1265 | ||
|
|
92ccc939aa | ||
|
|
95461cc148 | ||
|
|
3834e504b8 | ||
|
|
0f127f934c | ||
|
|
0f3d884786 | ||
|
|
da2248c516 | ||
|
|
fe2ae23f42 | ||
|
|
d15966c76c |
@@ -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
|
||||
|
||||
# Download geodat into a staging directory
|
||||
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
|
||||
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
|
||||
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
|
||||
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
|
||||
|
||||
RUN mkdir -p /tmp/empty
|
||||
|
||||
@@ -40,21 +40,15 @@ 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/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
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
|
||||
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=644 /tmp/geodat/*.dat /usr/local/share/xray/
|
||||
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /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=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/
|
||||
COPY --from=build --chown=0:65532 --chmod=750 /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=65532:65532 --chmod=700 /tmp/empty /var/log/xray
|
||||
|
||||
VOLUME /usr/local/etc/xray
|
||||
VOLUME /var/log/xray
|
||||
|
||||
@@ -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
|
||||
|
||||
# Download geodat into a staging directory
|
||||
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
|
||||
ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
|
||||
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
|
||||
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
|
||||
|
||||
RUN mkdir -p /tmp/empty
|
||||
|
||||
@@ -40,11 +40,6 @@ 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/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
|
||||
# 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:
|
||||
@@ -60,10 +55,9 @@ 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=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=0:0 --chmod=755 /tmp/empty /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=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/
|
||||
COPY --from=build --chown=0:65532 --chmod=750 /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=65532:65532 --chmod=700 /tmp/empty /var/log/xray
|
||||
|
||||
VOLUME /usr/local/etc/xray
|
||||
VOLUME /var/log/xray
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
max_attempts: 60
|
||||
command: |
|
||||
[ -d 'resources' ] || mkdir resources
|
||||
LIST=('Loyalsoldier v2ray-rules-dat geoip geoip' 'Loyalsoldier v2ray-rules-dat geosite geosite')
|
||||
LIST=('XTLS Xray-rules-dat geoip/geoip-lite geoip' 'XTLS Xray-rules-dat geosite/geosite geosite')
|
||||
for i in "${LIST[@]}"
|
||||
do
|
||||
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}'))
|
||||
|
||||
@@ -128,7 +128,7 @@ func TestDNSScriptGeoIPFallback(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
script := `
|
||||
local servers = require("xray.dns").Servers
|
||||
local us_ips = require("xray.geodata").BuildIPMatcher("geoip:us")
|
||||
local ru_ips = require("xray.geodata").BuildIPMatcher("geoip:ru")
|
||||
|
||||
local by_id = {}
|
||||
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)
|
||||
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
|
||||
if not err and us_ips:AnyMatch(ips) then
|
||||
if not err and ru_ips:AnyMatch(ips) then
|
||||
return ips, ttl, nil
|
||||
end
|
||||
return by_id.fallback:Query(domain, ipv4, ipv6, fake)
|
||||
@@ -152,7 +152,7 @@ end
|
||||
primary := &scriptNameServer{
|
||||
name: "primary",
|
||||
answers: map[string]net.IP{
|
||||
"us.example": net.ParseIP("2001:4860:4860::8888"),
|
||||
"ru.example": net.ParseIP("2a02:6b8::feed:0ff"),
|
||||
"other.example": net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
ttl: 30,
|
||||
@@ -191,7 +191,7 @@ end
|
||||
ip net.IP
|
||||
ttl uint32
|
||||
}{
|
||||
{"Us.Example.", net.ParseIP("2001:4860:4860::8888"), 30},
|
||||
{"Ru.Example.", net.ParseIP("2a02:6b8::feed:0ff"), 30},
|
||||
{"other.example", net.ParseIP("9.9.9.9"), 60},
|
||||
} {
|
||||
ips, ttl, err := server.LookupIP(tc.domain, option)
|
||||
|
||||
@@ -109,7 +109,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
|
||||
stats.Min = 0
|
||||
return stats
|
||||
}
|
||||
stats.Average = time.Duration(int(sum) / cnt)
|
||||
stats.Average = sum / time.Duration(cnt)
|
||||
var std float64
|
||||
if cnt < 2 {
|
||||
// no enough data for standard deviation, we assume it's half of the average rtt
|
||||
|
||||
@@ -415,7 +415,7 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
|
||||
|
||||
func BenchmarkMultiGeoIPMatcher(b *testing.B) {
|
||||
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
|
||||
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:ir", "geoip:ru"})
|
||||
common.Must(err)
|
||||
|
||||
matcher, err := NewIPMatcher(rules, MatcherAsType_Target)
|
||||
|
||||
@@ -400,13 +400,13 @@ func TestIPMatcher4CN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPMatcher6US(t *testing.T) {
|
||||
func TestIPMatcher6RU(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
matcher := buildIPMatcher("geoip:us")
|
||||
matcher := buildIPMatcher("geoip:ru")
|
||||
|
||||
if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
|
||||
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
|
||||
if !matcher.Match(xnet.ParseAddress("2a02:6b8::feed:0ff").IP()) {
|
||||
t.Error("expect RU geoip contain 2a02:6b8::feed:0ff, but actually not")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,11 +423,11 @@ func BenchmarkIPMatcher4CN(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIPMatcher6US(b *testing.B) {
|
||||
func BenchmarkIPMatcher6RU(b *testing.B) {
|
||||
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
matcher := buildIPMatcher("geoip:us")
|
||||
ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
|
||||
matcher := buildIPMatcher("geoip:ru")
|
||||
ip := xnet.ParseAddress("2a02:6b8::feed:0ff").IP()
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ func TestParseIPRules(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
|
||||
rules := []string{
|
||||
"geoip:us",
|
||||
"geoip:ru",
|
||||
"geoip:cn",
|
||||
"!geoip:cn",
|
||||
"!!geoip:cn",
|
||||
@@ -20,9 +20,9 @@ func TestParseIPRules(t *testing.T) {
|
||||
"!geoip:!cn",
|
||||
"ext:geoip.dat:!cn",
|
||||
"ext:geoip.dat:!!cn",
|
||||
"ext:geoip.dat:ca",
|
||||
"ext:geoip.dat:ir",
|
||||
"ext-ip:geoip.dat:!cn",
|
||||
"ext-ip:geoip.dat:!ca",
|
||||
"ext-ip:geoip.dat:!ir",
|
||||
"192.168.0.0/24",
|
||||
"!192.168.0.0/24",
|
||||
"!!192.168.0.0/24",
|
||||
|
||||
@@ -26,20 +26,20 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
|
||||
panic("Unsupported network type for process lookup.")
|
||||
}
|
||||
|
||||
var procFile string
|
||||
var procFiles []string
|
||||
|
||||
switch network {
|
||||
case "tcp":
|
||||
if net.ParseIP(srcIP).To4() != nil {
|
||||
procFile = "/proc/net/tcp"
|
||||
procFiles = []string{"/proc/net/tcp"}
|
||||
} else {
|
||||
procFile = "/proc/net/tcp6"
|
||||
procFiles = []string{"/proc/net/tcp6"}
|
||||
}
|
||||
case "udp":
|
||||
if net.ParseIP(srcIP).To4() != nil {
|
||||
procFile = "/proc/net/udp"
|
||||
procFiles = []string{"/proc/net/udp", "/proc/net/udp6"}
|
||||
} else {
|
||||
procFile = "/proc/net/udp6"
|
||||
procFiles = []string{"/proc/net/udp6", "/proc/net/udp"}
|
||||
}
|
||||
default:
|
||||
panic("Unsupported network type for process lookup.")
|
||||
@@ -50,12 +50,17 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
|
||||
return 0, "", "", errors.New("failed to format address: ", err)
|
||||
}
|
||||
|
||||
inode, err := findInodeInFile(procFile, targetHexAddr)
|
||||
portFallback := ""
|
||||
if network == "udp" {
|
||||
portFallback = fmt.Sprintf(":%04X", srcPort)
|
||||
}
|
||||
|
||||
inode, err := findInodeInFiles(procFiles, targetHexAddr, portFallback)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("could not search in ", procFile).Base(err)
|
||||
return 0, "", "", errors.New("could not search in ", strings.Join(procFiles, " and ")).Base(err)
|
||||
}
|
||||
if inode == "" {
|
||||
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile)
|
||||
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", strings.Join(procFiles, " or "))
|
||||
}
|
||||
|
||||
pidStr, err := findPidByInode(inode)
|
||||
@@ -102,31 +107,43 @@ func formatLittleEndianString(addr net.IP, port Port) (string, error) {
|
||||
return fmt.Sprintf("%s:%s", ipHex, portHex), nil
|
||||
}
|
||||
|
||||
func findInodeInFile(filePath, targetHexAddr string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) {
|
||||
onlyMatchedPort := ""
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
for i, filePath := range filePaths {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
if i > 0 && os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
localAddress := fields[1]
|
||||
if localAddress == targetHexAddr {
|
||||
inode := fields[9]
|
||||
return inode, nil
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
localAddress := fields[1]
|
||||
if localAddress == targetHexAddr {
|
||||
return fields[9], 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 "", scanner.Err()
|
||||
return onlyMatchedPort, nil
|
||||
}
|
||||
|
||||
func findPidByInode(inode string) (string, error) {
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 10
|
||||
Version_z byte = 10
|
||||
Version_z byte = 21
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/klauspost/cpuid/v2 v2.4.0
|
||||
github.com/klauspost/cpuid/v2 v2.4.1
|
||||
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.58.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/sync v0.24.0
|
||||
|
||||
@@ -33,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/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/klauspost/cpuid/v2 v2.4.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U=
|
||||
github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -97,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.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/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a h1:V5oicZXS0oqZx30gY/apgSVD7YvNBfrIOdcMnlelJY4=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a/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/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
|
||||
@@ -15,9 +15,11 @@ import (
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"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/header/custom"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||
@@ -74,6 +76,7 @@ var (
|
||||
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"filter": func() interface{} { return new(Filter) },
|
||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||
"fragment": func() interface{} { return new(FragmentMask) },
|
||||
"sudoku": func() interface{} { return new(Sudoku) },
|
||||
@@ -81,6 +84,7 @@ var (
|
||||
}, "type", "settings")
|
||||
|
||||
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"filter": func() interface{} { return new(Filter) },
|
||||
"header-custom": func() interface{} { return new(HeaderCustomUDP) },
|
||||
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
|
||||
"noise": func() interface{} { return new(NoiseMask) },
|
||||
@@ -93,6 +97,23 @@ var (
|
||||
}, "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 {
|
||||
Delay Int32Range `json:"delay"`
|
||||
Rand int32 `json:"rand"`
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
type TransportProtocol string
|
||||
@@ -86,6 +90,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
config.ProtocolName = protocol
|
||||
}
|
||||
|
||||
var xdriveH3 bool
|
||||
switch strings.ToLower(c.Security) {
|
||||
case "", "none":
|
||||
case "tls":
|
||||
@@ -100,6 +105,15 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
tm := serial.ToTypedMessage(ts)
|
||||
config.SecuritySettings = append(config.SecuritySettings, tm)
|
||||
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 == "" {
|
||||
// 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
|
||||
if fp := tls.GetFingerprint(strings.ToLower(tlsSettings.Fingerprint)); fp != nil && fp.Client != utls.HelloGolang.Client &&
|
||||
!strings.HasPrefix(fp.Client, utls.HelloRandomized.Client) {
|
||||
errors.LogWarning(context.Background(), `XDRIVE: "alpn" ["http/1.1"] reaches the wire only inside ECH, without "echConfigList" the hello carries the ALPN of the fingerprint and the server may pick h2`)
|
||||
}
|
||||
}
|
||||
case "reality":
|
||||
if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" {
|
||||
return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.")
|
||||
@@ -207,6 +221,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
})
|
||||
}
|
||||
if c.XDRIVESettings != nil {
|
||||
c.XDRIVESettings.h3 = xdriveH3
|
||||
xs, err := c.XDRIVESettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to build XDRIVE config.").Base(err)
|
||||
@@ -248,5 +263,20 @@ 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
|
||||
}
|
||||
|
||||
@@ -997,6 +997,10 @@ type XDriveConfig struct {
|
||||
EagerWindowMs uint32 `json:"eagerWindowMs"`
|
||||
HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
|
||||
Template json.RawMessage `json:"template"`
|
||||
Xmux XmuxConfig `json:"xmux"`
|
||||
|
||||
// set by StreamConfig when "alpn" is ["h3"]
|
||||
h3 bool
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
@@ -1004,6 +1008,10 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
switch c.Service {
|
||||
case "local":
|
||||
case "Google Drive":
|
||||
errors.PrintDeprecatedFeatureWarning(`"service": "Google Drive"`, `"service": "google-drive"`)
|
||||
c.Service = "google-drive"
|
||||
fallthrough
|
||||
case "google-drive":
|
||||
if len(c.Secrets) != 3 {
|
||||
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
|
||||
}
|
||||
@@ -1012,7 +1020,22 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New(`service "template" needs a "template" object`)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported service")
|
||||
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{
|
||||
RemoteFolder: c.RemoteFolder,
|
||||
@@ -1027,6 +1050,14 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
EagerWindowMs: c.EagerWindowMs,
|
||||
HoleTimeoutMs: c.HoleTimeoutMs,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
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"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -321,8 +324,25 @@ func TestXDriveRejectsUnknownService(t *testing.T) {
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Fatal("Build accepted an unsupported service")
|
||||
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template" or "local"`) {
|
||||
t.Fatalf("expected an unknown service error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveGoogleDriveName(t *testing.T) {
|
||||
for _, service := range []string{"google-drive", "Google Drive"} {
|
||||
config := &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b", "c"}}
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", service, err)
|
||||
}
|
||||
if got := built.(*xdrive.Config).Service; got != "google-drive" {
|
||||
t.Errorf("%s: service is %q, want google-drive", service, got)
|
||||
}
|
||||
config = &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b"}}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Errorf("%s: accepted two secrets", service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +375,131 @@ func TestXDriveTemplateStreamConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type logLines []string
|
||||
|
||||
func (l *logLines) Handle(msg log.Message) {
|
||||
*l = append(*l, msg.String())
|
||||
}
|
||||
|
||||
// "alpn" ["http/1.1"] is warned about where the hello does not carry it, which
|
||||
// is outside ECH with a fingerprint that has an ALPN of its own
|
||||
func TestXDriveHTTP1ALPNWarning(t *testing.T) {
|
||||
var lines logLines
|
||||
log.RegisterHandler(&lines)
|
||||
|
||||
for _, c := range []struct {
|
||||
method, tlsSettings string
|
||||
warn bool
|
||||
}{
|
||||
{"xdrive", `{}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"]}`, true},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "Firefox"}`, true},
|
||||
{"raw", `{"alpn": ["http/1.1"]}`, false},
|
||||
{"xdrive", `{"alpn": ["h2", "http/1.1"]}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "echConfigList": "udp://1.1.1.1"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "unsafe"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellogolang"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "randomized"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellorandomizednoalpn"}`, false},
|
||||
} {
|
||||
lines = nil
|
||||
config := new(StreamConfig)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"method": "`+c.method+`",
|
||||
"security": "tls",
|
||||
"tlsSettings": `+c.tlsSettings+`,
|
||||
"xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}
|
||||
}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err != nil {
|
||||
t.Fatalf("%s %s: %v", c.method, c.tlsSettings, err)
|
||||
}
|
||||
warned := false
|
||||
for _, line := range lines {
|
||||
warned = warned || strings.HasPrefix(line, "[Warning]") && strings.Contains(line, `"alpn"`)
|
||||
}
|
||||
if warned != c.warn {
|
||||
t.Errorf("%s %s: warned %v, want %v", c.method, c.tlsSettings, warned, c.warn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
config := new(XDriveConfig)
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
|
||||
@@ -364,3 +509,130 @@ func TestXDriveTemplateNeedsTemplate(t *testing.T) {
|
||||
t.Fatal("Build accepted a template service without a template")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
xdriveStream = `"streamSettings": {"network": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "local"}}`
|
||||
xdriveID = `"id": "27848739-7e62-4138-9fd3-098a63964b6b"`
|
||||
xdriveServer = `"address": "127.0.0.1", "port": 443, ` + xdriveID
|
||||
xdriveEnc = `"encryption": "mlkem768x25519plus.native.0rtt.XKbaq1BuzJKKKPxZbwgRpn-gYQbnhrYketgezASCL1I"`
|
||||
xdriveDec = `"decryption": "mlkem768x25519plus.native.600s.UKfUEMDdjlEnycTMBP0a08acky8vRRKSrM1MRnaYk3s"`
|
||||
)
|
||||
|
||||
func TestXDriveOutboundConfig(t *testing.T) {
|
||||
build := func(s string) error {
|
||||
c := new(OutboundDetourConfig)
|
||||
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range []string{
|
||||
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`,
|
||||
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": 0, "xudpConcurrency": 8, "xudpProxyUDP443": "allow"}}`,
|
||||
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "reverse": {"tag": "r"}}, ` + xdriveStream + `}`,
|
||||
`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `}]}]}, ` + xdriveStream + `, "mux": {"enabled": true}}`,
|
||||
} {
|
||||
if err := build(input); err != nil {
|
||||
t.Errorf("%s: %v", input, err)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct{ input, err string }{
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`},
|
||||
{`{"protocol": "vless", "settings": {"address": "example.com", "port": 443, ` + xdriveID + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": false}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpConcurrency": -1}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpProxyUDP443": "skip"}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`},
|
||||
{`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}]}]}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "testpre": 1}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"testpre"`},
|
||||
{`{"protocol": "vmess", "settings": {` + xdriveServer + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"},
|
||||
{`{"protocol": "trojan", "settings": {"address": "example.com", "port": 443, "password": "p"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"},
|
||||
{`{"protocol": "freedom", ` + xdriveStream + `}`, "VLESS"},
|
||||
} {
|
||||
if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) {
|
||||
t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveInboundConfig(t *testing.T) {
|
||||
build := func(s string) error {
|
||||
c := new(InboundDetourConfig)
|
||||
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range []string{
|
||||
`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`,
|
||||
`{"protocol": "vless", "listen": "@xdrive", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`,
|
||||
} {
|
||||
if err := build(input); err != nil {
|
||||
t.Errorf("%s: %v", input, err)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct{ input, err string }{
|
||||
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], "decryption": "none"}, ` + xdriveStream + `}`, `"decryption"`},
|
||||
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `}`, `"flow"`},
|
||||
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `, "flow": "xtls-rprx-vision"}], ` + xdriveDec + `}, ` + xdriveStream + `}`, `"flow"`},
|
||||
{`{"protocol": "vmess", "port": 443, "settings": {"clients": [{` + xdriveID + `}]}, ` + xdriveStream + `}`, "VLESS"},
|
||||
{`{"protocol": "vless", "port": "10800-10803", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`, "one port"},
|
||||
} {
|
||||
if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) {
|
||||
t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ import (
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
"github.com/xtls/xray-core/proxy/vless/inbound"
|
||||
"github.com/xtls/xray-core/proxy/vless/outbound"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -209,6 +213,9 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
if _, ok := ts.(*masque.ServerConfig); !ok && receiverSettings.StreamSettings != nil && receiverSettings.StreamSettings.ProtocolName == "masque" {
|
||||
return nil, errors.New("the masque transport can only be used by the masque inbound")
|
||||
}
|
||||
if err := validateXDrive(ts, receiverSettings.StreamSettings, nil, receiverSettings.PortList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &core.InboundHandlerConfig{
|
||||
Tag: c.Tag,
|
||||
@@ -262,6 +269,54 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateXDrive allows only VLESS with encryption on xdrive, and on the client Mux.Cool for every connection
|
||||
func validateXDrive(ts proto.Message, ss *internet.StreamConfig, ms *proxyman.MultiplexingConfig, pl *net.PortList) error {
|
||||
if ss == nil || ss.ProtocolName != "xdrive" {
|
||||
return nil
|
||||
}
|
||||
switch c := ts.(type) {
|
||||
case *outbound.Config:
|
||||
a, err := c.Vnext.User.Account.GetInstance()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account := a.(*vless.Account)
|
||||
if account.Flow != "" {
|
||||
return errors.New(`the xdrive transport doesn't support VLESS "flow"`)
|
||||
}
|
||||
if account.Encryption == "none" {
|
||||
return errors.New(`the xdrive transport needs VLESS "encryption", run "xray vlessenc" to get one`)
|
||||
}
|
||||
// reverse opens its own mux
|
||||
if account.Reverse == nil && (ms == nil || !ms.Enabled || ms.Concurrency < 0 || ms.XudpConcurrency < 0 || ms.XudpProxyUDP443 == "skip") {
|
||||
return errors.New(`the xdrive transport needs "mux" enabled, with no negative "concurrency" or "xudpConcurrency" and no "xudpProxyUDP443":"skip"`)
|
||||
}
|
||||
if account.Testpre != 0 {
|
||||
return errors.New(`the xdrive transport doesn't support VLESS "testpre"`)
|
||||
}
|
||||
case *inbound.Config:
|
||||
if c.Decryption == "none" {
|
||||
return errors.New(`the xdrive transport needs VLESS "decryption", run "xray vlessenc" to get one`)
|
||||
}
|
||||
for _, user := range c.Users {
|
||||
a, err := user.Account.GetInstance()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.(*vless.Account).Flow != "" {
|
||||
return errors.New(`the xdrive transport doesn't support VLESS "flow"`)
|
||||
}
|
||||
}
|
||||
// every port gets its own listener on the same folder, and each of them accepts the same session
|
||||
if pl != nil && len(pl.Ports()) != 1 {
|
||||
return errors.New("the xdrive transport can only listen on one port")
|
||||
}
|
||||
default:
|
||||
return errors.New("the xdrive transport can only be used by VLESS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if c.ProxySettings != nil {
|
||||
@@ -340,6 +395,9 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
|
||||
}
|
||||
if err := validateXDrive(ts, senderSettings.StreamSettings, senderSettings.MultiplexSettings, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+30
-2
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
@@ -86,7 +87,13 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
user := server.User
|
||||
var conn stat.Connection
|
||||
|
||||
mbuf, _ := link.Reader.ReadMultiBuffer()
|
||||
var mbuf buf.MultiBuffer
|
||||
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()
|
||||
firstPayload := bytespool.Alloc(len)
|
||||
mbuf, _ = buf.SplitBytes(mbuf, firstPayload)
|
||||
@@ -234,7 +241,8 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
|
||||
reader := bufio.NewReader(rawConn)
|
||||
resp, err := http.ReadResponse(reader, req)
|
||||
if err != nil {
|
||||
rawConn.Close()
|
||||
return nil, err
|
||||
@@ -245,6 +253,11 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
|
||||
rawConn.Close()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -375,6 +388,21 @@ func (h *http2Conn) Close() error {
|
||||
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() {
|
||||
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return NewClient(ctx, config.(*ClientConfig))
|
||||
|
||||
@@ -153,7 +153,7 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
|
||||
return DecodedUDPPacket{}, ErrBadHeaderType
|
||||
}
|
||||
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
|
||||
if diff > 30 {
|
||||
return DecodedUDPPacket{}, ErrBadTimestamp
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ func (f *SlidingWindow) Add(counter uint64) {
|
||||
|
||||
if counter > f.last {
|
||||
lastBlockIndex := f.last >> swBlockBitLog
|
||||
diff := int(blockIndex - lastBlockIndex)
|
||||
diff := blockIndex - lastBlockIndex
|
||||
if diff > swRingBlocks {
|
||||
diff = swRingBlocks
|
||||
}
|
||||
|
||||
for i := 0; i < diff; i++ {
|
||||
for i := 0; i < int(diff); i++ {
|
||||
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
|
||||
f.ring[lastBlockIndex] = 0
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ func ReadClientRequestHeaderWithFixed(reader *StreamReader, fixedChunk []byte) (
|
||||
}
|
||||
|
||||
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
@@ -431,7 +431,7 @@ func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt [
|
||||
}
|
||||
|
||||
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
|
||||
diff := math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package socks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
)
|
||||
|
||||
@@ -14,6 +14,9 @@ func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, expectedRemote *ne
|
||||
PacketConn: udpConn,
|
||||
AssociatedTCPConn: tcpConn,
|
||||
}
|
||||
t.Timer = signal.CancelAfterInactivity(context.Background(), func() {
|
||||
t.Close()
|
||||
}, net.ConnIdleTimeout)
|
||||
t.ExpectedRemote.Store(expectedRemote)
|
||||
return t
|
||||
}
|
||||
@@ -60,9 +63,7 @@ func (c *TempUDPConn) RemoteAddr() net.Addr {
|
||||
}
|
||||
|
||||
func (c *TempUDPConn) SetTimeout(d time.Duration) {
|
||||
c.Timer = signal.CancelAfterInactivity(context.Background(), func() {
|
||||
c.Close()
|
||||
}, d)
|
||||
c.Timer.SetTimeout(d)
|
||||
}
|
||||
|
||||
func (c *TempUDPConn) Close() error {
|
||||
|
||||
@@ -144,7 +144,8 @@ func (t *Handler) Start() error {
|
||||
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
|
||||
return errors.New("iface not found")
|
||||
}
|
||||
return c.Control(func(fd uintptr) {
|
||||
var controlErr, sockErr error
|
||||
controlErr = c.Control(func(fd uintptr) {
|
||||
addrPort, _ := netip.ParseAddrPort(address)
|
||||
// skip loopback
|
||||
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
|
||||
@@ -152,9 +153,13 @@ func (t *Handler) Start() error {
|
||||
}
|
||||
err := setinterface(network, address, fd, iface)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
|
||||
sockErr = errors.New("[tun] falied to set interface").Base(err)
|
||||
}
|
||||
})
|
||||
if sockErr != nil {
|
||||
return sockErr
|
||||
}
|
||||
return controlErr
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -41,8 +41,9 @@ const (
|
||||
type packet struct {
|
||||
proto byte
|
||||
src, dst netip.Addr
|
||||
// payload is what follows the IP header
|
||||
// payload is what follows the IP header, without the last rest bytes of it when they are in other buffers
|
||||
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
|
||||
// with the number id, which has more after it or not
|
||||
fragment bool
|
||||
@@ -51,8 +52,8 @@ type packet struct {
|
||||
id uint32
|
||||
}
|
||||
|
||||
// parsePacket reads the IP header of b
|
||||
func parsePacket(b []byte) (p packet, ok bool) {
|
||||
// parsePacket reads the IP header of a packet that is b and rest bytes after it
|
||||
func parsePacket(b []byte, rest int) (p packet, ok bool) {
|
||||
if len(b) < ipv4HeaderSize {
|
||||
return p, false
|
||||
}
|
||||
@@ -60,7 +61,7 @@ func parsePacket(b []byte) (p packet, ok bool) {
|
||||
case 4:
|
||||
size := int(b[0]&0xf) * 4
|
||||
total := int(binary.BigEndian.Uint16(b[2:]))
|
||||
if size < ipv4HeaderSize || total < size || total > len(b) {
|
||||
if size < ipv4HeaderSize || total < size || total > len(b)+rest {
|
||||
return p, false
|
||||
}
|
||||
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
|
||||
@@ -69,19 +70,19 @@ func parsePacket(b []byte) (p packet, ok bool) {
|
||||
p.proto = b[9]
|
||||
p.src = netip.AddrFrom4([4]byte(b[12:16]))
|
||||
p.dst = netip.AddrFrom4([4]byte(b[16:20]))
|
||||
p.payload = b[size:total]
|
||||
p.payload, p.rest = b[size:min(total, len(b))], max(total-len(b), 0)
|
||||
case 6:
|
||||
if len(b) < ipv6HeaderSize {
|
||||
return p, false
|
||||
}
|
||||
total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:]))
|
||||
if total > len(b) {
|
||||
if total > len(b)+rest {
|
||||
return p, false
|
||||
}
|
||||
p.proto = b[6]
|
||||
p.src = netip.AddrFrom16([16]byte(b[8:24]))
|
||||
p.dst = netip.AddrFrom16([16]byte(b[24:40]))
|
||||
p.payload = b[ipv6HeaderSize:total]
|
||||
p.payload, p.rest = b[ipv6HeaderSize:min(total, len(b))], max(total-len(b), 0)
|
||||
// hop-by-hop options, routing, destination options
|
||||
for p.proto == 0 || p.proto == 43 || p.proto == 60 {
|
||||
if len(p.payload) < 8 {
|
||||
|
||||
+94
-14
@@ -71,6 +71,10 @@ type stack struct {
|
||||
udp *udpConnectionHandler
|
||||
// addresses are what connections are opened from
|
||||
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
|
||||
conns map[flow]*tcpConn
|
||||
@@ -149,26 +153,66 @@ func (s *stack) Close() error {
|
||||
|
||||
// read takes the packets of the tun until it is closed
|
||||
func (s *stack) read() {
|
||||
// a tun that does not split segments gives them as large as a packet can be
|
||||
b := make([]byte, packetRoom+1<<16)
|
||||
for {
|
||||
b, err := s.tun.ReadPacket(b)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if !closed {
|
||||
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
|
||||
var err error
|
||||
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)
|
||||
for err == nil {
|
||||
var p []byte
|
||||
if p, err = s.tun.ReadPacket(b); err == nil {
|
||||
s.Input(p)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.Input(b)
|
||||
}
|
||||
s.mu.Lock()
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if !closed {
|
||||
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *stack) Input(b []byte) {
|
||||
p, ok := parsePacket(b)
|
||||
s.input(b, 0)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// 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
|
||||
@@ -180,6 +224,10 @@ func (s *stack) Input(b []byte) {
|
||||
}
|
||||
// what was put together may be of two packets with one number, its checksum tells
|
||||
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 {
|
||||
p, ok = s.fragments.add(p)
|
||||
}
|
||||
@@ -196,6 +244,37 @@ func (s *stack) Input(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *stack) local(remote netip.Addr) (netip.Addr, error) {
|
||||
for _, a := range s.addresses {
|
||||
@@ -237,6 +316,7 @@ func (s *stack) handleTCP(p packet) {
|
||||
window: binary.BigEndian.Uint16(t[14:]),
|
||||
options: t[tcpHeaderSize:size],
|
||||
payload: t[size:],
|
||||
rest: p.rest,
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -283,7 +363,7 @@ func (s *stack) refuse(f flow, seg segment) {
|
||||
binary.BigEndian.PutUint32(t[4:], seg.ack)
|
||||
t[13] = tcpRst
|
||||
} else {
|
||||
ack := seg.seq + uint32(len(seg.payload))
|
||||
ack := seg.seq + uint32(len(seg.payload)+seg.rest)
|
||||
if seg.flags&(tcpSyn|tcpFin) != 0 {
|
||||
ack++
|
||||
}
|
||||
|
||||
+125
-37
@@ -65,6 +65,20 @@ func (l *link) Segments() bool {
|
||||
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
|
||||
func (l *link) WriteSegment(header []byte, payload [][]byte, mss int) error {
|
||||
data := bytes.Join(payload, nil)
|
||||
@@ -206,7 +220,7 @@ func (p *peer) receive() segment {
|
||||
func (p *peer) dialed() (segment, []byte) {
|
||||
p.t.Helper()
|
||||
b := p.packet()
|
||||
packet, ok := parsePacket(b)
|
||||
packet, ok := parsePacket(b, 0)
|
||||
if !ok || packet.proto != protoTCP || len(packet.payload) < tcpHeaderSize {
|
||||
p.t.Fatal("not a segment: ", b)
|
||||
}
|
||||
@@ -223,7 +237,7 @@ func (p *peer) segment(b []byte) segment {
|
||||
} else if checksum(sum(b[:size], 0)) != 0 {
|
||||
p.t.Fatal("wrong checksum of the ip header")
|
||||
}
|
||||
packet, ok := parsePacket(b)
|
||||
packet, ok := parsePacket(b, 0)
|
||||
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)
|
||||
}
|
||||
@@ -464,30 +478,90 @@ func TestTCPOutOfOrder(t *testing.T) {
|
||||
read(t, a.conn, "jklmno")
|
||||
}
|
||||
|
||||
// A segment is as large as the system makes it when the tun does not split what it reads.
|
||||
// A segment is as large as the system makes it when the tun does not split what it reads, and one that is large,
|
||||
// the next and all taken stays in the buffers that such a tun reads it into.
|
||||
func TestTCPLargeSegment(t *testing.T) {
|
||||
conns := make(chan accepted, 1)
|
||||
p, l := newPeer(t, "10.0.0.1:1234", "10.0.0.2:80")
|
||||
newTestStack(t, l, accept(conns))
|
||||
p.connect(nil)
|
||||
a := take(t, conns)
|
||||
for _, segments := range []bool{false, true} {
|
||||
conns := make(chan accepted, 1)
|
||||
p, l := newPeer(t, "10.0.0.1:1234", "10.0.0.2:80")
|
||||
l.segments = segments
|
||||
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)
|
||||
a := take(t, conns)
|
||||
|
||||
sent := make([]byte, 60000)
|
||||
rand.Read(sent)
|
||||
p.send(p.seq, tcpAck|tcpPsh, nil, sent)
|
||||
var received []byte
|
||||
for len(received) < len(sent) {
|
||||
mb, err := a.conn.(buf.Reader).ReadMultiBuffer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
// one that waits for the one before it, that one, one with a part that came already, small ones around
|
||||
// one that fits a buffer, and the rest
|
||||
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+uint32(part[0]), tcpAck|tcpPsh, nil, sent[part[0]:part[1]])
|
||||
}
|
||||
for _, b := range mb {
|
||||
received = append(received, b.Bytes()...)
|
||||
// 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
|
||||
for len(received) < len(sent) {
|
||||
mb, err := a.conn.(buf.Reader).ReadMultiBuffer()
|
||||
if err != nil {
|
||||
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 {
|
||||
// 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()...)
|
||||
}
|
||||
buf.ReleaseMulti(mb)
|
||||
}
|
||||
if !bytes.Equal(received, sent) {
|
||||
t.Fatal("segments: ", segments, ", 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")
|
||||
}
|
||||
buf.ReleaseMulti(mb)
|
||||
}
|
||||
if !bytes.Equal(received, sent) {
|
||||
t.Fatal("read something else than what was sent")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,13 +1156,20 @@ func TestTCPSynAfterClose(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for _, test := range []struct{ src, dst, other string }{
|
||||
{"10.0.0.1:1234", "10.0.0.2:53", "10.0.0.3:5353"},
|
||||
{"[fd00::1]:1234", "[fd00::2]:53", "[fd00::3]:5353"},
|
||||
large := make([]byte, 20000)
|
||||
rand.Read(large)
|
||||
for _, test := range []struct {
|
||||
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)
|
||||
p, l := newPeer(t, test.src, test.dst)
|
||||
l.segments = test.segments
|
||||
newTestStack(t, l, accept(conns))
|
||||
|
||||
var b [ipv6HeaderSize + udpHeaderSize]byte
|
||||
@@ -1105,6 +1186,13 @@ func TestUDPAndICMP(t *testing.T) {
|
||||
t.Fatal("read ", mb.String(), " from ", a.conn.RemoteAddr(), " to ", a.dest, ", error: ", err)
|
||||
}
|
||||
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} {
|
||||
answer := buf.FromBytes([]byte("defg"))
|
||||
@@ -1114,7 +1202,7 @@ func TestUDPAndICMP(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := p.packet()
|
||||
packet, ok := parsePacket(raw)
|
||||
packet, ok := parsePacket(raw, 0)
|
||||
u := packet.payload
|
||||
// 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 ||
|
||||
@@ -1133,12 +1221,12 @@ func TestUDPAndICMP(t *testing.T) {
|
||||
proto, request, reply = protoICMPv6, 128, 129
|
||||
}
|
||||
n = putIPHeader(b[:], proto, p.src.Addr(), p.dst.Addr())
|
||||
setIPLength(b[:n], n+12)
|
||||
l.in <- append(b[:n:n], request, 0, 0, 0, 1, 2, 3, 4, 'a', 'b', 'c', 'd')
|
||||
packet, ok := parsePacket(p.packet())
|
||||
setIPLength(b[:n], n+8+len(large))
|
||||
l.in <- append(append(b[:n:n], request, 0, 0, 0, 1, 2, 3, 4), large...)
|
||||
packet, ok := parsePacket(p.joined(), 0)
|
||||
m := packet.payload
|
||||
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: ", packet)
|
||||
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) {
|
||||
t.Fatal("wrong echo reply of ", len(m), " bytes from ", packet.src, " to ", packet.dst)
|
||||
}
|
||||
var ip [ipv6HeaderSize]byte
|
||||
size := putIPHeader(ip[:], proto, packet.src, packet.dst)
|
||||
@@ -1373,7 +1461,7 @@ func TestUDPDial(t *testing.T) {
|
||||
t.Fatal("wrong header of the packet to ", to, ": ", raw[:ipv4HeaderSize])
|
||||
}
|
||||
numbers[to][binary.BigEndian.Uint16(raw[4:])] = true
|
||||
packet, ok := parsePacket(raw)
|
||||
packet, ok := parsePacket(raw, 0)
|
||||
u := packet.payload
|
||||
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" {
|
||||
@@ -1395,7 +1483,7 @@ func TestUDPDial(t *testing.T) {
|
||||
if _, err := conn.WriteTo([]byte("abc"), &net.UDPAddr{IP: across.Addr().AsSlice(), Port: int(across.Port())}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if packet, ok := parsePacket(p.packet()); !ok || packet.proto != protoUDP || packet.dst != across.Addr() ||
|
||||
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.proto != protoUDP || packet.dst != across.Addr() ||
|
||||
netip.AddrPortFrom(packet.src, binary.BigEndian.Uint16(packet.payload)) != acrossLocal {
|
||||
t.Fatal("wrong packet to ", across, ": ", packet)
|
||||
}
|
||||
@@ -1407,7 +1495,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
|
||||
l.in <- ping(p.src.Addr(), netip.MustParseAddrPort(test.other).Addr())
|
||||
l.in <- ping(p.src.Addr(), local.Addr())
|
||||
if packet, ok := parsePacket(p.packet()); !ok || packet.src != local.Addr() || packet.dst != p.src.Addr() || packet.payload[0]&^129 != 0 {
|
||||
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.src != local.Addr() || packet.dst != p.src.Addr() || packet.payload[0]&^129 != 0 {
|
||||
t.Fatal("wrong echo reply: ", packet)
|
||||
}
|
||||
p.silent(20 * time.Millisecond)
|
||||
@@ -1873,7 +1961,7 @@ func TestFragments(t *testing.T) {
|
||||
if err := a.conn.(buf.Writer).WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(sent)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
packet, ok := parsePacket(p.joined())
|
||||
packet, ok := parsePacket(p.joined(), 0)
|
||||
u := packet.payload
|
||||
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) {
|
||||
@@ -1898,7 +1986,7 @@ func TestFragments(t *testing.T) {
|
||||
mixed := split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{1}, 2000))), 1024, 11)
|
||||
l.in <- mixed[0]
|
||||
l.in <- split(summed(udpPacket(p.src, p.dst, bytes.Repeat([]byte{2}, 2000))), 1024, 11)[1]
|
||||
packet, ok = parsePacket(p.joined())
|
||||
packet, ok = parsePacket(p.joined(), 0)
|
||||
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) {
|
||||
t.Fatal("wrong echo reply: ", packet.src, packet.dst, packet.proto, len(m))
|
||||
@@ -2040,7 +2128,7 @@ func TestChecksumsAndLoopback(t *testing.T) {
|
||||
l.in <- damaged(echo)
|
||||
p.silent(50 * time.Millisecond)
|
||||
l.in <- echo
|
||||
if packet, ok := parsePacket(p.packet()); !ok || packet.proto != proto || string(packet.payload[4:]) != "\x01\x02\x03\x04abcd" {
|
||||
if packet, ok := parsePacket(p.packet(), 0); !ok || packet.proto != proto || string(packet.payload[4:]) != "\x01\x02\x03\x04abcd" {
|
||||
t.Fatal("wrong echo reply: ", packet)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ const (
|
||||
// tcpParts of how many parts of what was written it is made at most
|
||||
tcpSegment = math.MaxUint16 - ipv6HeaderSize - tcpHeaderSize
|
||||
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 = 100 * time.Millisecond
|
||||
@@ -87,7 +90,9 @@ type segment struct {
|
||||
flags byte
|
||||
window uint16
|
||||
options []byte
|
||||
payload []byte
|
||||
// payload is without its last rest bytes when those are in other buffers of the stack, see stack.whole
|
||||
payload []byte
|
||||
rest int
|
||||
}
|
||||
|
||||
// nextOption returns the first option of o and what follows it, kind is zero when there is none
|
||||
@@ -749,6 +754,12 @@ func (c *tcpConn) space() uint32 {
|
||||
|
||||
// receive takes the payload of a segment. c.mu is held
|
||||
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
|
||||
if len(payload) == 0 && !fin {
|
||||
if int32(c.rcvNxt-seq) > 0 {
|
||||
@@ -789,9 +800,14 @@ func (c *tcpConn) receive(s segment) {
|
||||
return
|
||||
}
|
||||
|
||||
c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload)
|
||||
c.rcvLen += int32(len(payload))
|
||||
c.rcvNxt += uint32(len(payload))
|
||||
if keep {
|
||||
c.rcvQueue = c.stack.keep(c.rcvQueue, payload, s.rest)
|
||||
} else {
|
||||
size = len(payload)
|
||||
c.rcvQueue = buf.MergeBytes(c.rcvQueue, payload)
|
||||
}
|
||||
c.rcvLen += int32(size)
|
||||
c.rcvNxt += uint32(size)
|
||||
for !fin && len(c.rcvHeld) > 0 && int32(c.rcvNxt-c.rcvHeld[0].seq) >= 0 {
|
||||
h := c.rcvHeld[0]
|
||||
if before := int32(c.rcvNxt - h.seq); before < h.data.Len() {
|
||||
@@ -809,7 +825,7 @@ func (c *tcpConn) receive(s segment) {
|
||||
c.readable.Signal()
|
||||
// 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
|
||||
if fin || now || c.rcvNxt-c.rcvAcked >= tcpWindow/4 {
|
||||
if fin || now || c.rcvNxt-c.rcvAcked >= uint32(c.window/4) {
|
||||
c.sendAck()
|
||||
} else {
|
||||
c.arm(tcpTick)
|
||||
|
||||
+5
-2
@@ -16,10 +16,13 @@ type Tun interface {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Segments reports whether WriteSegment can be used
|
||||
// Segments reports whether ReadParts and WriteSegment can be used
|
||||
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
|
||||
// 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
|
||||
|
||||
+43
-1
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
@@ -22,6 +24,14 @@ type fdTun struct {
|
||||
// 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
|
||||
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
|
||||
closed bool
|
||||
@@ -29,7 +39,25 @@ type fdTun struct {
|
||||
|
||||
// newFdTun takes a file descriptor that does not block
|
||||
func newFdTun(fd, vnet int) *fdTun {
|
||||
return &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet}
|
||||
t := &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) {
|
||||
@@ -38,6 +66,20 @@ func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
|
||||
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 {
|
||||
// a virtio_net_hdr of zeros leaves nothing to the system
|
||||
var vnet [packetRoom]byte
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//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")
|
||||
}
|
||||
}
|
||||
+11
-14
@@ -16,8 +16,9 @@ import (
|
||||
)
|
||||
|
||||
type bind struct {
|
||||
ctx context.Context
|
||||
resolveFunc func(host string) (net.IP, error)
|
||||
listenFunc func() (net.PacketConn, error)
|
||||
listenFunc func(ctx context.Context) (net.PacketConn, error)
|
||||
downFunc func() error
|
||||
reserved []byte
|
||||
|
||||
@@ -26,6 +27,12 @@ type bind struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (b *bind) SetCtx(ctx context.Context) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.ctx = ctx
|
||||
}
|
||||
|
||||
func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -34,7 +41,7 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
|
||||
return nil, 0, conn.ErrBindAlreadyOpen
|
||||
}
|
||||
|
||||
c, err := b.listenFunc()
|
||||
c, err := b.listenFunc(b.ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -52,12 +59,9 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
|
||||
case <-ch:
|
||||
default:
|
||||
errors.LogErrorInner(context.Background(), err, "unexpected closed")
|
||||
b.mu.Lock()
|
||||
downFunc := b.downFunc
|
||||
b.mu.Unlock()
|
||||
if downFunc != nil {
|
||||
if b.downFunc != nil {
|
||||
go func() {
|
||||
common.Must(downFunc())
|
||||
common.Must(b.downFunc())
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -79,13 +83,6 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
|
||||
}, uint16(c.LocalAddr().(*net.UDPAddr).Port), nil
|
||||
}
|
||||
|
||||
// setDownFunc sets downFunc after the device is created, since the device may already be using the bind.
|
||||
func (b *bind) setDownFunc(f func() error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.downFunc = f
|
||||
}
|
||||
|
||||
func (b *bind) Close() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
+165
-149
@@ -7,6 +7,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -31,18 +32,14 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
conf *DeviceConfig
|
||||
policyManager policy.Manager
|
||||
dns dns.Client
|
||||
p policy.Manager
|
||||
|
||||
streamSettings *internet.MemoryStreamConfig
|
||||
uplinkCounter stats.Counter
|
||||
downlinkCounter stats.Counter
|
||||
|
||||
tun tun.Device
|
||||
tnet *Net
|
||||
dev *device.Device
|
||||
mu sync.Mutex
|
||||
canup *atomic.Bool
|
||||
tnet *Net
|
||||
bind *bind
|
||||
dev *device.Device
|
||||
ipc string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
@@ -124,17 +121,86 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockup := newblockup(tun)
|
||||
bind := &bind{}
|
||||
logger := &device.Logger{
|
||||
Verbosef: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Debug,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
Errorf: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Error,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
}
|
||||
dev := device.NewDevice(blockup, bind, logger)
|
||||
bind.ctx = context.Background()
|
||||
bind.resolveFunc = MakeResolveLocal(d, streamSettings)
|
||||
bind.listenFunc = func(ctx context.Context) (net.PacketConn, error) {
|
||||
dest, err := net.ParseDestination("udp:" + conf.Peers[0].Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pktConn net.PacketConn
|
||||
if streamSettings.FinalMask != nil {
|
||||
conn, err := streamSettings.FinalMask.DialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
pktConn = conn.(*net.PacketConnWrapper).PacketConn
|
||||
} else {
|
||||
conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch c := conn.(type) {
|
||||
case *net.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
}
|
||||
if uplinkCounter != nil || downlinkCounter != nil {
|
||||
pktConn = &PacketCounterConnection{
|
||||
PacketConn: pktConn,
|
||||
ReadCounter: downlinkCounter,
|
||||
WriteCounter: uplinkCounter,
|
||||
}
|
||||
}
|
||||
return pktConn, nil
|
||||
}
|
||||
bind.downFunc = dev.Down
|
||||
bind.reserved = conf.Reserved
|
||||
var cfg strings.Builder
|
||||
cfg.WriteString("private_key=" + conf.SecretKey + "\n")
|
||||
for _, peer := range conf.Peers {
|
||||
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
|
||||
if peer.PreSharedKey != "" {
|
||||
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
|
||||
}
|
||||
cfg.WriteString("endpoint=" + peer.Endpoint + "\n")
|
||||
for _, ip := range peer.AllowedIps {
|
||||
cfg.WriteString("allowed_ip=" + ip + "\n")
|
||||
}
|
||||
if peer.KeepAlive != "" {
|
||||
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
conf: conf,
|
||||
policyManager: p,
|
||||
dns: d,
|
||||
p: p,
|
||||
|
||||
streamSettings: streamSettings,
|
||||
uplinkCounter: uplinkCounter,
|
||||
downlinkCounter: downlinkCounter,
|
||||
|
||||
tun: tun,
|
||||
tnet: tnet,
|
||||
canup: blockup.canup,
|
||||
tnet: tnet,
|
||||
bind: bind,
|
||||
dev: dev,
|
||||
ipc: cfg.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -149,7 +215,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ob.CanSpliceCopy = 3
|
||||
dialer.SetOutboundGateway(ctx, ob)
|
||||
|
||||
if err := h.init(ctx); err != nil {
|
||||
if err := h.up(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -159,7 +225,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
newCtx, newCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
sessionPolicy := h.policyManager.ForLevel(0)
|
||||
sessionPolicy := h.p.ForLevel(0)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, func() {
|
||||
cancel()
|
||||
@@ -234,150 +300,72 @@ func (h *Handler) Close() (err error) {
|
||||
if h.dev != nil {
|
||||
h.dev.Close()
|
||||
h.dev = nil
|
||||
h.tun = nil
|
||||
} else if h.tun != nil {
|
||||
h.tun.Close()
|
||||
h.tun = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) init(ctx context.Context) error {
|
||||
func (h *Handler) up(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.tun == nil {
|
||||
if h.dev == nil {
|
||||
return errors.New("closed")
|
||||
}
|
||||
if h.dev != nil {
|
||||
return h.dev.Up()
|
||||
h.bind.SetCtx(ctx)
|
||||
if !h.canup.Load() {
|
||||
h.canup.Store(true)
|
||||
}
|
||||
resolveFunc := h.resolveLocal
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint)
|
||||
if len(h.ipc) > 0 {
|
||||
if err := h.dev.IpcSet(h.ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
h.ipc = ""
|
||||
}
|
||||
return h.dev.Up()
|
||||
}
|
||||
|
||||
func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig) func(host string) (net.IP, error) {
|
||||
return func(host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, _, err := d.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pktConn net.PacketConn
|
||||
if h.streamSettings.FinalMask != nil {
|
||||
conn, err := h.streamSettings.FinalMask.DialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
got := ips
|
||||
if streamSettings.SocketSettings != nil {
|
||||
var got4, got6 []net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
got4 = append(got4, ip)
|
||||
} else {
|
||||
got6 = append(got6, ip)
|
||||
}
|
||||
}
|
||||
pktConn = conn.(*net.PacketConnWrapper).PacketConn
|
||||
} else {
|
||||
conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch c := conn.(type) {
|
||||
case *net.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
}
|
||||
if h.uplinkCounter != nil || h.downlinkCounter != nil {
|
||||
pktConn = &PacketCounterConnection{
|
||||
PacketConn: pktConn,
|
||||
ReadCounter: h.downlinkCounter,
|
||||
WriteCounter: h.uplinkCounter,
|
||||
}
|
||||
}
|
||||
return pktConn, nil
|
||||
}
|
||||
// device.NewDevice may use the bind right away (Up -> BindUpdate -> Open),
|
||||
// so everything it reads must be set before creating the device.
|
||||
bind := &bind{
|
||||
resolveFunc: resolveFunc,
|
||||
listenFunc: listenFunc,
|
||||
reserved: h.conf.Reserved,
|
||||
}
|
||||
logger := &device.Logger{
|
||||
Verbosef: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Debug,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
Errorf: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Error,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
}
|
||||
dev := device.NewDevice(h.tun, bind, logger)
|
||||
bind.setDownFunc(dev.Down)
|
||||
var cfg strings.Builder
|
||||
cfg.WriteString("private_key=" + h.conf.SecretKey + "\n")
|
||||
for _, peer := range h.conf.Peers {
|
||||
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
|
||||
if peer.PreSharedKey != "" {
|
||||
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
|
||||
}
|
||||
cfg.WriteString("endpoint=" + peer.Endpoint + "\n")
|
||||
for _, ip := range peer.AllowedIps {
|
||||
cfg.WriteString("allowed_ip=" + ip + "\n")
|
||||
}
|
||||
if peer.KeepAlive != "" {
|
||||
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
|
||||
}
|
||||
}
|
||||
err := dev.IpcSet(cfg.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = dev.Up()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.dev = dev
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
got := ips
|
||||
if h.streamSettings.SocketSettings != nil {
|
||||
var got4, got6 []net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
got4 = append(got4, ip)
|
||||
} else {
|
||||
got6 = append(got6, ip)
|
||||
}
|
||||
}
|
||||
switch h.streamSettings.SocketSettings.DomainStrategy {
|
||||
case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP:
|
||||
got = ips
|
||||
case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4:
|
||||
got = got4
|
||||
case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6:
|
||||
got = got6
|
||||
case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46:
|
||||
got = got4
|
||||
if len(got) == 0 {
|
||||
got = got6
|
||||
}
|
||||
case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64:
|
||||
got = got6
|
||||
if len(got) == 0 {
|
||||
switch streamSettings.SocketSettings.DomainStrategy {
|
||||
case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP:
|
||||
got = ips
|
||||
case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4:
|
||||
got = got4
|
||||
case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6:
|
||||
got = got6
|
||||
case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46:
|
||||
got = got4
|
||||
if len(got) == 0 {
|
||||
got = got6
|
||||
}
|
||||
case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64:
|
||||
got = got6
|
||||
if len(got) == 0 {
|
||||
got = got4
|
||||
}
|
||||
}
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
}
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
return got[dice.Roll(len(got))], nil
|
||||
}
|
||||
return got[dice.Roll(len(got))], nil
|
||||
}
|
||||
|
||||
type UDPConnClient struct {
|
||||
@@ -510,3 +498,31 @@ func (c *cache) Cache(host string, saddr []string, ttl uint32) {
|
||||
deadline: time.Now().Add(time.Second * time.Duration(ttl)),
|
||||
}
|
||||
}
|
||||
|
||||
type blockup struct {
|
||||
tun.Device
|
||||
ch chan tun.Event
|
||||
canup *atomic.Bool
|
||||
}
|
||||
|
||||
func newblockup(t tun.Device) *blockup {
|
||||
ch := make(chan tun.Event, cap(t.Events()))
|
||||
canup := &atomic.Bool{}
|
||||
go func() {
|
||||
for e := range t.Events() {
|
||||
if e&tun.EventUp != 0 && !canup.Load() {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case ch <- e:
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return &blockup{Device: t, ch: ch, canup: canup}
|
||||
}
|
||||
|
||||
func (b *blockup) Events() <-chan tun.Event {
|
||||
return b.ch
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func (s *Server) Start() error {
|
||||
if s.src.Address.Family().IsDomain() {
|
||||
return errors.New("address is domain")
|
||||
}
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
listenFunc := func(context.Context) (net.PacketConn, error) {
|
||||
var pktConn net.PacketConn
|
||||
var err error
|
||||
if s.streamSettings.FinalMask != nil {
|
||||
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"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/core"
|
||||
"github.com/xtls/xray-core/proxy/dokodemo"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
v2http "github.com/xtls/xray-core/proxy/http"
|
||||
v2httptest "github.com/xtls/xray-core/testing/servers/http"
|
||||
@@ -377,3 +379,88 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
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
|
||||
}
|
||||
@@ -22,6 +22,12 @@ type interConn struct {
|
||||
|
||||
client bool
|
||||
user *protocol.MemoryUser
|
||||
lock chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *interConn) unlock() {
|
||||
<-c.lock
|
||||
}
|
||||
|
||||
func (c *interConn) User() *protocol.MemoryUser {
|
||||
@@ -33,6 +39,9 @@ func (c *interConn) Read(b []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (c *interConn) Write(b []byte) (int, error) {
|
||||
c.lock <- struct{}{}
|
||||
defer c.unlock()
|
||||
|
||||
if c.client {
|
||||
c.client = false
|
||||
if _, err := c.stream.Write(append(quicvarint.Append(nil, FrameTypeTCPRequest), b...)); err != nil {
|
||||
@@ -40,11 +49,20 @@ func (c *interConn) Write(b []byte) (int, error) {
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
return c.stream.Write(b)
|
||||
}
|
||||
|
||||
func (c *interConn) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
select {
|
||||
case c.lock <- struct{}{}:
|
||||
defer c.unlock()
|
||||
default:
|
||||
c.stream.CancelRead(0)
|
||||
c.stream.CancelWrite(0)
|
||||
return nil
|
||||
}
|
||||
c.stream.CancelRead(0)
|
||||
return c.stream.Close()
|
||||
}
|
||||
|
||||
@@ -257,6 +257,7 @@ func (c *client) tcp(ctx context.Context) (stat.Connection, error) {
|
||||
remote: c.conn.RemoteAddr(),
|
||||
|
||||
client: true,
|
||||
lock: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ func (h *httpHandler) StreamDispatcher(ft http3.FrameType, stream *quic.Stream,
|
||||
remote: h.conn.RemoteAddr(),
|
||||
|
||||
user: h.user,
|
||||
lock: make(chan struct{}, 1),
|
||||
})
|
||||
return true, nil
|
||||
default:
|
||||
|
||||
@@ -68,7 +68,7 @@ func KeyLogWriterFromConfig(c *Config) io.Writer {
|
||||
return nil
|
||||
}
|
||||
|
||||
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644)
|
||||
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log")
|
||||
}
|
||||
|
||||
@@ -28,3 +28,22 @@ func (v *SocketConfig) ParseTFOValue() int {
|
||||
}
|
||||
return tfo
|
||||
}
|
||||
|
||||
// Dial should fail on bind errors, return this to indicate such errors.
|
||||
type BindError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *BindError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *BindError) Error() string {
|
||||
return "failed to bind to interface: " + e.err.Error()
|
||||
}
|
||||
|
||||
func NewBindError(err error) *BindError {
|
||||
return &BindError{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,15 +136,15 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
if config.Interface != "" {
|
||||
iface, err := net.InterfaceByName(config.Interface)
|
||||
if err != nil {
|
||||
return errors.New("failed to get interface ", config.Interface).Base(err)
|
||||
return NewBindError(errors.New("failed to get interface ", config.Interface).Base(err))
|
||||
}
|
||||
if network == "tcp6" || network == "udp6" {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil {
|
||||
return errors.New("failed to set IPV6_BOUND_IF").Base(err)
|
||||
return NewBindError(errors.New("failed to set IPV6_BOUND_IF").Base(err))
|
||||
}
|
||||
} else {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil {
|
||||
return errors.New("failed to set IP_BOUND_IF").Base(err)
|
||||
return NewBindError(errors.New("failed to set IP_BOUND_IF").Base(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
|
||||
if config.Interface != "" {
|
||||
if err := syscall.BindToDevice(int(fd), config.Interface); err != nil {
|
||||
return errors.New("failed to set Interface").Base(err)
|
||||
return NewBindError(errors.New("failed to set Interface").Base(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
if config.Interface != "" {
|
||||
inf, err := net.InterfaceByName(config.Interface)
|
||||
if err != nil {
|
||||
return errors.New("failed to find the interface").Base(err)
|
||||
return NewBindError(errors.New("failed to find the interface").Base(err))
|
||||
}
|
||||
// easy way to check if the address is ipv4
|
||||
isV4 := strings.Contains(address, ".")
|
||||
@@ -47,20 +47,20 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
binary.BigEndian.PutUint32(bytes[:], uint32(inf.Index))
|
||||
idx := *(*uint32)(unsafe.Pointer(&bytes[0]))
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)); err != nil {
|
||||
return errors.New("failed to set IP_UNICAST_IF").Base(err)
|
||||
return NewBindError(errors.New("failed to set IP_UNICAST_IF").Base(err))
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) {
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, int(idx)); err != nil {
|
||||
return errors.New("failed to set IP_MULTICAST_IF").Base(err)
|
||||
return NewBindError(errors.New("failed to set IP_MULTICAST_IF").Base(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, inf.Index); err != nil {
|
||||
return errors.New("failed to set IPV6_UNICAST_IF").Base(err)
|
||||
return NewBindError(errors.New("failed to set IPV6_UNICAST_IF").Base(err))
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) {
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_IF, inf.Index); err != nil {
|
||||
return errors.New("failed to set IPV6_MULTICAST_IF").Base(err)
|
||||
return NewBindError(errors.New("failed to set IPV6_MULTICAST_IF").Base(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,19 +68,28 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
}
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
lc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply external controller")
|
||||
}
|
||||
}
|
||||
return c.Control(func(fd uintptr) {
|
||||
if sockopt != nil {
|
||||
if err := applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); err != nil {
|
||||
errors.LogInfo(ctx, err, "failed to apply socket options")
|
||||
if sockopt != nil || len(Controllers) > 0 {
|
||||
lc.Control = func(network, _ string, c syscall.RawConn) error {
|
||||
// ListenPacket supplies a local address; dialer controllers need the destination.
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, destAddr.String(), c); err != nil {
|
||||
return errors.New("failed to apply external controller").Base(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
if sockopt != nil {
|
||||
var sockErr, controlErr error
|
||||
controlErr = c.Control(func(fd uintptr) {
|
||||
if sockErr = applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); sockErr != nil {
|
||||
errors.LogInfoInner(ctx, sockErr, "failed to apply socket options")
|
||||
}
|
||||
})
|
||||
if _, ok := sockErr.(*BindError); ok {
|
||||
return sockErr
|
||||
}
|
||||
return controlErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String())
|
||||
if err != nil {
|
||||
@@ -128,16 +137,22 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
dialer.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply external controller")
|
||||
return errors.New("failed to apply external controller").Base(err)
|
||||
}
|
||||
}
|
||||
return c.Control(func(fd uintptr) {
|
||||
if sockopt != nil {
|
||||
if err := applyOutboundSocketOptions(network, address, fd, sockopt); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply socket options")
|
||||
if sockopt != nil {
|
||||
var sockErr, controlErr error
|
||||
controlErr = c.Control(func(fd uintptr) {
|
||||
if sockErr = applyOutboundSocketOptions(network, address, fd, sockopt); sockErr != nil {
|
||||
errors.LogInfoInner(ctx, sockErr, "failed to apply socket options")
|
||||
}
|
||||
})
|
||||
if _, ok := sockErr.(*BindError); ok {
|
||||
return sockErr
|
||||
}
|
||||
})
|
||||
return controlErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +198,7 @@ func UseAlternativeSystemDialer(dialer SystemDialer) {
|
||||
|
||||
// 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.
|
||||
// It receives the network and resolved remote address, as in net.Dialer.Control.
|
||||
// It only works when effective dialer is the default dialer.
|
||||
//
|
||||
// xray:api:beta
|
||||
|
||||
@@ -466,7 +466,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
|
||||
}
|
||||
|
||||
if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" {
|
||||
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644)
|
||||
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log")
|
||||
} else {
|
||||
|
||||
@@ -3,46 +3,156 @@ package xdrive
|
||||
import (
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"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/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/reality"
|
||||
"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"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errStalled = errors.New("the request made no progress")
|
||||
|
||||
type serviceTransport struct {
|
||||
plain http.RoundTripper
|
||||
secure http.RoundTripper
|
||||
stall time.Duration
|
||||
}
|
||||
|
||||
// RoundTrip ends a request that stands still for t.stall, with no answer that
|
||||
// long after its body went out or no byte of either body, but not one that just
|
||||
// runs long: a large object on a slow link takes minutes and is fine as long as
|
||||
// its bytes move
|
||||
func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
next := t.plain
|
||||
if r.URL.Scheme == "https" {
|
||||
return t.secure.RoundTrip(r)
|
||||
next = t.secure
|
||||
}
|
||||
return t.plain.RoundTrip(r)
|
||||
|
||||
ctx, cancel := context.WithCancelCause(r.Context())
|
||||
timer := time.AfterFunc(t.stall, func() { cancel(errStalled) })
|
||||
alive := func() { timer.Reset(t.stall) }
|
||||
end := func() {
|
||||
timer.Stop()
|
||||
cancel(nil)
|
||||
}
|
||||
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 {
|
||||
r.Body = &progressBody{ReadCloser: r.Body, alive: alive}
|
||||
}
|
||||
// a body sent again by the transport itself, after a GOAWAY or on a reused
|
||||
// connection that failed, moves just the same
|
||||
if getBody := r.GetBody; getBody != nil {
|
||||
r.GetBody = func() (io.ReadCloser, error) {
|
||||
body, err := getBody()
|
||||
if err != nil || body == http.NoBody {
|
||||
return body, err
|
||||
}
|
||||
return &progressBody{ReadCloser: body, alive: alive}, nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := next.RoundTrip(r)
|
||||
if err != nil {
|
||||
err = stalled(ctx, err)
|
||||
end()
|
||||
return nil, err
|
||||
}
|
||||
alive()
|
||||
resp.Body = &progressBody{ReadCloser: resp.Body, alive: alive, end: end, ctx: ctx}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func newServiceClient(streamSettings *internet.MemoryStreamConfig, timeout time.Duration, maxConns int) *http.Client {
|
||||
// stalled returns errStalled for an error that the stall gave, which http2 reports
|
||||
// as a plain cancel
|
||||
func stalled(ctx context.Context, err error) error {
|
||||
if context.Cause(ctx) == errStalled {
|
||||
return errStalled
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type progressBody struct {
|
||||
io.ReadCloser
|
||||
alive func()
|
||||
end func()
|
||||
// ctx is that of the request, for the body of its response
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (b *progressBody) Read(p []byte) (int, error) {
|
||||
// http2 reads a request body up to 512 KiB at a time and sends it all before
|
||||
// the next read, which on a slow link could look like no progress
|
||||
n, err := b.ReadCloser.Read(p[:min(len(p), 16<<10)])
|
||||
if n > 0 {
|
||||
b.alive()
|
||||
}
|
||||
if err != nil && err != io.EOF && b.ctx != nil {
|
||||
err = stalled(b.ctx, err)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *progressBody) Close() error {
|
||||
err := b.ReadCloser.Close()
|
||||
if b.end != nil {
|
||||
b.end()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newServiceClient returns a client whose requests end after stall without
|
||||
// progress, see serviceTransport.RoundTrip, and whose connections close when
|
||||
// connCtx is done. Clients that share h1Hosts share which hosts answer over h1
|
||||
func newServiceClient(connCtx context.Context, h1Hosts *sync.Map, streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
realityConfig *reality.Config
|
||||
sockopt *internet.SocketConfig
|
||||
fronting *net.Destination
|
||||
tlsConfig *tls.Config
|
||||
sockopt *internet.SocketConfig
|
||||
fronting *net.Destination
|
||||
keepAlive = net.ChromeH2KeepAlivePeriod
|
||||
// h3 keeps alive only when asked to, see below
|
||||
h3KeepAlive time.Duration
|
||||
)
|
||||
if streamSettings != nil {
|
||||
tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
|
||||
realityConfig = reality.ConfigFromStreamSettings(streamSettings)
|
||||
sockopt = streamSettings.SocketSettings
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
overHTTP2 := allowsHTTP2(tlsConfig, realityConfig)
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{}
|
||||
}
|
||||
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
|
||||
|
||||
dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
|
||||
host, err := net.ParseDestination("tcp:" + addr)
|
||||
// fronting takes only the address and port, the network stays that of the
|
||||
// dial, which is UDP for h3 though the destination is built as TCP
|
||||
dial := func(ctx context.Context, network, addr string) (net.Conn, net.Destination, error) {
|
||||
host, err := net.ParseDestination(network + ":" + addr)
|
||||
if err != nil {
|
||||
return nil, host, errors.New("bad address: ", addr).Base(err)
|
||||
}
|
||||
@@ -56,74 +166,146 @@ func newServiceClient(streamSettings *internet.MemoryStreamConfig, timeout time.
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
if streamSettings.FinalMask != nil {
|
||||
conn, err = streamSettings.FinalMask.DialTCP(ctx, target)
|
||||
} else {
|
||||
switch {
|
||||
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)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, host, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
return conn, host, nil
|
||||
// h3 closes its QUIC connection instead, as the server never hears of a UDP
|
||||
// socket that closes
|
||||
if network == "udp" {
|
||||
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) {
|
||||
conn, _, err := dial(ctx, addr)
|
||||
conn, _, err := dial(ctx, "tcp", addr)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
dialTLS := func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
conn, host, err := dial(ctx, addr)
|
||||
// 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) {
|
||||
conn, host, err := dial(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if realityConfig != nil {
|
||||
return reality.UClient(conn, realityConfig, ctx, host)
|
||||
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
||||
var tlsConn net.Conn
|
||||
if fingerprint == nil {
|
||||
tlsConn = tls.Client(conn, gotlsConfig)
|
||||
} else {
|
||||
tlsConn = tls.UClient(conn, gotlsConfig, fingerprint)
|
||||
}
|
||||
|
||||
gotlsConfig := &gotls.Config{ServerName: host.Address.String()}
|
||||
if tlsConfig != nil {
|
||||
gotlsConfig = tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
||||
// the dial outlives the request that started it, and a handshake that hangs
|
||||
// would keep the one connection of the host to itself
|
||||
ctx, cancel := context.WithTimeout(ctx, stall)
|
||||
defer cancel()
|
||||
if err := tlsConn.(tls.Interface).HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if len(gotlsConfig.NextProtos) != 1 {
|
||||
if overHTTP2 {
|
||||
gotlsConfig.NextProtos = []string{"h2"}
|
||||
} else {
|
||||
gotlsConfig.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
|
||||
uconn := tls.UClient(conn, gotlsConfig, fingerprint)
|
||||
if err := uconn.(*tls.UConn).HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return uconn, nil
|
||||
}
|
||||
}
|
||||
return tls.Client(conn, gotlsConfig), nil
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
var secure http.RoundTripper
|
||||
if overHTTP2 {
|
||||
secure = &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *gotls.Config) (net.Conn, error) {
|
||||
return dialTLS(ctx, addr)
|
||||
},
|
||||
IdleConnTimeout: net.ConnIdleTimeout,
|
||||
}
|
||||
} else {
|
||||
secure = &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialTLS(ctx, addr)
|
||||
},
|
||||
secure := func(connsPerHost int) *http.Transport {
|
||||
return &http.Transport{
|
||||
DialTLSContext: dialTLS,
|
||||
ForceAttemptHTTP2: true,
|
||||
IdleConnTimeout: net.ConnIdleTimeout,
|
||||
MaxIdleConns: maxConns,
|
||||
MaxIdleConnsPerHost: maxConns,
|
||||
MaxConnsPerHost: maxConns,
|
||||
MaxConnsPerHost: connsPerHost,
|
||||
HTTP2: &http.HTTP2Config{SendPingTimeout: keepAlive},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,18 +318,165 @@ func newServiceClient(streamSettings *internet.MemoryStreamConfig, timeout time.
|
||||
MaxIdleConnsPerHost: maxConns,
|
||||
MaxConnsPerHost: maxConns,
|
||||
},
|
||||
secure: secure,
|
||||
secure: https,
|
||||
stall: stall,
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func allowsHTTP2(tlsConfig *tls.Config, realityConfig *reality.Config) bool {
|
||||
if realityConfig != nil {
|
||||
return true
|
||||
}
|
||||
if tlsConfig == nil {
|
||||
return false
|
||||
}
|
||||
return len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "h2"
|
||||
// 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
|
||||
// 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
|
||||
type secureTransport struct {
|
||||
h2, h1 http.RoundTripper
|
||||
h1Hosts *sync.Map
|
||||
}
|
||||
|
||||
func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
if _, ok := t.h1Hosts.Load(r.URL.Host); ok {
|
||||
return t.h1.RoundTrip(r)
|
||||
}
|
||||
resp, err := t.h2.RoundTrip(r)
|
||||
if err == nil && resp.ProtoMajor == 1 {
|
||||
t.h1Hosts.Store(r.URL.Host, true)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
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/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"
|
||||
)
|
||||
|
||||
@@ -68,7 +85,7 @@ func sniForSettings(t *testing.T, serverName string) string {
|
||||
driveFilesURL = "https://www.googleapis.com/drive/v3/files"
|
||||
defer func() { driveFilesURL = prev }()
|
||||
|
||||
client := newServiceClient(settings, 5*time.Second, 8)
|
||||
client := newServiceClient(context.Background(), new(sync.Map), settings, 5*time.Second, 8)
|
||||
req, err := http.NewRequest(http.MethodGet, driveFilesURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
@@ -99,3 +116,874 @@ func TestServiceSNIOverride(t *testing.T) {
|
||||
t.Fatalf("explicit serverName gave SNI %q, want www.google.com", got)
|
||||
}
|
||||
}
|
||||
|
||||
// slowReader hands out one byte every gap, the way a body crosses a slow link
|
||||
type slowReader struct {
|
||||
left int
|
||||
gap time.Duration
|
||||
}
|
||||
|
||||
func (r *slowReader) Read(p []byte) (int, error) {
|
||||
if r.left == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
time.Sleep(r.gap)
|
||||
r.left--
|
||||
p[0] = 'x'
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// a request that keeps moving outlives the stall limit many times over, one
|
||||
// that stands still does not get much past it
|
||||
func TestServiceClientEndsOnlyStalledRequests(t *testing.T) {
|
||||
const stall = 200 * time.Millisecond
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(io.Discard, r.Body)
|
||||
switch r.URL.Path {
|
||||
case "/slow":
|
||||
for i := 0; i < 20; i++ {
|
||||
w.Write([]byte("x"))
|
||||
w.(http.Flusher).Flush()
|
||||
time.Sleep(stall / 4)
|
||||
}
|
||||
case "/stuck":
|
||||
w.Write([]byte("x"))
|
||||
w.(http.Flusher).Flush()
|
||||
time.Sleep(5 * stall)
|
||||
case "/silent":
|
||||
time.Sleep(5 * stall)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := newServiceClient(context.Background(), new(sync.Map), &internet.MemoryStreamConfig{}, stall, 8)
|
||||
|
||||
fetch := func(path string, body io.Reader) (int, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, server.URL+path, body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
got, err := io.ReadAll(resp.Body)
|
||||
return len(got), err
|
||||
}
|
||||
|
||||
if _, err := fetch("/", &slowReader{left: 20, gap: stall / 4}); err != nil {
|
||||
t.Errorf("a slow upload failed: %v", err)
|
||||
}
|
||||
if n, err := fetch("/slow", nil); err != nil || n != 20 {
|
||||
t.Errorf("a slow download gave %d bytes, %v", n, err)
|
||||
}
|
||||
for _, path := range []string{"/stuck", "/silent"} {
|
||||
start := time.Now()
|
||||
_, err := fetch(path, nil)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// a body that the transport sends again by itself keeps the request going too
|
||||
replay := &serviceTransport{stall: stall, plain: roundTrip(func(r *http.Request) (*http.Response, error) {
|
||||
body, err := r.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for b := make([]byte, 1); ; time.Sleep(stall / 4) {
|
||||
if _, err := body.Read(b); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := context.Cause(r.Context()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
|
||||
})}
|
||||
req, err := http.NewRequest(http.MethodPost, "http://drive/", bytes.NewReader(make([]byte, 20)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp, err := replay.RoundTrip(req); err != nil {
|
||||
t.Errorf("a body sent again failed: %v", err)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// over TLS a request that stands still ends the same way, and says so, on h2
|
||||
// as on h1
|
||||
func TestServiceClientStallOverTLS(t *testing.T) {
|
||||
const stall = 200 * time.Millisecond
|
||||
for _, protos := range [][]string{chromeALPN, {"http/1.1"}} {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: protos}, 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(server.URL + path)
|
||||
if err == nil {
|
||||
if resp.Proto != picked(protos) {
|
||||
t.Fatalf("%s went over %s", path, resp.Proto)
|
||||
}
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
}
|
||||
if !errors.Is(err, errStalled) {
|
||||
t.Errorf("server %v: %s gave %v, want %v", protos, path, err, errStalled)
|
||||
}
|
||||
if took := time.Since(start); took > 4*stall {
|
||||
t.Errorf("server %v: %s took %v to give up", protos, path, took)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tlsServer is a TLS server that keeps the last hello it got and counts its
|
||||
// connections, and those closed
|
||||
type tlsServer struct {
|
||||
*httptest.Server
|
||||
mu sync.Mutex
|
||||
hello *gotls.ClientHelloInfo
|
||||
conns atomic.Int32
|
||||
closed atomic.Int32
|
||||
}
|
||||
|
||||
func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc) *tlsServer {
|
||||
t.Helper()
|
||||
|
||||
s := &tlsServer{Server: httptest.NewUnstartedServer(handler)}
|
||||
config.GetConfigForClient = func(hello *gotls.ClientHelloInfo) (*gotls.Config, error) {
|
||||
s.mu.Lock()
|
||||
s.hello = hello
|
||||
s.mu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
s.TLS = config
|
||||
s.Config.ConnState = func(_ net.Conn, state http.ConnState) {
|
||||
switch state {
|
||||
case http.StateNew:
|
||||
s.conns.Add(1)
|
||||
case http.StateClosed:
|
||||
s.closed.Add(1)
|
||||
}
|
||||
}
|
||||
s.StartTLS()
|
||||
t.Cleanup(s.Close)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *tlsServer) lastHello() *gotls.ClientHelloInfo {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.hello
|
||||
}
|
||||
|
||||
// settings pins the certificate of the server in config
|
||||
func (s *tlsServer) settings(config *tls.Config) *internet.MemoryStreamConfig {
|
||||
config.PinnedPeerCertSha256 = [][]byte{tls.GenerateCertHash(s.Certificate())}
|
||||
return &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: config}
|
||||
}
|
||||
|
||||
// picked is the protocol a server with ALPN protos answers a Chrome hello with
|
||||
func picked(protos []string) string {
|
||||
if protos[0] == "h2" {
|
||||
return "HTTP/2.0"
|
||||
}
|
||||
return "HTTP/1.1"
|
||||
}
|
||||
|
||||
var chromeALPN = []string{"h2", "http/1.1"}
|
||||
|
||||
// the hello offers the ALPN of the fingerprint whatever "alpn" says, or with Go
|
||||
// TLS that of the config, h2 and http/1.1 by default, and the client speaks what
|
||||
// the server picks from it
|
||||
func TestServiceClientSpeaksWhatServerPicks(t *testing.T) {
|
||||
for _, protos := range [][]string{chromeALPN, {"http/1.1"}} {
|
||||
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"}} {
|
||||
client := newServiceClient(context.Background(), new(sync.Map), server.settings(config), 5*time.Second, 8)
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("server %v, alpn %v, fingerprint %q: %v", protos, config.NextProtocol, config.Fingerprint, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.Proto != picked(protos) {
|
||||
t.Errorf("server %v, alpn %v, fingerprint %q: went over %s", protos, config.NextProtocol, config.Fingerprint, resp.Proto)
|
||||
}
|
||||
if offered := server.lastHello().SupportedProtos; !slices.Equal(offered, chromeALPN) {
|
||||
t.Errorf("server %v, alpn %v, fingerprint %q: the hello offered %v, want %v", protos, config.NextProtocol, config.Fingerprint, offered, chromeALPN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// with no tlsSettings the hello is that of Chrome too, which unlike Go TLS
|
||||
// offers certificate compression
|
||||
func TestServiceClientDefaultsToChrome(t *testing.T) {
|
||||
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)
|
||||
// the test certificate is not trusted, the hello is all that is needed
|
||||
if resp, err := client.Get(server.URL); err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
hello := server.lastHello()
|
||||
if hello == nil {
|
||||
t.Fatal("no hello")
|
||||
}
|
||||
if !slices.Equal(hello.SupportedProtos, chromeALPN) || !slices.Contains(hello.Extensions, 27) {
|
||||
t.Errorf("the hello offered %v with extensions %v, not like Chrome", hello.SupportedProtos, hello.Extensions)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// first ones as the later ones, and next to a host that answers over h1
|
||||
func TestServiceClientOneConnection(t *testing.T) {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
})
|
||||
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)
|
||||
get := func(url string) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
for i := range 2 {
|
||||
if i == 1 {
|
||||
get(h1.URL)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for range driveMaxInflight {
|
||||
wg.Go(func() { get(server.URL) })
|
||||
}
|
||||
wg.Wait()
|
||||
if n := server.conns.Load(); n != 1 {
|
||||
t.Errorf("round %d: %d parallel requests took %d connections", i, driveMaxInflight, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// in the format of xray tls ech, for public name public.example
|
||||
const (
|
||||
testECHConfigList = "AEH+DQA9AAAgACBziE85FLxJySxZAPRHs8CDCbuz1h76I9pLhctC/kgScAAEAAEAAQAOcHVibGljLmV4YW1wbGUAAA=="
|
||||
testECHServerKeys = "ACD2AjC5yuBQ3430geNDLdIRndloCry/DBb9ZeYp0TmeowBB/g0APQAAIAAgc4hPORS8ScksWQD0R7PAgwm7s9Ye+iPaS4XLQv5IEnAABAABAAEADnB1YmxpYy5leGFtcGxlAAA="
|
||||
)
|
||||
|
||||
// a server that speaks only h1, or gets ALPN http/1.1 inside ECH or from Go TLS,
|
||||
// answers over h1, and after that answer parallel requests get more than one
|
||||
// connection
|
||||
func TestServiceClientHTTP1(t *testing.T) {
|
||||
keys, err := base64.StdEncoding.DecodeString(testECHServerKeys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
echKeys, err := tls.ConvertToGoECHKeys(keys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ech := &tls.Config{ServerName: "example.com", NextProtocol: []string{"http/1.1"}, EchConfigList: testECHConfigList}
|
||||
|
||||
for _, c := range []struct {
|
||||
protos, seen []string
|
||||
config *tls.Config
|
||||
}{
|
||||
{[]string{"http/1.1"}, chromeALPN, &tls.Config{}},
|
||||
{chromeALPN, []string{"http/1.1"}, ech},
|
||||
{chromeALPN, []string{"http/1.1"}, &tls.Config{NextProtocol: []string{"http/1.1"}, Fingerprint: "unsafe"}},
|
||||
} {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: c.protos, EncryptedClientHelloKeys: echKeys}, func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
})
|
||||
client := newServiceClient(context.Background(), new(sync.Map), server.settings(c.config), 5*time.Second, 8)
|
||||
get := func() {
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.Proto != "HTTP/1.1" || resp.TLS.ECHAccepted != (c.config.EchConfigList != "") {
|
||||
t.Errorf("server %v, ALPN %v: went over %s, ECH accepted %v", c.protos, c.seen, resp.Proto, resp.TLS.ECHAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
get()
|
||||
first := server.conns.Load()
|
||||
var wg sync.WaitGroup
|
||||
for range 4 {
|
||||
wg.Go(get)
|
||||
}
|
||||
wg.Wait()
|
||||
if offered := server.lastHello().SupportedProtos; !slices.Equal(offered, c.seen) {
|
||||
t.Errorf("server %v: saw ALPN %v, want %v", c.protos, offered, c.seen)
|
||||
}
|
||||
if n := server.conns.Load() - first; n < 2 {
|
||||
t.Errorf("server %v, ALPN %v: 4 parallel requests over h1 took %d connections", c.protos, c.seen, n)
|
||||
}
|
||||
}
|
||||
|
||||
// without the keys the server gets the outer hello, which offers the ALPN of
|
||||
// Chrome
|
||||
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 {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if hello := outer.lastHello(); hello == nil {
|
||||
t.Error("no outer hello")
|
||||
} else if !slices.Equal(hello.SupportedProtos, chromeALPN) {
|
||||
t.Errorf("the outer hello offered %v, want %v", hello.SupportedProtos, chromeALPN)
|
||||
}
|
||||
}
|
||||
|
||||
// muteFirst leaves the first connection it accepts unanswered
|
||||
type muteFirst struct {
|
||||
net.Listener
|
||||
muted net.Conn
|
||||
}
|
||||
|
||||
func (l *muteFirst) Accept() (net.Conn, error) {
|
||||
if l.muted == nil {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.muted = conn
|
||||
}
|
||||
return l.Listener.Accept()
|
||||
}
|
||||
|
||||
// a handshake that hangs ends after stall, and does not keep the host from
|
||||
// getting another connection
|
||||
func TestServiceClientHungHandshake(t *testing.T) {
|
||||
const stall = 200 * time.Millisecond
|
||||
for _, fingerprint := range []string{"", "unsafe"} {
|
||||
server := &tlsServer{Server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))}
|
||||
server.Listener = &muteFirst{Listener: server.Listener}
|
||||
server.StartTLS()
|
||||
t.Cleanup(server.Close)
|
||||
client := newServiceClient(context.Background(), new(sync.Map), server.settings(&tls.Config{Fingerprint: fingerprint}), stall, 8)
|
||||
|
||||
if _, err := client.Get(server.URL); err == nil {
|
||||
t.Fatalf("fingerprint %q: a request got through a handshake that hung", fingerprint)
|
||||
}
|
||||
for i := range 2 {
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprint %q: request %d after a handshake that hung: %v", fingerprint, i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a connection whose handshake failed is closed, not left to the garbage
|
||||
// collector
|
||||
func TestServiceClientClosesFailedHandshake(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
for _, fingerprint := range []string{"", "unsafe"} {
|
||||
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())
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// not TLS, so the handshake fails at once
|
||||
conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := io.Copy(io.Discard, conn); err != nil {
|
||||
t.Errorf("fingerprint %q: the client left the connection open: %v", fingerprint, err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
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,6 +7,7 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
splithttp "github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
@@ -35,6 +36,7 @@ type Config struct {
|
||||
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"`
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -153,11 +155,18 @@ func (x *Config) GetTemplate() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetXmux() *splithttp.XmuxConfig {
|
||||
if x != nil {
|
||||
return x.Xmux
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_xdrive_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\"\xcb\x03\n" +
|
||||
"&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\x1a)transport/internet/splithttp/config.proto\"\x8e\x04\n" +
|
||||
"\x06Config\x12#\n" +
|
||||
"\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" +
|
||||
"\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" +
|
||||
@@ -171,7 +180,8 @@ const file_transport_internet_xdrive_config_proto_rawDesc = "" +
|
||||
"\x0feager_window_ms\x18\n" +
|
||||
" \x01(\rR\reagerWindowMs\x12&\n" +
|
||||
"\x0fhole_timeout_ms\x18\v \x01(\rR\rholeTimeoutMs\x12\x1a\n" +
|
||||
"\btemplate\x18\f \x01(\tR\btemplateB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
|
||||
"\btemplate\x18\f \x01(\tR\btemplate\x12A\n" +
|
||||
"\x04xmux\x18\r \x01(\v2-.xray.transport.internet.splithttp.XmuxConfigR\x04xmuxB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once
|
||||
@@ -187,14 +197,16 @@ 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_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{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
1, // 0: xray.transport.internet.xdrive.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_xdrive_config_proto_init() }
|
||||
|
||||
@@ -3,6 +3,8 @@ syntax = "proto3";
|
||||
package xray.transport.internet.xdrive;
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/xdrive";
|
||||
|
||||
import "transport/internet/splithttp/config.proto";
|
||||
|
||||
message Config {
|
||||
string remote_folder = 1;
|
||||
string service = 2;
|
||||
@@ -16,4 +18,5 @@ message Config {
|
||||
uint32 eager_window_ms = 10;
|
||||
uint32 hole_timeout_ms = 11;
|
||||
string template = 12;
|
||||
xray.transport.internet.splithttp.XmuxConfig xmux = 13;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ const (
|
||||
driveMaxAttempts = 8
|
||||
driveMaxInflight = 32
|
||||
driveInlineLimit = 12000
|
||||
driveTimeout = 60 * time.Second
|
||||
// driveTimeout ends a request that makes no progress for that long
|
||||
driveTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,11 +39,21 @@ var (
|
||||
)
|
||||
|
||||
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
|
||||
clientID string
|
||||
clientSecret string
|
||||
refreshToken string
|
||||
client *http.Client
|
||||
tokenClient *http.Client
|
||||
tokenURL string
|
||||
filesURL string
|
||||
uploadURL string
|
||||
@@ -52,8 +63,6 @@ type driveStorage struct {
|
||||
token string
|
||||
tokenExpiry time.Time
|
||||
|
||||
inflight chan struct{}
|
||||
|
||||
idMu sync.Mutex
|
||||
ids map[string]string
|
||||
}
|
||||
@@ -71,21 +80,41 @@ func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config
|
||||
}
|
||||
}
|
||||
|
||||
h1Hosts := new(sync.Map)
|
||||
client := newServiceClient(context.Background(), h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
|
||||
return &driveStorage{
|
||||
folder: config.RemoteFolder,
|
||||
clientID: config.Secrets[0],
|
||||
clientSecret: config.Secrets[1],
|
||||
refreshToken: config.Secrets[2],
|
||||
client: newServiceClient(streamSettings, driveTimeout, driveMaxInflight),
|
||||
tokenURL: driveTokenURL,
|
||||
filesURL: driveFilesURL,
|
||||
uploadURL: driveUploadURL,
|
||||
backoff: driveInitialBackoff,
|
||||
inflight: make(chan struct{}, driveMaxInflight),
|
||||
ids: make(map[string]string),
|
||||
driveShared: &driveShared{
|
||||
conns: newConnPool(config.Xmux, func(connCtx context.Context) *http.Client {
|
||||
return newServiceClient(connCtx, h1Hosts, streamSettings, driveTimeout, driveMaxInflight)
|
||||
}),
|
||||
folder: config.RemoteFolder,
|
||||
clientID: config.Secrets[0],
|
||||
clientSecret: config.Secrets[1],
|
||||
refreshToken: config.Secrets[2],
|
||||
tokenClient: client,
|
||||
tokenURL: driveTokenURL,
|
||||
filesURL: driveFilesURL,
|
||||
uploadURL: driveUploadURL,
|
||||
backoff: driveInitialBackoff,
|
||||
ids: make(map[string]string),
|
||||
},
|
||||
client: client,
|
||||
inflight: make(chan struct{}, driveMaxInflight),
|
||||
}, 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 {
|
||||
return strings.ReplaceAll(name, "/", flatSeparator)
|
||||
}
|
||||
@@ -114,7 +143,7 @@ func (s *driveStorage) accessToken(ctx context.Context) (string, error) {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
resp, err := s.tokenClient.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to refresh the access token").Base(err)
|
||||
}
|
||||
@@ -153,7 +182,7 @@ func jitter(backoff time.Duration) time.Duration {
|
||||
if half <= 0 {
|
||||
return backoff
|
||||
}
|
||||
return half + time.Duration(dice.Roll(int(half)))
|
||||
return half + time.Duration(dice.RollInt63n(int64(half)))
|
||||
}
|
||||
|
||||
func rateLimited(payload []byte) bool {
|
||||
@@ -571,5 +600,8 @@ func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error)
|
||||
}
|
||||
|
||||
func (s *driveStorage) Close() error {
|
||||
if s.release != nil {
|
||||
s.release()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func liveDriveConfig(t *testing.T) *Config {
|
||||
|
||||
config := &Config{
|
||||
RemoteFolder: secrets.Folder,
|
||||
Service: "Google Drive",
|
||||
Service: "google-drive",
|
||||
Secrets: []string{secrets.ClientID, secrets.ClientSecret, secrets.RefreshToken},
|
||||
SegmentBytes: 256 * 1024,
|
||||
FlushIntervalMs: 100,
|
||||
|
||||
@@ -317,7 +317,7 @@ func driveSettings() *internet.MemoryStreamConfig {
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
RemoteFolder: "folder-id",
|
||||
Service: "Google Drive",
|
||||
Service: "google-drive",
|
||||
Secrets: []string{"client", "secret", "refresh"},
|
||||
FlushIntervalMs: 5,
|
||||
PollIntervalMs: 5,
|
||||
@@ -562,6 +562,22 @@ func TestDriveSharedClient(t *testing.T) {
|
||||
if first != second {
|
||||
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) {
|
||||
newFakeDrive(t)
|
||||
|
||||
settings := driveSettings()
|
||||
settings.ProtocolSettings.(*Config).Service = "Google Drive"
|
||||
if _, err := newStorage(settings); err != nil {
|
||||
t.Fatalf("newStorage: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveInlineListing(t *testing.T) {
|
||||
|
||||
@@ -35,7 +35,7 @@ func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
|
||||
switch config.Service {
|
||||
case "local":
|
||||
return newLocalStorage(config.RemoteFolder)
|
||||
case "Google Drive":
|
||||
case "google-drive", "Google Drive":
|
||||
return sharedStorage(streamSettings, config, func() (Storage, error) {
|
||||
return newDriveStorage(streamSettings, config)
|
||||
})
|
||||
@@ -48,6 +48,19 @@ 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 (
|
||||
sharedMu sync.Mutex
|
||||
shared = make(map[string]Storage)
|
||||
|
||||
@@ -33,6 +33,14 @@ type opTemplate struct {
|
||||
Headers map[string]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
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 {
|
||||
@@ -54,12 +62,20 @@ type storageTemplate struct {
|
||||
}
|
||||
|
||||
type templateStorage struct {
|
||||
tmpl *storageTemplate
|
||||
client *http.Client
|
||||
folder string
|
||||
secrets []string
|
||||
|
||||
*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
|
||||
tokenClient *http.Client
|
||||
folder string
|
||||
secrets []string
|
||||
|
||||
tokenMu sync.Mutex
|
||||
token string
|
||||
@@ -94,15 +110,35 @@ func newTemplateStorage(streamSettings *internet.MemoryStreamConfig, config *Con
|
||||
conc = maxTemplateConcurrency
|
||||
}
|
||||
|
||||
h1Hosts := new(sync.Map)
|
||||
client := newServiceClient(context.Background(), h1Hosts, streamSettings, driveTimeout, conc)
|
||||
return &templateStorage{
|
||||
tmpl: tmpl,
|
||||
client: newServiceClient(streamSettings, driveTimeout, conc),
|
||||
folder: config.RemoteFolder,
|
||||
secrets: config.Secrets,
|
||||
templateShared: &templateShared{
|
||||
conns: newConnPool(config.Xmux, func(connCtx context.Context) *http.Client {
|
||||
return newServiceClient(connCtx, h1Hosts, streamSettings, driveTimeout, conc)
|
||||
}),
|
||||
tmpl: tmpl,
|
||||
tokenClient: client,
|
||||
folder: config.RemoteFolder,
|
||||
secrets: config.Secrets,
|
||||
},
|
||||
client: client,
|
||||
inflight: make(chan struct{}, conc),
|
||||
}, 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
|
||||
|
||||
func (s *templateStorage) baseVars() map[string]string {
|
||||
@@ -216,7 +252,7 @@ func (s *templateStorage) accessToken(ctx context.Context) (string, error) {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
resp, err := s.tokenClient.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to fetch the token").Base(err)
|
||||
}
|
||||
@@ -262,6 +298,11 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
|
||||
backoff := driveInitialBackoff
|
||||
var lastErr error
|
||||
|
||||
url := op.URL
|
||||
if !op.presigned {
|
||||
url = subst(url, vars)
|
||||
}
|
||||
|
||||
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
select {
|
||||
@@ -275,10 +316,14 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
|
||||
}
|
||||
}
|
||||
|
||||
authHeaders, err := s.authHeaders(ctx, vars)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
var authHeaders map[string]string
|
||||
if !op.presigned {
|
||||
headers, err := s.authHeaders(ctx, vars)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
authHeaders = headers
|
||||
}
|
||||
|
||||
method := op.Method
|
||||
@@ -290,7 +335,7 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
|
||||
if body != nil {
|
||||
reader = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, subst(op.URL, vars), reader)
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, reader)
|
||||
if err != nil {
|
||||
return 0, nil, errors.New("failed to build request").Base(err)
|
||||
}
|
||||
@@ -325,7 +370,7 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" {
|
||||
if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" && !op.presigned {
|
||||
s.invalidateToken()
|
||||
lastErr = errors.New("the service rejected the token")
|
||||
continue
|
||||
@@ -339,6 +384,22 @@ func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[strin
|
||||
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 {
|
||||
vars := s.baseVars()
|
||||
vars["name"] = s.storedName(name)
|
||||
@@ -349,7 +410,7 @@ func (s *templateStorage) Put(ctx context.Context, name string, data []byte) err
|
||||
body = []byte(subst(s.tmpl.Put.Body, vars))
|
||||
}
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.Put, vars, body)
|
||||
status, payload, err := s.run(ctx, &s.tmpl.Put, vars, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -363,7 +424,7 @@ func (s *templateStorage) Get(ctx context.Context, name string) ([]byte, error)
|
||||
vars := s.baseVars()
|
||||
vars["name"] = s.storedName(name)
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.Get, vars, nil)
|
||||
status, payload, err := s.run(ctx, &s.tmpl.Get, vars, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -381,7 +442,7 @@ func (s *templateStorage) Delete(ctx context.Context, name string) error {
|
||||
vars := s.baseVars()
|
||||
vars["name"] = s.storedName(name)
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.Delete, vars, nil)
|
||||
status, payload, err := s.run(ctx, &s.tmpl.Delete, vars, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -396,7 +457,7 @@ func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, err
|
||||
flat := s.storedName(prefix)
|
||||
vars["prefix"] = flat
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.List, vars, nil)
|
||||
status, payload, err := s.run(ctx, &s.tmpl.List, vars, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -441,5 +502,8 @@ func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, err
|
||||
}
|
||||
|
||||
func (s *templateStorage) Close() error {
|
||||
if s.release != nil {
|
||||
s.release()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func templateSettings(store *fakeStore, auth map[string]interface{}, secrets []s
|
||||
"delete": map[string]interface{}{"method": "DELETE", "url": base + "/folder/{name}"},
|
||||
"list": map[string]interface{}{
|
||||
"method": "PROPFIND",
|
||||
"url": base + "/folder/",
|
||||
"url": base + "/folder/?prefix={prefix}",
|
||||
"namesRegex": `<d:href>/folder/([^<]+)</d:href>`,
|
||||
},
|
||||
"retry": map[string]interface{}{"status": []int{429, 500, 502, 503}},
|
||||
@@ -297,6 +297,12 @@ func TestTemplateConcurrency(t *testing.T) {
|
||||
if cap(storage.inflight) != 4 {
|
||||
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) {
|
||||
@@ -306,3 +312,113 @@ func TestTemplateConcurrencyDefault(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage = sessionStorage(storage)
|
||||
|
||||
session, err := newSessionID()
|
||||
if err != nil {
|
||||
@@ -228,8 +229,10 @@ func (l *Listener) claim(session string) bool {
|
||||
}
|
||||
|
||||
func (l *Listener) newSessionConn(session string) *Conn {
|
||||
return newConn(l.ctx, l.storage,
|
||||
storage := sessionStorage(l.storage)
|
||||
return newConn(l.ctx, storage,
|
||||
downlinkPrefix(session), uplinkPrefix(session), l.params, func() {
|
||||
storage.Close()
|
||||
l.mu.Lock()
|
||||
delete(l.active, session)
|
||||
l.mu.Unlock()
|
||||
|
||||
@@ -4,16 +4,21 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
gotls "crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"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/tls"
|
||||
)
|
||||
|
||||
const testPatience = 30 * time.Second
|
||||
@@ -581,3 +586,388 @@ func TestInlinePayload(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user