Compare commits

..
28 Commits
Author SHA1 Message Date
Meo597 bba0ae39d1 lua: revert stdin check 2026-10-15 18:32:34 +08:00
Meo597 03ee3f872f disable signature & privileged 2026-10-15 12:22:56 +08:00
Meo597 65449d4f03 lua: require explicit registry sizing for program states to reduce memory usage 2026-10-15 11:18:27 +08:00
Meo597 d5bfb0a5c7 lua: reduce binding setup and error redaction allocations 2026-10-15 10:54:43 +08:00
Meo597 afa28af4e0 lua: defer read-only slice element bindings until first access 2026-10-15 10:06:48 +08:00
Meo597 ecd8719d98 main: validate resolved config file extensions for run 2026-10-15 07:56:35 +08:00
Meo597 74456ca5e9 Revert "lua: prevent configuration files from being executed as scripts"
This reverts commit 796bbd763a.
2026-10-15 06:36:39 +08:00
Meo597 b408f7f0c7 lua: add recursive read-only bindings for borrowed slices 2026-10-15 06:36:12 +08:00
Meo597 ef4f4888df lua: allow os time functions in sandbox 2026-10-15 04:33:35 +08:00
Meo597 c2a9b074f4 lua: redact memory addresses in sandbox errors 2026-10-15 03:57:24 +08:00
Meo597 9726bbc9d7 lua: prevent memory address leaks through sandbox string.format and refine comments 2026-10-15 03:07:54 +08:00
Meo597 6cd0c8a414 lua: preserve native Stringer and error output in sandbox tostring() 2026-10-15 02:28:36 +08:00
Meo597 796bbd763a lua: prevent configuration files from being executed as scripts 2026-10-15 01:34:40 +08:00
Meo597 e04191e65b lua: validate script extension after resolving symlinks 2026-10-14 23:12:30 +08:00
Meo597 bd28a87f0b geodata: prevent downloaded assets from compiling as Lua 2026-10-14 22:29:56 +08:00
Meo597 a04770587e geodata: validate entry and code tags before matching 2026-10-14 22:04:54 +08:00
Meo597 eb9c6030ab geodata: reject entry lengths exceeding asset size 2026-10-14 22:03:08 +08:00
Meo597 e4124d3dbb geodata: stricter varint overflow checks 2026-10-14 21:36:24 +08:00
Meo597 4c0f3a94c5 disable inline lua 2026-10-14 10:18:40 +08:00
Meo597 19842ed0c2 luar: whitelist 2026-10-14 09:49:58 +08:00
Meo597 34efc32ff0 sandbox: disable more func 2026-10-14 09:16:32 +08:00
Meo597 4828ed8270 cmd: add Ed25519 key generation and Lua script signing 2026-10-14 08:14:05 +08:00
Meo597 e84f3065dc fix format 2026-10-14 07:07:03 +08:00
Meo597 fa9dc21a55 Replace privileged PSK with Ed25519 signature verification 2026-10-14 06:57:32 +08:00
Meo597 590a5236bb Reduce memory usage 2026-10-13 12:31:04 +08:00
Meo597 1aae252fc3 Lua: Restrict PSK privileges to local Lua files 2026-10-13 11:38:06 +08:00
Meo597 20c4c8f956 Lua: Hide package from sandboxed scripts 2026-10-13 11:24:47 +08:00
Meo597 4c571f5e06 Lua: Add sandbox mechanism 2026-10-13 09:52:31 +08:00
77 changed files with 1937 additions and 6004 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
stats.Min = 0
return stats
}
stats.Average = sum / time.Duration(cnt)
stats.Average = time.Duration(int(sum) / cnt)
var std float64
if cnt < 2 {
// no enough data for standard deviation, we assume it's half of the average rtt
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 10
Version_z byte = 17
Version_z byte = 10
)
var (
+13 -10
View File
@@ -4,6 +4,7 @@ go 1.27
require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
@@ -19,20 +20,21 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
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 v0.57.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
golang.org/x/net v0.60.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11
lukechampine.com/blake3 v1.4.1
@@ -40,12 +42,13 @@ require (
)
require (
github.com/andybalholm/brotli v1.2.3 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
@@ -55,9 +58,9 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.43.0 // indirect
golang.org/x/tools v0.51.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
)
+20 -20
View File
@@ -1,10 +1,12 @@
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
@@ -31,8 +33,8 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
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/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
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/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
@@ -79,10 +81,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
@@ -95,8 +95,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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 v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
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/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
@@ -107,12 +107,12 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,22 +121,22 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+10 -140
View File
@@ -9,15 +9,12 @@ import (
"fmt"
"net/netip"
"net/url"
"os"
"regexp"
"strings"
googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors"
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/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -31,7 +28,6 @@ import (
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
@@ -308,26 +304,11 @@ type NoiseItem struct {
}
type NoiseMask struct {
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
Script string `json:"script"`
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
}
func (c *NoiseMask) Build() (proto.Message, error) {
if c.Script != "" {
if len(c.Noise) > 0 {
return nil, errors.New(`"script" and "noise" can't be used together`)
}
if _, err := xlua.Compile(c.Script); err != nil {
return nil, errors.New("failed to compile the noise script").Base(err)
}
return &noise.Config{
ResetMin: int64(c.Reset.From),
ResetMax: int64(c.Reset.To),
Script: c.Script,
}, nil
}
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
for _, item := range c.Noise {
if len(item.Packet) > 0 && item.Rand.To > 0 {
@@ -818,10 +799,7 @@ type XDNSDomain struct {
}
type XDNSResolver struct {
Addrs []string `json:"addrs"`
Worker int32 `json:"worker"`
TlsConfig *TLSConfig `json:"tlsConfig"`
QuicConfig *QuicParamsConfig `json:"quicConfig"`
Addrs []string `json:"addrs"`
}
type XDNS struct {
@@ -856,26 +834,6 @@ func (c *XDNS) Build() (proto.Message, error) {
}
}
for i := range c.Resolvers {
worker := c.Resolvers[i].Worker
if worker == 0 {
worker = 32
}
var tlsConfig *tls.Config
var quicConfig *internet.QuicParams
if c.Resolvers[i].TlsConfig != nil {
proto, err := c.Resolvers[i].TlsConfig.Build()
if err != nil {
return nil, err
}
tlsConfig = proto.(*tls.Config)
}
if c.Resolvers[i].QuicConfig != nil {
proto, err := c.Resolvers[i].QuicConfig.Build()
if err != nil {
return nil, err
}
quicConfig = proto.(*internet.QuicParams)
}
for j := range c.Resolvers[i].Addrs {
var u *url.URL
var e error
@@ -887,21 +845,18 @@ func (c *XDNS) Build() (proto.Message, error) {
if e != nil {
return nil, e
}
var addr string
switch u.Scheme {
case "tcp", "udp":
host := u.Hostname()
port := u.Port()
if port == "" {
port = "53"
}
addr = net.JoinHostPort(host, port)
case "https":
addr = u.String()
default:
return nil, errors.New("invalid protocol")
}
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: addr, Worker: worker, TlsConfig: tlsConfig, QuicConfig: quicConfig})
var host, port string
host = u.Hostname()
port = u.Port()
if port == "" {
port = "53"
}
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: net.JoinHostPort(host, port)})
}
}
if c.ExtraPoll < 0 || c.ExtraPoll > 3 {
@@ -1194,91 +1149,6 @@ type QuicParamsConfig struct {
DisableStatelessReset bool `json:"disableStatelessReset"`
}
func (config *QuicParamsConfig) Build() (proto.Message, error) {
profile := strings.ToLower(config.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := config.BrutalUp.Bps()
if err != nil {
return nil, err
}
down, err := config.BrutalDown.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
config.Congestion = strings.ToLower(config.Congestion)
switch config.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", config.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if config.InitStreamReceiveWindow > 0 && config.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if config.MaxStreamReceiveWindow > 0 && config.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if config.InitConnectionReceiveWindow > 0 && config.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if config.MaxConnectionReceiveWindow > 0 && config.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if config.MaxIdleTimeout != 0 && (config.MaxIdleTimeout < 4 || config.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if config.KeepAlivePeriod != 0 && (config.KeepAlivePeriod < 2 || config.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if config.MaxIncomingStreams != 0 && config.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if config.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
return &internet.QuicParams{
Congestion: config.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: config.BrutalDisableLossCompensation,
InitStreamReceiveWindow: config.InitStreamReceiveWindow,
MaxStreamReceiveWindow: config.MaxStreamReceiveWindow,
InitConnReceiveWindow: config.InitConnectionReceiveWindow,
MaxConnReceiveWindow: config.MaxConnectionReceiveWindow,
MaxIdleTimeout: config.MaxIdleTimeout,
KeepAlivePeriod: config.KeepAlivePeriod,
DisablePathMtuDiscovery: config.DisablePathMTUDiscovery,
DisableChromeParrot: config.DisableChromeParrot,
DisableGSO: config.DisableGSO,
MaxIncomingStreams: config.MaxIncomingStreams,
DisableStatelessReset: config.DisableStatelessReset,
}, nil
}
type FinalMask struct {
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
@@ -2,9 +2,6 @@ package conf
import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
@@ -137,68 +134,3 @@ func TestNoiseExpFromJSON(t *testing.T) {
t.Errorf("item 3 = %+v", items[3])
}
}
const noiseScript = `function HandleNoise(plan, address) plan:Add("n") end
`
func writeNoiseScript(t *testing.T, script string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "noise.lua")
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
t.Fatal(err)
}
return path
}
func buildNoiseMask(t *testing.T, mask *NoiseMask) *noise.Config {
t.Helper()
msg, err := mask.Build()
if err != nil {
t.Fatal(err)
}
return msg.(*noise.Config)
}
func TestNoiseScriptFromFile(t *testing.T) {
path := writeNoiseScript(t, noiseScript)
cfg := buildNoiseMask(t, &NoiseMask{Script: path, Reset: Int32Range{From: 30, To: 60}})
if cfg.Script != path {
t.Errorf("got script %q, want the path", cfg.Script)
}
if len(cfg.Items) != 0 {
t.Errorf("got %d items, want none", len(cfg.Items))
}
if cfg.ResetMin != 30 || cfg.ResetMax != 60 {
t.Errorf("got reset %d-%d, want 30-60", cfg.ResetMin, cfg.ResetMax)
}
}
func TestNoiseScriptFromJSON(t *testing.T) {
path := writeNoiseScript(t, noiseScript)
input := `{"script": ` + strconv.Quote(path) + `, "reset": "30-60"}`
var mask NoiseMask
if err := json.Unmarshal([]byte(input), &mask); err != nil {
t.Fatal(err)
}
if cfg := buildNoiseMask(t, &mask); cfg.Script != path {
t.Errorf("got script %q, want the path", cfg.Script)
}
}
func TestNoiseScriptRejected(t *testing.T) {
path := writeNoiseScript(t, noiseScript)
invalid := writeNoiseScript(t, "function HandleNoise(")
for name, mask := range map[string]*NoiseMask{
"script with noise": {Script: path, Noise: []NoiseItem{{Rand: Int32Range{From: 10, To: 20}}}},
"missing file": {Script: filepath.Join(t.TempDir(), "absent.lua")},
"not lua": {Script: invalid},
} {
t.Run(name, func(t *testing.T) {
if _, err := mask.Build(); err == nil {
t.Error("expected an error")
}
})
}
}
+85 -18
View File
@@ -1,15 +1,13 @@
package conf
import (
"context"
"slices"
"os"
"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"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
)
type TransportProtocol string
@@ -104,14 +102,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type
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.")
@@ -238,25 +228,102 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
if c.FinalMask != nil {
for _, mask := range c.FinalMask.Tcp {
proto, err := mask.Build(true)
u, err := mask.Build(true)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(proto))
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u))
}
for _, mask := range c.FinalMask.Udp {
proto, err := mask.Build(false)
u, err := mask.Build(false)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(proto))
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u))
}
if c.FinalMask.QuicParams != nil {
proto, err := c.FinalMask.QuicParams.Build()
profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := c.FinalMask.QuicParams.BrutalUp.Bps()
if err != nil {
return nil, err
}
config.QuicParams = proto.(*internet.QuicParams)
down, err := c.FinalMask.QuicParams.BrutalDown.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion)
switch c.FinalMask.QuicParams.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if c.FinalMask.QuicParams.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
}
}
}
+1 -5
View File
@@ -1004,10 +1004,6 @@ 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")
}
@@ -1016,7 +1012,7 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
return nil, errors.New(`service "template" needs a "template" object`)
}
default:
return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`)
return nil, errors.New("unsupported service")
}
config := &xdrive.Config{
RemoteFolder: c.RemoteFolder,
+2 -151
View File
@@ -5,11 +5,9 @@ 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/xdrive"
"google.golang.org/protobuf/proto"
)
@@ -323,25 +321,8 @@ 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 || !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)
}
if _, err := config.Build(); err == nil {
t.Fatal("Build accepted an unsupported service")
}
}
@@ -374,56 +355,6 @@ 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)
}
}
}
func TestXDriveTemplateNeedsTemplate(t *testing.T) {
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
@@ -433,83 +364,3 @@ 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)
}
}
}
-58
View File
@@ -17,11 +17,7 @@ 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 (
@@ -213,9 +209,6 @@ 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,
@@ -269,54 +262,6 @@ 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 {
@@ -395,9 +340,6 @@ 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
}
+3 -3
View File
@@ -1,11 +1,11 @@
package all
import (
"crypto/mldsa"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/xray-core/main/commands/base"
)
@@ -39,8 +39,8 @@ func executeMLDSA65(cmd *base.Command, args []string) {
} else {
rand.Read(seed[:])
}
key, _ := mldsa.NewPrivateKey(mldsa.MLDSA65(), seed[:])
pub, _ := mldsa65.NewKeyFromSeed(&seed)
fmt.Printf("Seed: %v\nVerify: %v\n",
base64.RawURLEncoding.EncodeToString(seed[:]),
base64.RawURLEncoding.EncodeToString(key.PublicKey().Bytes()))
base64.RawURLEncoding.EncodeToString(pub.Bytes()))
}
+1 -1
View File
@@ -327,7 +327,7 @@ func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr,
dns = remoteDNS
}
dev, tnet, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, nil)
dev, tnet, _, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, true)
if err != nil {
return nil, err
}
+9 -13
View File
@@ -22,7 +22,6 @@ import (
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
xtun "github.com/xtls/xray-core/proxy/tun"
"github.com/xtls/xray-core/proxy/wireguard"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
@@ -167,11 +166,17 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if mtu == 0 {
mtu = masque.MinPacketSize
}
dev, _, gstack, err := wireguard.CreateNetTUN(local, nil, mtu, false)
if err != nil {
return nil, err
}
s := &Server{
validator: users,
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
ctx: core.ToBackgroundDetachedContext(ctx),
mtu: mtu,
dev: dev,
pools: pools,
local: local,
tunnels: make(map[netip.Addr]*serverTunnel),
@@ -182,11 +187,7 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if content := session.ContentFromContext(ctx); content != nil {
s.sniffing = content.SniffingRequest
}
dev, _, err := wireguard.CreateNetTUN(local, nil, mtu, s)
if err != nil {
return nil, err
}
s.dev = dev
wireguard.CreateForwarder(gstack, s.handleConnection)
return s, nil
}
@@ -499,8 +500,7 @@ func packetDestination(packet []byte) (netip.Addr, bool) {
return netip.Addr{}, false
}
// HandleConnection implements tun.ConnectionHandler
func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
defer conn.Close()
source := net.DestinationFromAddr(conn.RemoteAddr())
addr, _ := netip.AddrFromSlice(source.Address.IP())
@@ -534,13 +534,9 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
})
errors.LogInfo(ctx, "processing from ", source, " to ", dest)
writer := buf.NewWriter(conn)
if dest.Network == net.Network_TCP {
writer = &xtun.TCPWriter{Writer: writer}
}
link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: writer,
Writer: buf.NewWriter(conn),
}
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err))
-57
View File
@@ -7,17 +7,13 @@ import (
"net/netip"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"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/features/routing"
"github.com/xtls/xray-core/transport"
"golang.zx2c4.com/wireguard/tun"
)
@@ -330,56 +326,3 @@ func TestPacketDestination(t *testing.T) {
require.False(t, ok)
}
}
// stackConn is a TCP connection as the stack hands it over: it is its own buf.Writer.
type stackConn struct {
net.Conn
closed atomic.Int32
}
func (c *stackConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c *stackConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
buf.ReleaseMulti(mb)
return nil
}
func (c *stackConn) Close() error {
c.closed.Add(1)
return nil
}
func (c *stackConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IP{10, 13, 0, 2}, Port: 1234}
}
// closingDispatcher does what an outbound does when the other side has no more to send.
type closingDispatcher struct {
routing.Dispatcher
conn *stackConn
closed int32
}
func (d *closingDispatcher) DispatchLink(_ context.Context, _ net.Destination, link *transport.Link) error {
common.Close(link.Writer)
d.closed = d.conn.closed.Load()
return nil
}
func TestServerKeepsConnectionOpenWhenWriterCloses(t *testing.T) {
conn := &stackConn{}
dispatcher := &closingDispatcher{conn: conn}
s := &Server{
ctx: context.Background(),
dispatcher: dispatcher,
tunnels: map[netip.Addr]*serverTunnel{netip.MustParseAddr("10.13.0.2"): newServerTunnel(nil, &protocol.MemoryUser{})},
}
s.HandleConnection(conn, net.TCPDestination(net.ParseAddress("192.0.2.1"), 443))
if dispatcher.closed != 0 {
t.Fatal("the connection was closed with its writer, while the client may still be sending")
}
if got := conn.closed.Load(); got != 1 {
t.Fatalf("connection closed %d times, want 1", got)
}
}
+6 -9
View File
@@ -180,14 +180,14 @@ type VisionReader struct {
isUplink bool
conn net.Conn
input *bytes.Reader
rawInput **bytes.Buffer
rawInput *bytes.Buffer
ob *session.Outbound
// internal
directReadCounter stats.Counter
}
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput **bytes.Buffer, ob *session.Outbound) *VisionReader {
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, ob *session.Outbound) *VisionReader {
return &VisionReader{
Reader: reader,
trafficState: trafficState,
@@ -261,15 +261,12 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, inputBuffer)
}
if rawInputBuffer, err := buf.ReadFrom(w.rawInput); err == nil && !rawInputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
}
*w.input = bytes.Reader{} // release memory
w.input = nil
if raw := *w.rawInput; raw != nil {
if rawInputBuffer, err := buf.ReadFrom(raw); err == nil && !rawInputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
}
// *raw = bytes.Buffer{} // may not be suitable for Go 1.27.3+
}
*w.rawInput = bytes.Buffer{} // release memory
w.rawInput = nil
if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil {
+5 -11
View File
@@ -169,17 +169,11 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
}
sessionItem.Unlock()
}
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
})
if err != nil {
continue
}
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
})
if err != nil {
continue
}
payloadBuf := buf.New()
+5 -11
View File
@@ -336,17 +336,11 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
}
sessionItem.Unlock()
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
})
if err != nil {
continue
}
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
})
if err != nil {
continue
}
pBuf := buf.New()
+4 -10
View File
@@ -253,16 +253,10 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
}
sessionItem.Unlock()
}
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil {
b.Release()
continue
}
link, err := sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil {
b.Release()
continue
}
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
+4 -4
View File
@@ -169,8 +169,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
}
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
return errors.New("connection ends").Base(err)
}
@@ -212,8 +212,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return nil
}
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
return errors.New("connection ends").Base(err)
}
+1 -1
View File
@@ -153,7 +153,7 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
return DecodedUDPPacket{}, ErrBadHeaderType
}
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
if diff > 30 {
return DecodedUDPPacket{}, ErrBadTimestamp
}
+2 -2
View File
@@ -49,12 +49,12 @@ func (f *SlidingWindow) Add(counter uint64) {
if counter > f.last {
lastBlockIndex := f.last >> swBlockBitLog
diff := blockIndex - lastBlockIndex
diff := int(blockIndex - lastBlockIndex)
if diff > swRingBlocks {
diff = swRingBlocks
}
for i := 0; i < int(diff); i++ {
for i := 0; i < diff; i++ {
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
f.ring[lastBlockIndex] = 0
}
+21 -8
View File
@@ -18,8 +18,12 @@ import (
)
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
s.currentConn.Store(conn)
s.timer.Update()
if s.currentConn.Load() == nil {
s.currentConn.Store(conn)
}
if s.timer != nil {
s.timer.Update()
}
}
func (s *ServerUDPSession) WriteToClient(b []byte) error {
@@ -53,16 +57,19 @@ func (s *ServerUDPSession) EnsureLink(
policyManager policy.Manager,
responseEncoder func(dest net.Destination, payload []byte) ([]byte, error),
) (*transport.Link, error) {
s.UpdateConn(conn)
if link := s.link.Load(); link != nil {
return link, nil
}
s.Lock()
defer s.Unlock()
if link := s.link.Load(); link != nil {
s.UpdateConn(conn)
return link, nil
}
s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx)
inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil {
@@ -87,6 +94,7 @@ func (s *ServerUDPSession) EnsureLink(
return nil, err
}
s.link.Store(link)
sessionPolicy := policyManager.ForLevel(level)
s.timer = signal.CancelAfterInactivity(sessCtx, func() {
if s.manager != nil {
@@ -95,7 +103,6 @@ func (s *ServerUDPSession) EnsureLink(
s.Close()
cancel()
}, sessionPolicy.Timeouts.ConnectionIdle)
s.link.Store(link)
go handleUDPResponse(s, link, dest, responseEncoder)
return link, nil
@@ -111,13 +118,19 @@ func ResetTCPConn(conn net.Conn) {
}
func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) {
defer s.timer.SetTimeout(0)
defer func() {
if s.timer != nil {
s.timer.SetTimeout(0)
}
}()
for {
resMb, err := link.Reader.ReadMultiBuffer()
if err != nil {
return
}
s.timer.Update()
if s.timer != nil {
s.timer.Update()
}
for i, rb := range resMb {
b := rb.Bytes()
if encode != nil {
+4 -12
View File
@@ -11,7 +11,6 @@ import (
"sync"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/antireplay"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -254,7 +253,7 @@ func ReadClientRequestHeaderWithFixed(reader *StreamReader, fixedChunk []byte) (
}
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
if diff > 30 {
return nil, ErrBadTimestamp
}
@@ -431,7 +430,7 @@ func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt [
}
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
diff := math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
if diff > 30 {
return nil, ErrBadTimestamp
}
@@ -630,7 +629,6 @@ func InitServerStream(conn net.Conn, method *CipherMethod, psk, saltSlice []byte
func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.Reader, writer buf.Writer, link *transport.Link) error {
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
defer timer.SetTimeout(0)
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
requestDone := func() error {
@@ -646,12 +644,6 @@ func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
}
requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil {
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
return errors.New("connection ends").Base(err)
}
return nil
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
}
+12 -18
View File
@@ -56,21 +56,20 @@ func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
return nil
}
// TCPWriter is the writing side of a TCP connection of the stack for the dispatcher. It has no Close: the outbound
// closes its writer when the other side has no more to send, and the system may still be sending
type TCPWriter struct {
Writer buf.Writer
// Counter counts what is written, if there is one
Counter stats.Counter
// tunTCPWriter is the writing side of a TCP connection for the dispatcher. It has no Close: the outbound closes
// its writer when the other side has no more to send, and the system may still be sending
type tunTCPWriter struct {
writer buf.Writer
counter stats.Counter
}
func (w *TCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
func (w *tunTCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
size := mb.Len()
if err := w.Writer.WriteMultiBuffer(mb); err != nil {
if err := w.writer.WriteMultiBuffer(mb); err != nil {
return err
}
if w.Counter != nil {
w.Counter.Add(int64(size))
if w.counter != nil {
w.counter.Add(int64(size))
}
return nil
}
@@ -144,8 +143,7 @@ func (t *Handler) Start() error {
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
return errors.New("iface not found")
}
var controlErr, sockErr error
controlErr = c.Control(func(fd uintptr) {
return c.Control(func(fd uintptr) {
addrPort, _ := netip.ParseAddrPort(address)
// skip loopback
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
@@ -153,13 +151,9 @@ func (t *Handler) Start() error {
}
err := setinterface(network, address, fd, iface)
if err != nil {
sockErr = errors.New("[tun] falied to set interface").Base(err)
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
}
})
if sockErr != nil {
return sockErr
}
return controlErr
})
}
@@ -249,7 +243,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn), Counter: t.uplinkCounter}
writer := buf.NewWriter(conn)
if destination.Network != net.Network_UDP {
writer = &TCPWriter{Writer: writer, Counter: t.downlinkCounter}
writer = &tunTCPWriter{writer: writer, counter: t.downlinkCounter}
} else if t.downlinkCounter != nil {
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
}
+106
View File
@@ -0,0 +1,106 @@
package icmp
import (
"github.com/xtls/xray-core/common/errors"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func ProtocolLabel(netProto tcpip.NetworkProtocolNumber) string {
switch netProto {
case header.IPv4ProtocolNumber:
return "ipv4"
case header.IPv6ProtocolNumber:
return "ipv6"
default:
return "unknown"
}
}
func ParseEchoRequest(netProto tcpip.NetworkProtocolNumber, message []byte) (uint16, uint16, bool) {
switch netProto {
case header.IPv4ProtocolNumber:
if len(message) < header.ICMPv4MinimumSize {
return 0, 0, false
}
icmpHdr := header.ICMPv4(message)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
return 0, 0, false
}
return icmpHdr.Ident(), icmpHdr.Sequence(), true
case header.IPv6ProtocolNumber:
if len(message) < header.ICMPv6MinimumSize {
return 0, 0, false
}
icmpHdr := header.ICMPv6(message)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
return 0, 0, false
}
return icmpHdr.Ident(), icmpHdr.Sequence(), true
default:
return 0, 0, false
}
}
func RewriteChecksum(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
switch netProto {
case header.IPv4ProtocolNumber:
if len(message) < header.ICMPv4MinimumSize {
return errors.New("invalid icmpv4 packet")
}
icmpHdr := header.ICMPv4(message)
icmpHdr.SetChecksum(0)
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0)))
return nil
case header.IPv6ProtocolNumber:
if len(message) < header.ICMPv6MinimumSize {
return errors.New("invalid icmpv6 packet")
}
icmpHdr := header.ICMPv6(message)
icmpHdr.SetChecksum(0)
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr[:header.ICMPv6MinimumSize],
Src: srcIP,
Dst: dstIP,
PayloadCsum: checksum.Checksum(icmpHdr.Payload(), 0),
PayloadLen: len(icmpHdr.Payload()),
}))
return nil
default:
return errors.New("unsupported icmp network protocol")
}
}
func BuildLocalEchoReply(netProto tcpip.NetworkProtocolNumber, request []byte, srcIP, dstIP tcpip.Address) ([]byte, error) {
reply := append([]byte(nil), request...)
switch netProto {
case header.IPv4ProtocolNumber:
if len(reply) < header.ICMPv4MinimumSize {
return nil, errors.New("invalid icmpv4 echo packet")
}
icmpHdr := header.ICMPv4(reply)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
return nil, errors.New("not an icmpv4 echo request")
}
reply[0] = byte(header.ICMPv4EchoReply)
case header.IPv6ProtocolNumber:
if len(reply) < header.ICMPv6MinimumSize {
return nil, errors.New("invalid icmpv6 echo packet")
}
icmpHdr := header.ICMPv6(reply)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
return nil, errors.New("not an icmpv6 echo request")
}
reply[0] = byte(header.ICMPv6EchoReply)
default:
return nil, errors.New("unsupported icmp network protocol")
}
if err := RewriteChecksum(netProto, reply, srcIP, dstIP); err != nil {
return nil, err
}
return reply, nil
}
+174
View File
@@ -0,0 +1,174 @@
package icmp
import (
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func TestParseEchoRequest(t *testing.T) {
t.Run("ipv4 echo", func(t *testing.T) {
var zero tcpip.Address
packet := []byte{
byte(header.ICMPv4Echo), 0,
0, 0,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb,
}
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
t.Fatal(err)
}
ident, sequence, ok := ParseEchoRequest(header.IPv4ProtocolNumber, packet)
if !ok {
t.Fatal("expected ipv4 echo request to parse")
}
if ident != 0x1234 || sequence != 0x5678 {
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
}
})
t.Run("ipv6 echo", func(t *testing.T) {
packet := []byte{
byte(header.ICMPv6EchoRequest), 0,
0, 0,
0xab, 0xcd,
0xef, 0x01,
0xaa, 0xbb,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
t.Fatal(err)
}
ident, sequence, ok := ParseEchoRequest(header.IPv6ProtocolNumber, packet)
if !ok {
t.Fatal("expected ipv6 echo request to parse")
}
if ident != 0xabcd || sequence != 0xef01 {
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
}
})
}
func TestRewriteChecksum(t *testing.T) {
t.Run("ipv4", func(t *testing.T) {
var zero tcpip.Address
packet := []byte{
byte(header.ICMPv4Echo), 0,
0xff, 0xff,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
t.Fatal(err)
}
icmpHdr := header.ICMPv4(packet)
if got, want := icmpHdr.Checksum(), header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksumPayloadV4(icmpHdr.Payload())); got != want {
t.Fatalf("unexpected ipv4 checksum: got %x want %x", got, want)
}
})
t.Run("ipv6", func(t *testing.T) {
packet := []byte{
byte(header.ICMPv6EchoReply), 0,
0xff, 0xff,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
t.Fatal(err)
}
icmpHdr := header.ICMPv6(packet)
want := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr[:header.ICMPv6MinimumSize],
Src: src,
Dst: dst,
PayloadLen: len(icmpHdr.Payload()),
PayloadCsum: checksumPayloadV6(icmpHdr.Payload()),
})
if got := icmpHdr.Checksum(); got != want {
t.Fatalf("unexpected ipv6 checksum: got %x want %x", got, want)
}
})
}
func TestBuildLocalEchoReply(t *testing.T) {
t.Run("ipv4", func(t *testing.T) {
request := []byte{
byte(header.ICMPv4Echo), 0,
0, 0,
0x12, 0x34,
0x56, 0x78,
0xaa, 0xbb, 0xcc,
}
src := tcpip.Address{}
dst := tcpip.Address{}
if err := RewriteChecksum(header.IPv4ProtocolNumber, request, src, dst); err != nil {
t.Fatal(err)
}
reply, err := BuildLocalEchoReply(header.IPv4ProtocolNumber, request, dst, src)
if err != nil {
t.Fatal(err)
}
if request[0] != byte(header.ICMPv4Echo) {
t.Fatal("request mutated")
}
icmpHdr := header.ICMPv4(reply)
if icmpHdr.Type() != header.ICMPv4EchoReply || icmpHdr.Code() != header.ICMPv4UnusedCode {
t.Fatalf("unexpected ipv4 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
}
if icmpHdr.Ident() != 0x1234 || icmpHdr.Sequence() != 0x5678 {
t.Fatalf("unexpected ipv4 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
}
})
t.Run("ipv6", func(t *testing.T) {
request := []byte{
byte(header.ICMPv6EchoRequest), 0,
0, 0,
0xab, 0xcd,
0xef, 0x01,
0xaa, 0xbb, 0xcc,
}
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
if err := RewriteChecksum(header.IPv6ProtocolNumber, request, src, dst); err != nil {
t.Fatal(err)
}
reply, err := BuildLocalEchoReply(header.IPv6ProtocolNumber, request, dst, src)
if err != nil {
t.Fatal(err)
}
if request[0] != byte(header.ICMPv6EchoRequest) {
t.Fatal("request mutated")
}
icmpHdr := header.ICMPv6(reply)
if icmpHdr.Type() != header.ICMPv6EchoReply || icmpHdr.Code() != header.ICMPv6UnusedCode {
t.Fatalf("unexpected ipv6 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
}
if icmpHdr.Ident() != 0xabcd || icmpHdr.Sequence() != 0xef01 {
t.Fatalf("unexpected ipv6 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
}
})
}
func checksumPayloadV4(payload []byte) uint16 {
return checksum.Checksum(payload, 0)
}
func checksumPayloadV6(payload []byte) uint16 {
return checksum.Checksum(payload, 0)
}
+2 -231
View File
@@ -2,14 +2,8 @@ package tun
import (
"encoding/binary"
"math"
"math/bits"
"net/netip"
"slices"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
)
const (
@@ -22,19 +16,6 @@ const (
protoTCP = 6
protoUDP = 17
protoICMPv6 = 58
// the smallest that a link is believed to take whatever is said about it: what every IPv4 host takes,
// and the smallest link that IPv6 runs on
ipv4SmallestMTU = 576
ipv6SmallestMTU = 1280
// the header that an IPv6 packet has in front of each of its fragments, and its number
fragmentHeaderSize = 8
protoFragment = 44
// fragmentsMost is how many packets can be waiting for the rest of their fragments,
// fragmentsTime how long they are waited for at most
fragmentsMost = 64
fragmentsTime = 30 * time.Second
)
// packet is an IP packet with its header read
@@ -43,15 +24,9 @@ type packet struct {
src, dst netip.Addr
// payload is what follows the IP header
payload []byte
// 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
more bool
offset int
id uint32
}
// parsePacket reads the IP header of b
// parsePacket reads the IP header of b. Fragments are not accepted: what the system sends to the tun fits its MTU
func parsePacket(b []byte) (p packet, ok bool) {
if len(b) < ipv4HeaderSize {
return p, false
@@ -60,12 +35,9 @@ 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) || binary.BigEndian.Uint16(b[6:])&0x3fff != 0 {
return p, false
}
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
p.fragment, p.more, p.offset, p.id = true, f&0x2000 != 0, int(f&0x1fff)*8, uint32(binary.BigEndian.Uint16(b[4:]))
}
p.proto = b[9]
p.src = netip.AddrFrom4([4]byte(b[12:16]))
p.dst = netip.AddrFrom4([4]byte(b[16:20]))
@@ -93,55 +65,12 @@ func parsePacket(b []byte) (p packet, ok bool) {
}
p.proto, p.payload = p.payload[0], p.payload[size:]
}
if p.proto == protoFragment {
f := p.payload
if len(f) < fragmentHeaderSize {
return p, false
}
p.fragment, p.more, p.offset, p.id = true, f[3]&1 != 0, int(binary.BigEndian.Uint16(f[2:])&^7), binary.BigEndian.Uint32(f[4:])
p.proto, p.payload = f[0], f[fragmentHeaderSize:]
}
default:
return p, false
}
return p, true
}
// verified reports whether the checksum of what the packet carries is right
func (p packet) verified() bool {
data := p.payload
switch p.proto {
case protoICMPv4:
return checksum(sum(data, 0)) == 0
case protoUDP:
if len(data) < udpHeaderSize {
return false
}
// the packet may be longer than what it carries
size := int(binary.BigEndian.Uint16(data[4:]))
if size < udpHeaderSize || size > len(data) {
return false
}
if p.src.Is4() && data[6]|data[7] == 0 {
// zero stands for no checksum
return true
}
data = data[:size]
case protoTCP, protoICMPv6:
default:
return true
}
pseudo := uint64(p.proto) + uint64(len(data))
if p.src.Is4() {
src, dst := p.src.As4(), p.dst.As4()
pseudo = sum(dst[:], sum(src[:], pseudo))
} else {
src, dst := p.src.As16(), p.dst.As16()
pseudo = sum(dst[:], sum(src[:], pseudo))
}
return checksum(sum(data, pseudo)) == 0
}
// putIPHeader writes the header of a packet from src to dst into b, without its length, and returns the size of the header
func putIPHeader(b []byte, proto byte, src, dst netip.Addr) int {
if src.Is4() {
@@ -230,161 +159,3 @@ func checksum(s uint64) uint16 {
s = s>>16 + s&0xffff
return ^uint16(s)
}
// writeFragments writes a packet that is larger than mtu to tun in fragments that are not. header is its IP header
// with what follows that up to payload, which is no more than the header of a UDP packet. id is the number
// of the packet if it is an IPv6 one
func writeFragments(tun Tun, mtu int, id uint32, header, payload []byte) error {
var b [ipv6HeaderSize + fragmentHeaderSize + udpHeaderSize]byte
size := ipv4HeaderSize
if header[0]>>4 == 6 {
size = ipv6HeaderSize
}
n := copy(b[:], header[:size])
if size == ipv6HeaderSize {
// the number goes into a header of its own, behind the IP header. That of IPv4 has it
b[6], b[n] = protoFragment, header[6]
binary.BigEndian.PutUint32(b[n+4:], id)
n += fragmentHeaderSize
}
// first goes in front of payload, in the first fragment
first := header[size:]
if n+len(first) > len(b) {
return errors.New("no room for a header of ", len(first), " bytes in a fragment")
}
// every fragment but the last is a number of 8 bytes
room, total := (mtu-n)&^7, len(first)+len(payload)
if room < 8 {
return errors.New("no room for a fragment in an MTU of ", mtu)
}
for offset := 0; offset < total; {
h, part := b[:n], min(room, total-offset)
last := offset+part == total
if size == ipv6HeaderSize {
f := uint16(offset)
if !last {
f |= 1
}
binary.BigEndian.PutUint16(b[size+2:], f)
} else {
f := uint16(offset / 8)
if !last {
f |= 0x2000
}
binary.BigEndian.PutUint16(b[6:], f)
}
setIPLength(b[:size], n+part)
data := payload[max(offset-len(first), 0):min(offset+part-len(first), len(payload))]
if offset == 0 {
h = b[:n+copy(b[n:], first)]
}
if err := tun.WritePacket(h, data); err != nil {
return err
}
offset += part
}
return nil
}
// fragmented is a packet of which not every fragment has come yet
type fragmented struct {
// data is what follows its IP header, as far as its last fragment that has come goes
data []byte
// parts are what of data has come, in order and apart from each other
parts [][2]int
// size is that of data when all of it has come, known from the last fragment
size int
known bool
since time.Time
}
// reassembly puts the packets that come in fragments together
type reassembly struct {
mu sync.Mutex
packets map[fragments]*fragmented
}
// fragments is what the fragments of a packet have in common
type fragments struct {
src, dst netip.Addr
proto byte
id uint32
}
// add takes the fragment p of a packet. With the last one that was missing, it returns the packet.
// A packet whose fragments do not fit each other is given up
func (r *reassembly) add(p packet) (packet, bool) {
fragment, offset, end := p.payload, p.offset, p.offset+len(p.payload)
// what an IP packet can carry
most := math.MaxUint16
if p.src.Is4() {
most -= ipv4HeaderSize
}
if len(fragment) == 0 || end > most || p.more && len(fragment)%8 != 0 {
return p, false
}
key, now := fragments{p.src, p.dst, p.proto, p.id}, time.Now()
r.mu.Lock()
defer r.mu.Unlock()
f := r.packets[key]
if f != nil && now.Sub(f.since) > fragmentsTime {
// what is left of a packet whose other fragments were lost: this is one of another packet with its number
f = nil
}
if f == nil {
if _, stale := r.packets[key]; !stale && len(r.packets) >= fragmentsMost {
// the one that is waited for the longest makes room, not the one that comes now
var longest fragments
var since time.Time
for other, old := range r.packets {
if since.IsZero() || old.since.Before(since) {
longest, since = other, old.since
}
}
delete(r.packets, longest)
}
if r.packets == nil {
r.packets = make(map[fragments]*fragmented)
}
f = &fragmented{since: now}
r.packets[key] = f
}
for _, part := range f.parts {
if offset >= part[0] && end <= part[1] {
// came already
return p, false
}
if offset < part[1] && part[0] < end {
// fragments of one packet do not overlap: these are of two, or made to be taken for something else
delete(r.packets, key)
return p, false
}
}
if f.known && end > f.size || !p.more && len(f.data) > end {
// past its end, or an end before what came
delete(r.packets, key)
return p, false
}
if !p.more {
f.size, f.known = end, true
}
if len(f.data) < end {
f.data = append(f.data, make([]byte, end-len(f.data))...)
}
copy(f.data[offset:], fragment)
i := 0
for i < len(f.parts) && f.parts[i][1] < offset {
i++
}
j := i
for ; j < len(f.parts) && f.parts[j][0] <= end; j++ {
offset, end = min(offset, f.parts[j][0]), max(end, f.parts[j][1])
}
f.parts = slices.Replace(f.parts, i, j, [2]int{offset, end})
if !f.known || f.parts[0] != [2]int{0, f.size} {
return p, false
}
delete(r.packets, key)
p.payload, p.fragment = f.data[:f.size], false
return p, true
}
+42 -306
View File
@@ -3,12 +3,7 @@ package tun
import (
"context"
"encoding/binary"
"io"
"math"
"math/rand/v2"
gonet "net"
"net/netip"
"slices"
"sync"
"sync/atomic"
"time"
@@ -20,16 +15,8 @@ import (
// Stack interface implement ip protocol stack, bridging raw network packets and data streams
type Stack interface {
// Start makes the stack read the packets of its tun
Start() error
Close() error
// Input takes a packet of a tun that hands them over instead of being read. The packet is not kept,
// but it may be written to. The packets of one connection have to come one after another
Input(packet []byte)
// DialTCP opens a connection to address from the one of StackOptions.Addresses that is of its family
DialTCP(ctx context.Context, address netip.AddrPort) (net.Conn, error)
// DialUDP opens a port on StackOptions.Addresses. What anyone sends to the port is read from it
DialUDP() (net.PacketConn, error)
}
// StackOptions for the stack implementation
@@ -37,12 +24,6 @@ type StackOptions struct {
Tun Tun
MTU uint32
IdleTimeout time.Duration
// Addresses are what the stack opens connections from
Addresses []netip.Addr
// Remote is set if the tun is a tunnel to another system and not the tun of this one: what comes through it
// has come a long way, so its checksums are looked at, nothing but that way limits the windows, and the
// segments of TCP carry the times of both sides if the other side has them
Remote bool
}
// flow is what the packets of one connection have in common, src is the side of the system
@@ -52,8 +33,7 @@ type flow struct {
// stack turns the packets that the system sends to the tun into connections and back.
// It has only what the two need to talk through a tun, and a local link when the system passes on
// what others send, see tcpConn. The tun may also be a tunnel to a system that is far away, and the stack
// the one that opens the connections
// what others send, see tcpConn
type stack struct {
ctx context.Context
tun Tun
@@ -62,58 +42,32 @@ type stack struct {
segmenter segmenter
// window is the window of every connection
window int32
// verify is set if the checksums of what comes are looked at
verify bool
// stamps is set if connections carry the times of RFC 7323 when the other side does
stamps bool
// handle is given every new connection that the other side opens. Without it those are refused
// handle is given every new connection
handle func(conn net.Conn, destination net.Destination)
udp *udpConnectionHandler
// addresses are what connections are opened from
addresses []netip.Addr
mu sync.Mutex
conns map[flow]*tcpConn
// sockets are the ports that DialUDP has opened
sockets map[netip.AddrPort]*udpSocket
closed bool
// opening is how many of conns wait for the answer to their SYN or SYN-ACK
mu sync.Mutex
conns map[flow]*tcpConn
closed bool
// opening is how many of conns wait for the answer to their SYN-ACK
opening atomic.Int32
// fragments are the packets that are larger than a link on their way and come in parts.
// identification numbers the ones that leave like that: a counter for each of a few groups of destinations,
// each from a start of its own, so that no two of them that go one way meet, and no destination learns how much
// goes to the others
fragments reassembly
identification [64]atomic.Uint32
}
// NewStack builds new ip stack. Without a handler it takes no connections, only opens them
func NewStack(ctx context.Context, options StackOptions, handler ConnectionHandler) (Stack, error) {
// NewStack builds new ip stack
func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) {
s := &stack{
ctx: ctx,
tun: options.Tun,
mtu: int(options.MTU),
window: tcpWindow,
addresses: options.Addresses,
verify: options.Remote,
stamps: options.Remote,
conns: make(map[flow]*tcpConn),
sockets: make(map[netip.AddrPort]*udpSocket),
ctx: ctx,
tun: options.Tun,
mtu: int(options.MTU),
window: tcpWindow,
handle: handler.HandleConnection,
conns: make(map[flow]*tcpConn),
}
if t, ok := options.Tun.(segmenter); ok && t.Segments() {
s.segmenter = t
s.window = tcpLargeWindow
}
if options.Remote {
s.window = tcpLargeWindow
}
for i := range s.identification {
s.identification[i].Store(rand.Uint32())
}
if handler != nil {
s.handle = handler.HandleConnection
s.udp = newUdpConnectionHandler(s.handle, s.writeUDP)
}
s.udp = newUdpConnectionHandler(s.handle, s.writeUDP)
return s, nil
}
@@ -131,8 +85,6 @@ func (s *stack) Close() error {
for _, c := range s.conns {
conns = append(conns, c)
}
sockets := s.sockets
s.sockets = nil
s.mu.Unlock()
for _, c := range conns {
c.mu.Lock()
@@ -141,9 +93,6 @@ func (s *stack) Close() error {
}
c.mu.Unlock()
}
for _, u := range sockets {
u.queue.close()
}
return nil
}
@@ -162,59 +111,19 @@ func (s *stack) read() {
}
return
}
s.Input(b)
}
}
// Input implements Stack. Several tunnels may hand over their packets at once
func (s *stack) Input(b []byte) {
p, ok := parsePacket(b)
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
// would be dialed as that: ::ffff:0.0.0.0 is this machine as well
return
}
if s.verify && b[0]>>4 == 4 && checksum(sum(b[:int(b[0]&0xf)*4], 0)) != 0 {
return
}
// what was put together may be of two packets with one number, its checksum tells
verify := s.verify || p.fragment
if p.fragment {
p, ok = s.fragments.add(p)
}
if !ok || verify && !p.verified() {
return
}
switch p.proto {
case protoTCP:
s.handleTCP(p)
case protoUDP:
s.handleUDP(p)
case protoICMPv4, protoICMPv6:
s.handleICMP(p)
}
}
// 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 {
if a.Is4() == remote.Is4() {
return a, nil
p, ok := parsePacket(b)
if !ok {
continue
}
switch p.proto {
case protoTCP:
s.handleTCP(p)
case protoUDP:
s.handleUDP(p)
case protoICMPv4, protoICMPv6:
s.handleICMP(p)
}
}
return netip.Addr{}, errors.New("no address to reach ", remote, " from")
}
// ephemeral returns a port for what is opened from here: the first that free takes, from a random one on
func ephemeral(free func(port uint16) bool) (uint16, bool) {
start := rand.Uint32()
for i := range uint32(1 << 15) {
if port := uint16(1<<15 | (start+i)&(1<<15-1)); free(port) {
return port, true
}
}
return 0, false
}
func (s *stack) handleTCP(p packet) {
@@ -245,7 +154,7 @@ func (s *stack) handleTCP(p packet) {
if c != nil && c.input(seg) {
return
}
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn || s.handle == nil {
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn {
if seg.flags&tcpRst == 0 {
s.refuse(f, seg)
}
@@ -255,8 +164,7 @@ func (s *stack) handleTCP(p packet) {
if s.opening.Load() >= tcpOpening {
return
}
c = newTCPConn(s, f)
c.synchronize(seg)
c = newTCPConn(s, f, seg)
s.mu.Lock()
if s.closed {
s.mu.Unlock()
@@ -266,7 +174,7 @@ func (s *stack) handleTCP(p packet) {
s.mu.Unlock()
s.opening.Add(1)
c.mu.Lock()
c.sendSyn()
c.sendSynAck()
c.arm(tcpTick)
c.mu.Unlock()
}
@@ -316,189 +224,51 @@ func (s *stack) handleUDP(p packet) {
if size < udpHeaderSize || size > len(u) {
return
}
var socket *udpSocket
if s.handle == nil {
// only for a port that was opened from here
s.mu.Lock()
socket = s.sockets[netip.AddrPortFrom(p.dst, binary.BigEndian.Uint16(u[2:]))]
s.mu.Unlock()
if socket == nil {
return
}
}
// the packet waits for its reader, longer than read keeps it
b := buf.NewWithSize(int32(size - udpHeaderSize))
b.Write(u[udpHeaderSize:size])
src := net.UDPDestination(net.IPAddress(p.src.AsSlice()), net.Port(binary.BigEndian.Uint16(u)))
if socket == nil {
s.udp.HandlePacket(src, net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:]))), b)
} else if b.UDP = &src; !socket.queue.push(b) {
b.Release()
}
dst := net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:])))
s.udp.HandlePacket(src, dst, b)
}
// writeUDP writes a packet with the payload from src to dst to the tun
func (s *stack) writeUDP(payload []byte, src net.Destination, dst net.Destination) error {
srcIP, _ := netip.AddrFromSlice(src.Address.IP())
dstIP, _ := netip.AddrFromSlice(dst.Address.IP())
return s.sendUDP(payload, netip.AddrPortFrom(srcIP, uint16(src.Port)), netip.AddrPortFrom(dstIP, uint16(dst.Port)))
}
func (s *stack) sendUDP(payload []byte, src, dst netip.AddrPort) error {
srcIP, dstIP := src.Addr().Unmap(), dst.Addr().Unmap()
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() || len(payload) > math.MaxUint16-udpHeaderSize {
return errors.New("failed to write udp packet of ", len(payload), " bytes from ", src, " to ", dst)
srcIP, dstIP = srcIP.Unmap(), dstIP.Unmap()
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() {
return errors.New("failed to write udp packet from ", src, " to ", dst)
}
var b [ipv6HeaderSize + udpHeaderSize]byte
n := putIPHeader(b[:], protoUDP, srcIP, dstIP)
u := b[n : n+udpHeaderSize]
binary.BigEndian.PutUint16(u, src.Port())
binary.BigEndian.PutUint16(u[2:], dst.Port())
binary.BigEndian.PutUint16(u, uint16(src.Port))
binary.BigEndian.PutUint16(u[2:], uint16(dst.Port))
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(payload)))
setIPLength(b[:n], n+udpHeaderSize+len(payload))
check := transportChecksum(b[:n], protoUDP, u, payload)
if check == 0 {
// zero stands for no checksum
check = 0xffff
}
binary.BigEndian.PutUint16(u[6:], check)
return s.write(b[:n+udpHeaderSize], payload)
return s.tun.WritePacket(b[:n+udpHeaderSize], payload)
}
// write completes the IP header of a packet that is in two parts and writes it to the tun, in fragments if it is
// larger than the MTU. A segment of a connection never is, and is not written by this
func (s *stack) write(header, payload []byte) error {
size, last := ipv6HeaderSize, 39
if header[0]>>4 == 4 {
size, last = ipv4HeaderSize, 19
}
// the group is chosen by the last bytes of the destination
id := s.identification[(header[last]^header[last-1]<<3)%byte(len(s.identification))].Add(1)
if header[0]>>4 == 4 {
// a link on the way may split it as well, and what is put together there is told apart by its number
binary.BigEndian.PutUint16(header[4:], uint16(id))
}
if len(header)+len(payload) > s.mtu {
return writeFragments(s.tun, s.mtu, id, header, payload)
}
setIPLength(header[:size], len(header)+len(payload))
return s.tun.WritePacket(header, payload)
}
// DialUDP implements Stack
func (s *stack) DialUDP() (net.PacketConn, error) {
u := &udpSocket{stack: s, queue: newPacketQueue(udpQueueLimit)}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil, io.ErrClosedPipe
}
port, ok := ephemeral(func(port uint16) bool {
for _, a := range s.addresses {
if s.sockets[netip.AddrPortFrom(a, port)] != nil {
return false
}
}
return true
})
if !ok || len(s.addresses) == 0 {
return nil, errors.New("no port left on ", s.addresses)
}
u.port = port
for _, a := range s.addresses {
s.sockets[netip.AddrPortFrom(a, port)] = u
}
return u, nil
}
// udpSocket is a UDP port that was opened from here, on every address of the stack. It is not bound to one
// other side: what anyone sends to it is read from it, with who sent it
type udpSocket struct {
stack *stack
port uint16
queue *packetQueue
}
func (u *udpSocket) ReadFrom(p []byte) (int, net.Addr, error) {
b, ok := u.queue.pop()
if !ok {
return 0, nil, gonet.ErrClosed
}
defer b.Release()
return copy(p, b.Bytes()), b.UDP.RawNetAddr(), nil
}
func (u *udpSocket) WriteTo(p []byte, addr net.Addr) (int, error) {
to, ok := addr.(*net.UDPAddr)
if !ok {
return 0, errors.New("not a udp address: ", addr)
}
local, err := u.stack.local(to.AddrPort().Addr().Unmap())
if err != nil {
return 0, err
}
if err := u.stack.sendUDP(p, netip.AddrPortFrom(local, u.port), to.AddrPort()); err != nil {
return 0, err
}
return len(p), nil
}
func (u *udpSocket) Close() error {
u.stack.mu.Lock()
for _, a := range u.stack.addresses {
if local := netip.AddrPortFrom(a, u.port); u.stack.sockets[local] == u {
delete(u.stack.sockets, local)
}
}
u.stack.mu.Unlock()
u.queue.close()
return nil
}
// LocalAddr implements net.PacketConn with the first address of the stack
func (u *udpSocket) LocalAddr() net.Addr {
return gonet.UDPAddrFromAddrPort(netip.AddrPortFrom(u.stack.addresses[0], u.port))
}
func (u *udpSocket) SetDeadline(time.Time) error {
return nil
}
func (u *udpSocket) SetReadDeadline(time.Time) error {
return nil
}
func (u *udpSocket) SetWriteDeadline(time.Time) error {
return nil
}
// handleICMP answers an echo request as its destination would, and takes the word that a packet of a connection
// was too large for a link on its way or found no way at all
// handleICMP answers an echo request as its destination would
func (s *stack) handleICMP(p packet) {
m := p.payload
request, reply := byte(8), byte(0)
if p.proto == protoICMPv6 {
request, reply = 128, 129
}
if len(m) < 8 {
return
}
switch {
case p.proto == protoICMPv4 && m[0] == 3 && m[1] == 4:
s.undelivered(m[8:], max(int(binary.BigEndian.Uint16(m[6:])), ipv4SmallestMTU))
return
case p.proto == protoICMPv6 && m[0] == 2:
s.undelivered(m[8:], max(int(binary.BigEndian.Uint32(m[4:])), ipv6SmallestMTU))
return
case p.proto == protoICMPv4 && m[0] == 3, p.proto == protoICMPv6 && m[0] == 1:
s.undelivered(m[8:], 0)
return
}
if m[0] != request || m[1] != 0 || s.handle == nil && !slices.Contains(s.addresses, p.dst) {
// a stack that takes no connections is one host, not all of them
if len(m) < 8 || m[0] != request || m[1] != 0 {
return
}
var b [ipv6HeaderSize]byte
n := putIPHeader(b[:], p.proto, p.dst, p.src)
setIPLength(b[:n], n+len(m))
// the reply is the request with another type
m[0], m[2], m[3] = reply, 0, 0
if p.proto == protoICMPv6 {
@@ -506,39 +276,5 @@ func (s *stack) handleICMP(p packet) {
} else {
binary.BigEndian.PutUint16(m[2:], checksum(sum(m, 0)))
}
_ = s.write(b[:n], m)
}
// undelivered takes the start of a packet that was sent from here and did not get to where it goes. If it did not fit
// a link on its way, which takes packets of mtu bytes, the connection that sent it goes on with smaller segments.
// Without an mtu there is no way at all, and a connection that is being opened is not waited for
func (s *stack) undelivered(b []byte, mtu int) {
var proto byte
var local, remote netip.Addr
switch {
case len(b) >= ipv4HeaderSize && b[0]>>4 == 4:
proto, local, remote = b[9], netip.AddrFrom4([4]byte(b[12:16])), netip.AddrFrom4([4]byte(b[16:20]))
b = b[min(int(b[0]&0xf)*4, len(b)):]
case len(b) >= ipv6HeaderSize && b[0]>>4 == 6:
proto, local, remote = b[6], netip.AddrFrom16([16]byte(b[8:24])), netip.AddrFrom16([16]byte(b[24:40]))
b = b[ipv6HeaderSize:]
}
// of the segment there are the ports and the sequence number at least
if proto != protoTCP || len(b) < 8 {
return
}
s.mu.Lock()
c := s.conns[flow{
src: netip.AddrPortFrom(remote, binary.BigEndian.Uint16(b[2:])),
dst: netip.AddrPortFrom(local, binary.BigEndian.Uint16(b)),
}]
s.mu.Unlock()
if c == nil {
return
}
if seq := binary.BigEndian.Uint32(b[4:]); mtu != 0 {
c.shrink(seq, mtu)
} else {
c.unreachable(seq)
}
_ = s.tun.WritePacket(b[:n], m)
}
+129 -1453
View File
File diff suppressed because it is too large Load Diff
+41 -287
View File
@@ -1,12 +1,10 @@
package tun
import (
"context"
"encoding/binary"
"io"
"math"
"math/rand/v2"
"net/netip"
"slices"
"sync"
"time"
@@ -39,9 +37,7 @@ const (
tcpHeld = 1024
// tcpSmallest is the smallest mss that the system is believed, the one of Linux
tcpSmallest = 48
// tcpStamps is what the times of both sides take of the options of every segment that carries them (RFC 7323)
tcpStamps = 12
// tcpOpening is how many connections that the other side opens can be waiting for the answer to their SYN-ACK
// tcpOpening is how many connections can be waiting for the answer to their SYN-ACK
tcpOpening = 65535
// tcpStart is how many segments are sent before any of them is acknowledged
tcpStart = 10
@@ -52,18 +48,12 @@ const (
// tcpTick is how often a connection that waits for something looks at what it waits for
tcpTick = 100 * time.Millisecond
// ticks without anything acknowledged before it is sent again, twice as many every next time, a minute at most,
// while an open connection that waits sends again every two seconds at most or as often as an answer takes,
// twice as seldom every next minute: when its way was away for a while it goes on soon after the way is back,
// and the next segment still waits the doubled time, so that its answer can be timed
// ticks without anything acknowledged before it is sent again, twice as many every next time, a minute at most
tcpRetransmitTicks = 3
tcpRetransmitTicksOpen = 20
tcpRetransmitTicksMost = 600
tcpMinuteTicks = int(time.Minute / tcpTick)
// an open connection is given up after four minutes of ticks without an acknowledgment since the timer first sent
// it again, as what is behind a wireless link may be away for a while (ticks, so that the time a computer sleeps
// is not counted), and one that never answered after it was sent again that many times, after twenty seconds
tcpGiveUpTicks = 4 * tcpMinuteTicks
// how many times it is sent again before the connection is given up: that is after four minutes, as what is
// behind a wireless link may be away for a while, and after twenty seconds for one that never answered
tcpRetransmits = 10
tcpSynRetransmits = 5
// ticks between the probes of a window that stays closed
tcpProbeTicks = 10
@@ -72,15 +62,10 @@ const (
)
var (
errTCPReset = errors.New("connection reset")
errTCPTimeout = errors.New("connection timed out")
errTCPRefused = errors.New("connection refused")
errTCPUnreachable = errors.New("destination unreachable")
errTCPReset = errors.New("connection reset")
errTCPTimeout = errors.New("connection timed out")
)
// tcpStarted is where the times that segments carry count from
var tcpStarted = time.Now()
// segment is a TCP segment that came from the tun
type segment struct {
seq, ack uint32
@@ -107,7 +92,7 @@ type held struct {
data *buf.Buffer
}
// tcpConn is a TCP connection that the system opened through the tun, or one that was opened to it by DialTCP.
// tcpConn is a TCP connection that the system opened through the tun.
// Mostly the two are on the same machine, with nothing but the queue of the tun in between, which drops what
// does not fit. But the system may also pass on what others send, as a router does, and then the link to them
// is in between. So a connection has windows that are small enough, keeps what came after a lost segment,
@@ -129,19 +114,12 @@ type tcpConn struct {
sndShift, rcvShift uint8
// sack is set if the system takes selective acknowledgments
sack bool
// stamps is set if every segment carries the time of this side and the last one that came of the other side,
// which sees by it how long its segments take, also the ones that it sent again (RFC 7323).
// stampBase is what the time of this side starts from, stampSent the one that the last segment carried
stamps bool
stampBase, stampSent uint32
mu sync.Mutex
readable sync.Cond
writable sync.Cond
established bool
// opened is there while DialTCP waits for the answer to its SYN, and closed when that wait is over
opened chan struct{}
// closed is set when Close is called
closed bool
// err is why the connection is gone
@@ -162,8 +140,6 @@ type tcpConn struct {
rcvHeld []held
// rcvLast is the seq of the last one that was put into rcvHeld
rcvLast uint32
// stamp is the time of the other side that is sent back to it
stamp uint32
sndUna, sndNxt uint32
sndWnd uint32
@@ -189,9 +165,6 @@ type tcpConn struct {
recover uint32
// lost is the end of what is lost past the last of sacked, resent the end of what was sent again since then
lost, resent uint32
// headAt is when the segment at sndUna was sent again, or took that place while something was lost.
// It is zero when that segment is not sent once more for the system still asking for it: it was already
headAt time.Time
// timed is the end of a segment that was sent at timedAt and not again, to see how long its acknowledgment takes.
// The first one is the SYN-ACK
timed uint32
@@ -203,17 +176,17 @@ type tcpConn struct {
armed bool
// stalled counts ticks without an acknowledgment of what waits for one
stalled, retransmitTicks, retransmits int
// lostTicks counts the ticks since the timer sent again the first time since something was acknowledged
lostTicks int
lingered bool
lingered bool
}
func newTCPConn(s *stack, f flow) *tcpConn {
func newTCPConn(s *stack, f flow, syn segment) *tcpConn {
c := &tcpConn{
stack: s,
flow: f,
src: net.TCPDestination(net.IPAddress(f.src.Addr().AsSlice()), net.Port(f.src.Port())),
dst: net.TCPDestination(net.IPAddress(f.dst.Addr().AsSlice()), net.Port(f.dst.Port())),
rcvNxt: syn.seq + 1,
sndWnd: uint32(syn.window),
window: s.window,
ssthresh: uint32(s.window),
timedAt: time.Now(),
@@ -223,17 +196,6 @@ func newTCPConn(s *stack, f flow) *tcpConn {
c.writable.L = &c.mu
c.ipSize = putIPHeader(c.ip[:], protoTCP, f.dst.Addr(), f.src.Addr())
c.mss = s.mtu - c.ipSize - tcpHeaderSize
c.sndUna = rand.Uint32()
c.sndNxt = c.sndUna + 1
c.stampBase = rand.Uint32()
return c
}
// synchronize takes what the SYN of the other side says. c.mu is held, or nobody else has c yet
func (c *tcpConn) synchronize(syn segment) {
c.rcvNxt = syn.seq + 1
c.sndWnd = uint32(syn.window)
c.sack, c.stamps, c.sndShift, c.rcvShift = false, false, 0, 0
for kind, value, o := nextOption(syn.options); kind != 0; kind, value, o = nextOption(o) {
switch {
case kind == 2 && len(value) == 2:
@@ -243,77 +205,20 @@ func (c *tcpConn) synchronize(syn segment) {
c.rcvShift = tcpWindowShift
case kind == 4:
c.sack = true
case kind == 8 && len(value) == 8 && c.stack.stamps:
c.stamps, c.stamp = true, binary.BigEndian.Uint32(value)
}
}
if c.stamps {
c.mss -= tcpStamps
}
c.mss = max(c.mss, tcpSmallest)
c.cwnd = tcpStart * uint32(c.mss)
}
// DialTCP implements Stack
func (s *stack) DialTCP(ctx context.Context, remote netip.AddrPort) (net.Conn, error) {
remote = netip.AddrPortFrom(remote.Addr().Unmap(), remote.Port())
local, err := s.local(remote.Addr())
if err != nil {
return nil, err
}
opened := make(chan struct{})
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil, io.ErrClosedPipe
}
f := flow{src: remote}
port, ok := ephemeral(func(port uint16) bool {
f.dst = netip.AddrPortFrom(local, port)
return s.conns[f] == nil
})
if !ok {
s.mu.Unlock()
return nil, errors.New("no port left to reach ", remote, " from")
}
f.dst = netip.AddrPortFrom(local, port)
c := newTCPConn(s, f)
// what the SYN offers, until the answer says what of it is taken
c.sack, c.stamps, c.rcvShift, c.opened = true, s.stamps, tcpWindowShift, opened
s.conns[f] = c
s.mu.Unlock()
s.opening.Add(1)
c.mu.Lock()
if c.err == nil {
c.sendSyn()
c.arm(tcpTick)
}
c.mu.Unlock()
select {
case <-opened:
case <-ctx.Done():
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.established {
if c.err == nil {
c.abort(ctx.Err())
}
return nil, c.err
}
return c, nil
c.sndUna = rand.Uint32()
c.sndNxt = c.sndUna + 1
return c
}
// header writes the headers of a segment with size bytes of payload into b and returns their size.
// The checksum of the segment is left to write. c.mu is held
func (c *tcpConn) header(b []byte, seq uint32, flags byte, options []byte, size int) int {
n := copy(b, c.ip[:c.ipSize])
stamps := 0
if c.stamps {
stamps = tcpStamps
}
t := b[n : n+tcpHeaderSize+stamps+len(options)]
t := b[n : n+tcpHeaderSize+len(options)]
binary.BigEndian.PutUint16(t, c.flow.dst.Port())
binary.BigEndian.PutUint16(t[2:], c.flow.src.Port())
binary.BigEndian.PutUint32(t[4:], seq)
@@ -328,18 +233,7 @@ func (c *tcpConn) header(b []byte, seq uint32, flags byte, options []byte, size
binary.BigEndian.PutUint16(t[14:], uint16(min(window>>c.rcvShift, math.MaxUint16)))
}
t[16], t[17], t[18], t[19] = 0, 0, 0, 0
if c.stamps {
o := t[tcpHeaderSize:]
o[0], o[1], o[2], o[3] = 1, 1, 8, 10
if !c.sending {
// what is sent while WriteMultiBuffer writes a segment has the time of that one: it may get to the tun
// first, and the other side drops a segment with a time before the last one that it took
c.stampSent = c.stampBase + uint32(time.Since(tcpStarted)/time.Millisecond)
}
binary.BigEndian.PutUint32(o[4:], c.stampSent)
binary.BigEndian.PutUint32(o[8:], c.stamp)
}
copy(t[tcpHeaderSize+stamps:], options)
copy(t[tcpHeaderSize:], options)
setIPLength(b[:n], n+len(t)+size)
c.rcvAcked = c.rcvNxt
c.rcvAdvertised = window
@@ -360,8 +254,7 @@ func (c *tcpConn) send(seq uint32, flags byte, options, payload []byte) {
c.write(b[:c.header(b[:], seq, flags, options, len(payload))], payload)
}
// sendSyn sends the answer to the SYN of the other side, or the SYN of a connection that is opened from here
func (c *tcpConn) sendSyn() {
func (c *tcpConn) sendSynAck() {
options := [12]byte{2, 4}
binary.BigEndian.PutUint16(options[2:], uint16(c.stack.mtu-c.ipSize-tcpHeaderSize))
n := 4
@@ -371,11 +264,7 @@ func (c *tcpConn) sendSyn() {
if c.rcvShift != 0 {
n += copy(options[n:], []byte{1, 3, 3, c.rcvShift})
}
flags := byte(tcpSyn | tcpAck)
if c.opened != nil {
flags = tcpSyn
}
c.send(c.sndUna, flags, options[:n], nil)
c.send(c.sndUna, tcpSyn|tcpAck, options[:n], nil)
}
// sendAck acknowledges what was received, and tells what of rcvHeld waits for a lost segment
@@ -389,12 +278,8 @@ func (c *tcpConn) sendAck() {
c.send(c.sndNxt, tcpAck, nil, nil)
return
}
// the block with the segment that came last goes first. Next to the times there is room for one less
var room [4][2]uint32
blocks := room[:]
if c.stamps {
blocks = room[:3]
}
// the block with the segment that came last goes first
var blocks [4][2]uint32
n, last := 1, false
for i := 0; i < len(c.rcvHeld); {
start, end, recent := c.rcvHeld[i].seq, c.rcvHeld[i].seq, false
@@ -421,22 +306,6 @@ func (c *tcpConn) sendAck() {
c.send(c.sndNxt, tcpAck, options[:4+8*len(list)], nil)
}
// stamped keeps the time that a segment of the other side carries, to send it back. It is the one of the segment
// that the stream goes on with, so that the other side sees how long that one took, and not one of what came
// before it. c.mu is held
func (c *tcpConn) stamped(s segment) {
if int32(s.seq-c.rcvAcked) > 0 {
return
}
for kind, value, o := nextOption(s.options); kind != 0; kind, value, o = nextOption(o) {
if kind == 8 && len(value) == 8 {
if stamp := binary.BigEndian.Uint32(value); int32(stamp-c.stamp) > 0 {
c.stamp = stamp
}
}
}
}
// input takes a segment of the connection. It returns false if the connection is gone
// and the segment is the start of another one
func (c *tcpConn) input(s segment) bool {
@@ -445,32 +314,6 @@ func (c *tcpConn) input(s segment) bool {
if c.err != nil {
return s.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn
}
if c.opened != nil {
// only the answer to the SYN is waited for, or the word that nobody takes it
switch {
case s.flags&tcpAck == 0:
case s.ack != c.sndNxt:
// of a connection that the other side still has from before: it is told that there is none
if s.flags&tcpRst == 0 {
c.stack.refuse(c.flow, s)
}
case s.flags&tcpRst != 0:
c.abort(errTCPRefused)
case s.flags&tcpSyn != 0:
c.synchronize(s)
c.established = true
c.sndUna, c.sndBase = s.ack, s.ack
c.stalled, c.retransmits = 0, 0
if !c.timedAt.IsZero() {
c.clocked()
}
c.stack.opening.Add(-1)
close(c.opened)
c.opened = nil
c.sendAck()
}
return true
}
if s.flags&tcpRst != 0 {
// from where the system is, or was before its FIN, not from someone who knows no more than the addresses
if s.seq-c.rcvNxt <= uint32(c.window) || c.rcvFin && s.seq+1 == c.rcvNxt {
@@ -481,12 +324,9 @@ func (c *tcpConn) input(s segment) bool {
if s.flags&tcpSyn != 0 {
switch {
case !c.established && s.seq+1 == c.rcvNxt:
c.sendSyn()
c.sendSynAck()
c.timedAt = time.Time{}
case c.closed && s.flags&tcpAck == 0 && !c.stack.verify:
// the system of this machine opens the same ports again: it has let the old connection go.
// What comes through a tunnel is answered with what is expected instead, as anyone who knows
// the addresses could send it, and the other side answers that with a reset if it is gone
case c.closed && s.flags&tcpAck == 0:
c.abort(errTCPReset)
return false
default:
@@ -497,9 +337,6 @@ func (c *tcpConn) input(s segment) bool {
if s.flags&tcpAck == 0 {
return true
}
if c.stamps {
c.stamped(s)
}
if !c.established {
if s.ack != c.sndNxt {
return true
@@ -548,7 +385,6 @@ func (c *tcpConn) acknowledge(s segment) {
if !c.timedAt.IsZero() && int32(c.sndUna-c.timed) >= 0 {
c.clocked()
}
recovering := c.recovering
switch {
case c.recovering && int32(c.sndUna-c.recover) >= 0:
c.recovering = false
@@ -557,18 +393,11 @@ func (c *tcpConn) acknowledge(s segment) {
if end := c.sndUna + min(uint32(c.mss), c.recover-c.sndUna); int32(end-c.lost) > 0 {
c.lost = end
}
c.headAt = time.Now()
}
// the way takes more if what it was given has arrived, as long as it was given about as much as it takes
if sent >= c.cwnd/2 {
if c.cwnd < c.ssthresh {
grow := acked
if recovering {
// after a timeout: one segment that is sent again can acknowledge all that came after it,
// which says nothing about how much the way takes at once
grow = min(grow, 2*uint32(c.mss))
}
c.cwnd = min(c.cwnd+grow, c.ssthresh)
c.cwnd = min(c.cwnd+acked, c.ssthresh)
} else if !c.recovering {
// a segment more for every cwnd that arrives
c.cwnd = min(c.cwnd+max(uint32(uint64(c.mss)*uint64(acked)/uint64(c.cwnd)), 1), uint32(c.window))
@@ -588,22 +417,12 @@ func (c *tcpConn) acknowledge(s segment) {
}
}
}
if !c.recovering && c.sndUna != c.sndNxt && (len(c.sacked) > 0 || !c.sack && c.dupAcks >= 3) {
// the system has something past sndUna, or says the same for the third time when it cannot say what it has:
// the segment at sndUna is lost. One that can say it repeats itself also for what it got twice
if !c.recovering && c.sndUna != c.sndNxt && (len(c.sacked) > 0 || c.dupAcks >= 3) {
// the system has something past sndUna, or says the same for the third time: the segment at sndUna is lost
c.congested()
c.lost = c.sndUna + min(uint32(c.mss), c.sndNxt-c.sndUna)
}
if c.recovering {
if acked == 0 && c.srtt != 0 && !c.headAt.IsZero() && time.Since(c.headAt) > max(c.srtt+max(c.srtt/4, 4*c.rttvar), tcpTick/4) {
// the segment that the system waits for was sent again longer ago than an answer takes, and the system
// still answers what comes after it: that one was lost as well. It is sent once more, and only once:
// the timer would find it too, but late, and then nothing is known anymore of how much the way takes
if start, end := c.hole(c.sndUna); start == c.sndUna && start != end {
c.retransmit(start, start+min(end-start, uint32(c.mss)))
c.headAt, c.stalled = time.Time{}, 0
}
}
c.resend()
}
c.sndWnd = window
@@ -632,12 +451,7 @@ func (c *tcpConn) clocked() {
c.rttvar += ((c.srtt - d).Abs() - c.rttvar) / 4
c.srtt += (d - c.srtt) / 8
}
c.retransmitTicks = c.answerTicks()
}
// answerTicks is how many ticks an acknowledgment is waited for as long as nothing was sent again. c.mu is held
func (c *tcpConn) answerTicks() int {
return max(tcpRetransmitTicks, int((c.srtt+4*c.rttvar)/tcpTick)+2)
c.retransmitTicks = max(tcpRetransmitTicks, int((c.srtt+4*c.rttvar)/tcpTick)+2)
}
// sackAdd puts what the system says it has into sacked. c.mu is held
@@ -667,7 +481,7 @@ func (c *tcpConn) congested() {
c.ssthresh = max(min(c.cwnd, c.sndNxt-c.sndUna)/2, 2*uint32(c.mss))
c.cwnd = c.ssthresh
c.recovering, c.recover, c.resent = true, c.sndNxt, c.sndUna
c.timedAt, c.headAt = time.Time{}, time.Time{}
c.timedAt = time.Time{}
}
// hole returns the first part at or after seq of what is lost: what the system does not have
@@ -729,10 +543,6 @@ func (c *tcpConn) resend() {
room = c.cwnd - flight
}
end = start + min(end-start, room)
if start == c.sndUna {
// its answer is waited for from now on
c.headAt, c.stalled = time.Now(), 0
}
c.retransmit(start, end)
c.resent = end
}
@@ -832,9 +642,9 @@ func (c *tcpConn) release() {
// hold keeps a segment that is not the next one. c.mu is held
func (c *tcpConn) hold(seq uint32, payload []byte) {
i := len(c.rcvHeld)
for i > 0 && int32(c.rcvHeld[i-1].seq-seq) >= 0 {
i--
i := 0
for i < len(c.rcvHeld) && int32(c.rcvHeld[i].seq-seq) < 0 {
i++
}
// without what the one before and the one after have
if i > 0 {
@@ -856,9 +666,8 @@ func (c *tcpConn) hold(seq uint32, payload []byte) {
if len(payload) == 0 || len(c.rcvHeld) == tcpHeld {
return
}
// as much memory as it carries: the room that is left counts bytes, and a thousand tiny segments in pooled
// buffers would take two megabytes
b := buf.FromBytes(slices.Clone(payload))
b := buf.NewWithSize(int32(len(payload)))
b.Write(payload)
c.rcvHeld = slices.Insert(c.rcvHeld, i, held{seq, b})
c.rcvLast = seq
}
@@ -907,38 +716,6 @@ func (c *tcpConn) queued(parts [][]byte, seq uint32, size int) ([][]byte, int) {
return parts, n
}
// shrink makes the segments fit a link on the way that takes packets of mtu bytes, when the one at seq did not.
// None of what is on its way fits: it is sent again, the first segment at once and the others as they are
// acknowledged. Only what is said about a segment that was sent and is not acknowledged is believed
func (c *tcpConn) shrink(seq uint32, mtu int) {
c.mu.Lock()
defer c.mu.Unlock()
mss := mtu - c.ipSize - tcpHeaderSize
if c.stamps {
mss -= tcpStamps
}
mss = max(mss, tcpSmallest)
if c.err != nil || !c.established || seq-c.sndUna >= c.sndNxt-c.sndUna || mss >= c.mss {
return
}
c.mss = mss
// as what is lost, without the way taking less for it: it is the size that it did not take
c.recovering, c.recover, c.lost = true, c.sndNxt, c.sndNxt
c.timedAt = time.Time{}
c.resent = c.sndUna + min(uint32(mss), c.sndNxt-c.sndUna)
c.headAt = time.Now()
c.retransmit(c.sndUna, c.resent)
}
// unreachable ends the wait for the answer to the SYN at seq, which a router on the way could not deliver
func (c *tcpConn) unreachable(seq uint32) {
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil && c.opened != nil && seq == c.sndUna {
c.abort(errTCPUnreachable)
}
}
// arm makes tick run after d, unless it is going to run already. c.mu is held
func (c *tcpConn) arm(d time.Duration) {
if c.armed {
@@ -965,34 +742,19 @@ func (c *tcpConn) tick() {
}
switch {
case c.sndUna != c.sndNxt:
c.stalled++
due := c.stalled >= c.retransmitTicks
if c.established && c.retransmits > 0 {
due = due || c.stalled >= max(tcpRetransmitTicksOpen, c.answerTicks())<<(c.lostTicks/tcpMinuteTicks)
}
if due {
if c.retransmits++; c.retransmits == 1 {
c.lostTicks = 0
} else {
c.lostTicks += c.stalled
}
if c.established && c.lostTicks > tcpGiveUpTicks || !c.established && c.retransmits > tcpSynRetransmits {
if c.stalled++; c.stalled >= c.retransmitTicks {
if c.retransmits++; c.retransmits > tcpRetransmits || !c.established && c.retransmits > tcpSynRetransmits {
c.abort(errTCPTimeout)
return
}
c.stalled = 0
c.retransmitTicks = min(c.retransmitTicks*2, tcpRetransmitTicksMost)
if !c.established {
c.sendSyn()
c.sendSynAck()
c.timedAt = time.Time{}
} else {
// what the system does not say it has is lost, and the way to it is tried with a segment again.
// It takes half of what it took before the loss, not half of that again if the loss was seen already
ssthresh, recovering := c.ssthresh, c.recovering
// what the system does not say it has is lost, and the way to it is tried with a segment again
c.congested()
if recovering {
c.ssthresh = ssthresh
}
c.cwnd, c.lost = uint32(c.mss), c.sndNxt
c.resend()
}
@@ -1030,10 +792,6 @@ func (c *tcpConn) abort(err error) {
if c.timer != nil {
c.timer.Stop()
}
if c.opened != nil {
close(c.opened)
c.opened = nil
}
for _, h := range c.rcvHeld {
h.data.Release()
}
@@ -1121,9 +879,9 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
c.writing = len(mb)
// a segment is one part of a Buffer, or as much as there is to send if the tun splits it by itself
var room [tcpParts][]byte
parts := room[:0:1]
limit, parts := c.mss, room[:0:1]
if c.stack.segmenter != nil {
parts = room[:0]
limit, parts = tcpSegment, room[:0]
}
for end := c.sndNxt + size; c.sndNxt != end; {
// not a part of a segment when all of it can go later, with what is acknowledged by then
@@ -1139,11 +897,7 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
if c.err != nil {
break
}
// the mss may have become smaller while the last segment was written
most := min(c.mss, int(c.space()))
if c.stack.segmenter != nil {
most = min(tcpSegment, int(c.space()))
}
most := min(limit, int(c.space()))
if most > c.mss {
// the same for what the tun splits
most -= most % c.mss
@@ -1153,7 +907,7 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
if c.sndNxt+uint32(n) == end {
flags |= tcpPsh
}
var b [ipv6HeaderSize + tcpHeaderSize + tcpStamps]byte
var b [ipv6HeaderSize + tcpHeaderSize]byte
h := b[:c.header(b[:], c.sndNxt, flags, nil, n)]
if c.sndUna == c.sndNxt {
// the ticks without an acknowledgment are counted from the first segment that waits for one
+16 -76
View File
@@ -39,7 +39,9 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
u.RLock()
conn, found := u.udpConns[src]
if found {
if !conn.queue.push(data) {
select {
case conn.egress <- data:
default:
errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full")
data.Release()
}
@@ -53,14 +55,17 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
conn, found = u.udpConns[src]
if !found {
conn = &udpConn{handler: u, queue: newPacketQueue(udpQueueLimit), src: src, dst: dst}
egress := make(chan *buf.Buffer, 1024)
conn = &udpConn{handler: u, egress: egress, src: src, dst: dst}
u.udpConns[src] = conn
go u.handleConnection(conn, dst)
}
// queue the packet for the reader of the connection, or discard it
if !conn.queue.push(data) {
// send packet data to the egress channel, if it has buffer, or discard
select {
case conn.egress <- data:
default:
errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full 2")
data.Release()
}
@@ -71,7 +76,7 @@ func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
// Close runs twice per flow; a newer conn may already own this src.
if u.udpConns[conn.src] == conn {
delete(u.udpConns, conn.src)
conn.queue.close()
close(conn.egress)
}
u.Unlock()
}
@@ -80,13 +85,13 @@ func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
type udpConn struct {
handler *udpConnectionHandler
queue *packetQueue
src net.Destination
dst net.Destination
egress chan *buf.Buffer
src net.Destination
dst net.Destination
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
b, ok := c.queue.pop()
b, ok := <-c.egress
if !ok {
return nil, io.EOF
}
@@ -95,7 +100,7 @@ func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
// Read packets from the connection
func (c *udpConn) Read(p []byte) (int, error) {
b, ok := c.queue.pop()
b, ok := <-c.egress
if !ok {
return 0, io.EOF
}
@@ -132,7 +137,7 @@ func (c *udpConn) Write(p []byte) (int, error) {
// sending packets back mean sending payload with source/destination reversed
err := c.handler.writePacket(p, c.dst, c.src)
if err != nil {
return 0, err
return 0, nil
}
return len(p), nil
@@ -163,68 +168,3 @@ func (c *udpConn) SetReadDeadline(t time.Time) error {
func (c *udpConn) SetWriteDeadline(t time.Time) error {
return nil
}
// udpQueueLimit bounds the packets waiting for one UDP flow; more are dropped.
const udpQueueLimit = 1024
// packetQueue holds the packets waiting for one UDP flow. Unlike a buffered
// channel of the same bound it only allocates for packets actually queued, so
// the many idle flows kept until the idle timeout cost next to nothing.
type packetQueue struct {
mu sync.Mutex
items []*buf.Buffer
limit int
notify chan struct{}
closed bool
}
func newPacketQueue(limit int) *packetQueue {
return &packetQueue{limit: limit, notify: make(chan struct{}, 1)}
}
// push queues b and reports whether it was accepted.
func (q *packetQueue) push(b *buf.Buffer) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed || len(q.items) >= q.limit {
return false
}
q.items = append(q.items, b)
select {
case q.notify <- struct{}{}:
default:
}
return true
}
// pop blocks until a packet is queued or the queue is closed and drained.
func (q *packetQueue) pop() (*buf.Buffer, bool) {
for {
q.mu.Lock()
if len(q.items) > 0 {
b := q.items[0]
q.items[0] = nil
q.items = q.items[1:]
if len(q.items) == 0 {
q.items = nil
}
q.mu.Unlock()
return b, true
}
if q.closed {
q.mu.Unlock()
return nil, false
}
q.mu.Unlock()
<-q.notify
}
}
func (q *packetQueue) close() {
q.mu.Lock()
defer q.mu.Unlock()
if !q.closed {
q.closed = true
close(q.notify)
}
}
+15 -6
View File
@@ -6,9 +6,11 @@ import (
gotls "crypto/tls"
"encoding/base64"
"io"
"reflect"
"strconv"
"strings"
"time"
"unsafe"
"github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/app/reverse"
@@ -546,7 +548,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
}
var input *bytes.Reader
var rawInput **bytes.Buffer
var rawInput *bytes.Buffer
switch requestAddons.Flow {
case vless.XRV:
if account.Flow == requestAddons.Flow {
@@ -558,23 +560,30 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
inbound.CanSpliceCopy = 3
fallthrough // we will break Mux connections that contain TCP requests
case protocol.RequestCommandTCP:
var target net.Conn
var t reflect.Type
var p uintptr
if commonConn, ok := connection.(*encryption.CommonConn); ok {
if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransportWithoutSecurity(iConn) {
inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport / another securityConn should not be penetrated
}
target = commonConn
t = reflect.TypeOf(commonConn).Elem()
p = uintptr(unsafe.Pointer(commonConn))
} else if tlsConn, ok := iConn.(*tls.Conn); ok {
if tlsConn.ConnectionState().Version != gotls.VersionTLS13 {
return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, tlsConn.ConnectionState().Version)
}
target = tlsConn.Conn
t = reflect.TypeOf(tlsConn.Conn).Elem()
p = uintptr(unsafe.Pointer(tlsConn.Conn))
} else if realityConn, ok := iConn.(*reality.Conn); ok {
target = realityConn.Conn
t = reflect.TypeOf(realityConn.Conn).Elem()
p = uintptr(unsafe.Pointer(realityConn.Conn))
} else {
return errors.New("XTLS only supports TLS and REALITY directly for now.")
}
input, rawInput = vless.AccessTLSBuffers(target)
i, _ := t.FieldByName("input")
r, _ := t.FieldByName("rawInput")
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))
}
} else {
return errors.New("account " + account.ID.String() + " is not able to use the flow " + requestAddons.Flow)
+17 -7
View File
@@ -5,9 +5,11 @@ import (
"context"
gotls "crypto/tls"
"encoding/base64"
"reflect"
"strings"
"sync"
"time"
"unsafe"
utls "github.com/refraction-networking/utls"
proxymanConfig "github.com/xtls/xray-core/app/proxyman"
@@ -242,7 +244,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
}
var input *bytes.Reader
var rawInput **bytes.Buffer
var rawInput *bytes.Buffer
allowUDP443 := false
switch requestAddons.Flow {
case vless.XRV + "-udp443":
@@ -259,22 +261,30 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
case protocol.RequestCommandMux:
fallthrough // let server break Mux connections that contain TCP requests
case protocol.RequestCommandTCP, protocol.RequestCommandRvs:
var target net.Conn
var t reflect.Type
var p uintptr
if commonConn, ok := conn.(*encryption.CommonConn); ok {
if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransportWithoutSecurity(iConn) {
ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport / another securityConn should not be penetrated
}
target = commonConn
t = reflect.TypeOf(commonConn).Elem()
p = uintptr(unsafe.Pointer(commonConn))
} else if tlsConn, ok := iConn.(*tls.Conn); ok {
target = tlsConn.Conn
t = reflect.TypeOf(tlsConn.Conn).Elem()
p = uintptr(unsafe.Pointer(tlsConn.Conn))
} else if utlsConn, ok := iConn.(*tls.UConn); ok {
target = utlsConn.Conn
t = reflect.TypeOf(utlsConn.Conn).Elem()
p = uintptr(unsafe.Pointer(utlsConn.Conn))
} else if realityConn, ok := iConn.(*reality.UConn); ok {
target = realityConn.Conn
t = reflect.TypeOf(realityConn.Conn).Elem()
p = uintptr(unsafe.Pointer(realityConn.Conn))
} else {
return errors.New("XTLS only supports TLS and REALITY directly for now.")
}
input, rawInput = vless.AccessTLSBuffers(target)
i, _ := t.FieldByName("input")
r, _ := t.FieldByName("rawInput")
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))
default:
panic("unknown VLESS request command")
}
-21
View File
@@ -5,28 +5,7 @@
// clients with 'socks' for proxying.
package vless
import (
"bytes"
"net"
"reflect"
"github.com/xtls/xray-core/common/utils"
)
const (
None = "none"
XRV = "xtls-rprx-vision"
)
// Old TLS lib holds bytes.Buffer, but new TLS lib holds *bytes.Buffer
// Since Xray has 3 TLS libraries (std, utls, REALITY server)
// we detect the type of rawInput and return the appropriate pointer
func AccessTLSBuffers(conn net.Conn) (*bytes.Reader, **bytes.Buffer) {
input := utils.AccessField[bytes.Reader](conn, "input")
field := reflect.ValueOf(conn).Elem().FieldByName("rawInput")
if field.Kind() == reflect.Pointer {
return input, utils.AccessField[*bytes.Buffer](conn, "rawInput")
}
raw := utils.AccessField[bytes.Buffer](conn, "rawInput")
return input, &raw
}
+14 -11
View File
@@ -16,9 +16,8 @@ import (
)
type bind struct {
ctx context.Context
resolveFunc func(host string) (net.IP, error)
listenFunc func(ctx context.Context) (net.PacketConn, error)
listenFunc func() (net.PacketConn, error)
downFunc func() error
reserved []byte
@@ -27,12 +26,6 @@ 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()
@@ -41,7 +34,7 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
return nil, 0, conn.ErrBindAlreadyOpen
}
c, err := b.listenFunc(b.ctx)
c, err := b.listenFunc()
if err != nil {
return nil, 0, err
}
@@ -59,9 +52,12 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
case <-ch:
default:
errors.LogErrorInner(context.Background(), err, "unexpected closed")
if b.downFunc != nil {
b.mu.Lock()
downFunc := b.downFunc
b.mu.Unlock()
if downFunc != nil {
go func() {
common.Must(b.downFunc())
common.Must(downFunc())
}()
}
}
@@ -83,6 +79,13 @@ 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()
+151 -167
View File
@@ -7,7 +7,6 @@ import (
"reflect"
"strings"
"sync"
"sync/atomic"
"time"
"golang.zx2c4.com/wireguard/tun"
@@ -32,14 +31,18 @@ import (
)
type Handler struct {
p policy.Manager
conf *DeviceConfig
policyManager policy.Manager
dns dns.Client
canup *atomic.Bool
tnet *Net
bind *bind
dev *device.Device
ipc string
mu sync.Mutex
streamSettings *internet.MemoryStreamConfig
uplinkCounter stats.Counter
downlinkCounter stats.Counter
tun tun.Device
tnet *Net
dev *device.Device
mu sync.Mutex
}
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
@@ -114,93 +117,24 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
errors.LogWarning(context.Background(), "Using kernel TUN")
tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu))
} else {
errors.LogWarning(context.Background(), "Using built-in TCP/IP stack")
tun, tnet, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), nil)
errors.LogWarning(context.Background(), "Using gVisor TUN")
tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true)
}
if err != nil {
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{
p: p,
conf: conf,
policyManager: p,
dns: d,
canup: blockup.canup,
tnet: tnet,
bind: bind,
dev: dev,
ipc: cfg.String(),
streamSettings: streamSettings,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
tun: tun,
tnet: tnet,
}, nil
}
@@ -215,7 +149,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.CanSpliceCopy = 3
dialer.SetOutboundGateway(ctx, ob)
if err := h.up(ctx); err != nil {
if err := h.init(ctx); err != nil {
return err
}
@@ -225,7 +159,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
newCtx, newCancel = context.WithCancel(context.Background())
}
sessionPolicy := h.p.ForLevel(0)
sessionPolicy := h.policyManager.ForLevel(0)
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, func() {
cancel()
@@ -300,72 +234,150 @@ 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) up(ctx context.Context) error {
func (h *Handler) init(ctx context.Context) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.dev == nil {
if h.tun == nil {
return errors.New("closed")
}
h.bind.SetCtx(ctx)
if !h.canup.Load() {
h.canup.Store(true)
if h.dev != nil {
return h.dev.Up()
}
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})
resolveFunc := h.resolveLocal
listenFunc := func() (net.PacketConn, error) {
dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint)
if err != nil {
return nil, 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)
}
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)
}
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
}
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)
}
if len(got) == 0 {
return nil, dns.ErrEmptyResponse
switch c := conn.(type) {
case *net.PacketConnWrapper:
pktConn = c.PacketConn
case *cnc.Connection:
pktConn = &internet.FakePacketConn{Conn: c}
default:
panic(reflect.TypeOf(c))
}
}
return got[dice.Roll(len(got))], nil
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 {
got = got4
}
}
if len(got) == 0 {
return nil, dns.ErrEmptyResponse
}
}
return got[dice.Roll(len(got))], nil
}
type UDPConnClient struct {
@@ -498,31 +510,3 @@ 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
}
+91
View File
@@ -0,0 +1,91 @@
package wireguard
import (
"context"
"github.com/xtls/xray-core/common/errors"
tunicmp "github.com/xtls/xray-core/proxy/tun/icmp"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
)
// CreateICMPEchoResponder answers ICMP echo requests from peers locally, the way
// the TUN inbound does: ICMP is not proxied, but ping and connectivity checks
// through the tunnel get a reply instead of timing out.
//
// In promiscuous mode gVisor skips its own IPv4 echo reply for addresses that are
// not assigned to the NIC and leaves it to a custom handler; IPv6 is registered
// too so both families behave the same.
func CreateICMPEchoResponder(gstack *stack.Stack) {
gstack.SetTransportProtocolHandler(icmp.ProtocolNumber4, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
return handleICMPEcho(gstack, header.IPv4ProtocolNumber, id, pkt)
})
gstack.SetTransportProtocolHandler(icmp.ProtocolNumber6, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
return handleICMPEcho(gstack, header.IPv6ProtocolNumber, id, pkt)
})
}
func handleICMPEcho(gstack *stack.Stack, netProto tcpip.NetworkProtocolNumber, id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
srcIP := id.RemoteAddress
dstIP := id.LocalAddress
if srcIP.Len() == 0 || dstIP.Len() == 0 {
return true
}
headerBytes := pkt.TransportHeader().Slice()
payloadBytes := pkt.Data().AsRange().ToSlice()
message := make([]byte, len(headerBytes)+len(payloadBytes))
copy(message, headerBytes)
copy(message[len(headerBytes):], payloadBytes)
if _, _, ok := tunicmp.ParseEchoRequest(netProto, message); !ok {
return true
}
reply, err := tunicmp.BuildLocalEchoReply(netProto, message, dstIP, srcIP)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to build local icmp echo reply")
return true
}
if err := writeRawICMPPacket(gstack, netProto, reply, dstIP, srcIP); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to write local icmp echo reply")
}
return true
}
func writeRawICMPPacket(gstack *stack.Stack, netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: header.IPv6MinimumSize,
Payload: buffer.MakeWithData(message),
})
defer pkt.DecRef()
if netProto == header.IPv4ProtocolNumber {
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
ipHdr.Encode(&header.IPv4Fields{
TotalLength: uint16(header.IPv4MinimumSize + len(message)),
TTL: 64,
Protocol: uint8(header.ICMPv4ProtocolNumber),
SrcAddr: srcIP,
DstAddr: dstIP,
})
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
} else {
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
ipHdr.Encode(&header.IPv6Fields{
PayloadLength: uint16(len(message)),
TransportProtocol: header.ICMPv6ProtocolNumber,
HopLimit: 64,
SrcAddr: srcIP,
DstAddr: dstIP,
})
}
if err := gstack.WriteRawPacket(1, netProto, buffer.MakeWithView(pkt.ToView())); err != nil {
return errors.New("failed to write raw icmp packet back to stack ", err)
}
return nil
}
+176
View File
@@ -0,0 +1,176 @@
package wireguard
import (
"bytes"
"net/netip"
"testing"
"time"
"github.com/xtls/xray-core/common/net"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
func newICMPTestStack(t *testing.T) *netTun {
t.Helper()
dev, _, gstack, err := CreateNetTUN([]netip.Addr{
netip.MustParseAddr("10.66.0.1"),
netip.MustParseAddr("fd00::1"),
}, nil, 1420, false)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { dev.Close() })
CreateForwarder(gstack, func(conn net.Conn, dest net.Destination) { conn.Close() })
CreateICMPEchoResponder(gstack)
return dev.(*netTun)
}
// startReader must run before the request is written: the stack may answer
// synchronously inside Write, and netTun hands packets over an unbuffered channel.
func startReader(dev *netTun) <-chan []byte {
got := make(chan []byte, 1)
go func() {
buf := make([]byte, 2048)
sizes := make([]int, 1)
if _, err := dev.Read([][]byte{buf}, sizes, 0); err == nil {
got <- buf[:sizes[0]]
}
}()
return got
}
func awaitPacket(t *testing.T, got <-chan []byte) []byte {
t.Helper()
select {
case p := <-got:
return p
case <-time.After(2 * time.Second):
t.Fatal("no echo reply from the stack")
return nil
}
}
func TestICMPv4EchoReply(t *testing.T) {
dev := newICMPTestStack(t)
src := tcpip.AddrFrom4([4]byte{10, 66, 0, 2})
dst := tcpip.AddrFrom4([4]byte{1, 1, 1, 1})
payload := []byte("xray wireguard ping")
icmpMsg := make([]byte, header.ICMPv4MinimumSize+len(payload))
req := header.ICMPv4(icmpMsg)
req.SetType(header.ICMPv4Echo)
req.SetIdent(0x1234)
req.SetSequence(7)
copy(req.Payload(), payload)
req.SetChecksum(header.ICMPv4Checksum(req[:header.ICMPv4MinimumSize], checksum.Checksum(payload, 0)))
pkt := make([]byte, header.IPv4MinimumSize+len(icmpMsg))
ip := header.IPv4(pkt)
ip.Encode(&header.IPv4Fields{
TotalLength: uint16(len(pkt)),
TTL: 64,
Protocol: uint8(header.ICMPv4ProtocolNumber),
SrcAddr: src,
DstAddr: dst,
})
ip.SetChecksum(^ip.CalculateChecksum())
copy(pkt[header.IPv4MinimumSize:], icmpMsg)
got := startReader(dev)
if _, err := dev.Write([][]byte{pkt}, 0); err != nil {
t.Fatal(err)
}
reply := header.IPv4(awaitPacket(t, got))
if !reply.IsValid(len(reply)) {
t.Fatal("invalid ipv4 reply")
}
if reply.SourceAddress() != dst || reply.DestinationAddress() != src {
t.Fatalf("reply addresses %v -> %v, want %v -> %v", reply.SourceAddress(), reply.DestinationAddress(), dst, src)
}
if reply.TransportProtocol() != header.ICMPv4ProtocolNumber {
t.Fatalf("reply protocol %v, want icmpv4", reply.TransportProtocol())
}
echo := header.ICMPv4(reply.Payload())
if echo.Type() != header.ICMPv4EchoReply {
t.Fatalf("reply type %v, want echo reply", echo.Type())
}
if echo.Ident() != 0x1234 || echo.Sequence() != 7 {
t.Fatalf("reply ident/seq %#x/%d, want 0x1234/7", echo.Ident(), echo.Sequence())
}
if !bytes.Equal(echo.Payload(), payload) {
t.Fatalf("reply payload %q, want %q", echo.Payload(), payload)
}
if checksum.Checksum(echo, 0) != 0xffff {
t.Fatal("bad icmpv4 checksum")
}
}
func TestICMPv6EchoReply(t *testing.T) {
dev := newICMPTestStack(t)
src := tcpip.AddrFrom16([16]byte{0xfd, 15: 2})
dst := tcpip.AddrFrom16([16]byte{0x26, 0x06, 0x47, 0x00, 0x47, 0x00, 15: 0x11})
payload := []byte("xray wireguard ping6")
icmpMsg := make([]byte, header.ICMPv6MinimumSize+len(payload))
req := header.ICMPv6(icmpMsg)
req.SetType(header.ICMPv6EchoRequest)
req.SetIdent(0x4321)
req.SetSequence(9)
copy(req.Payload(), payload)
req.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: req[:header.ICMPv6MinimumSize],
Src: src,
Dst: dst,
PayloadCsum: checksum.Checksum(payload, 0),
PayloadLen: len(payload),
}))
pkt := make([]byte, header.IPv6MinimumSize+len(icmpMsg))
ip := header.IPv6(pkt)
ip.Encode(&header.IPv6Fields{
PayloadLength: uint16(len(icmpMsg)),
TransportProtocol: header.ICMPv6ProtocolNumber,
HopLimit: 64,
SrcAddr: src,
DstAddr: dst,
})
copy(pkt[header.IPv6MinimumSize:], icmpMsg)
got := startReader(dev)
if _, err := dev.Write([][]byte{pkt}, 0); err != nil {
t.Fatal(err)
}
reply := header.IPv6(awaitPacket(t, got))
if !reply.IsValid(len(reply)) {
t.Fatal("invalid ipv6 reply")
}
if reply.SourceAddress() != dst || reply.DestinationAddress() != src {
t.Fatalf("reply addresses %v -> %v, want %v -> %v", reply.SourceAddress(), reply.DestinationAddress(), dst, src)
}
echo := header.ICMPv6(reply.Payload())
if echo.Type() != header.ICMPv6EchoReply {
t.Fatalf("reply type %v, want echo reply", echo.Type())
}
if echo.Ident() != 0x4321 || echo.Sequence() != 9 {
t.Fatalf("reply ident/seq %#x/%d, want 0x4321/9", echo.Ident(), echo.Sequence())
}
if !bytes.Equal(echo.Payload(), payload) {
t.Fatalf("reply payload %q, want %q", echo.Payload(), payload)
}
zeroed := header.ICMPv6(append([]byte(nil), echo[:header.ICMPv6MinimumSize]...))
zeroed.SetChecksum(0)
want := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: zeroed,
Src: dst,
Dst: src,
PayloadCsum: checksum.Checksum(echo.Payload(), 0),
PayloadLen: len(echo.Payload()),
})
if echo.Checksum() != want {
t.Fatalf("icmpv6 checksum %#x, want %#x", echo.Checksum(), want)
}
}
+146 -82
View File
@@ -10,6 +10,7 @@ import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
@@ -17,63 +18,101 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/xtls/xray-core/common/buf"
xnet "github.com/xtls/xray-core/common/net"
xtun "github.com/xtls/xray-core/proxy/tun"
"golang.zx2c4.com/wireguard/tun"
"golang.org/x/net/dns/dnsmessage"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
// netTunQueue is how many packets of the stack can wait to be read from its device
const netTunQueue = 1024
// netTun is the device of a TCP/IP stack of Xray: what is written to it goes to the stack,
// and what the stack sends is read from it
type netTun struct {
stack xtun.Stack
events chan tun.Event
// packets are what the stack has sent and Read has not taken yet
packets chan *buf.Buffer
closed chan struct{}
closeOnce sync.Once
mtu int
ep *channel.Endpoint
stack *stack.Stack
events chan tun.Event
notifyHandle *channel.NotificationHandle
incomingPacket chan *buffer.View
closed chan struct{}
mtu int
dnsServers []netip.Addr
hasV4, hasV6 bool
}
// CreateNetTUN returns a device with a TCP/IP stack on it, and what opens connections through that from
// localAddresses. With a handler the stack takes every connection that comes from the device, for any address.
// Without one it takes none
func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handler xtun.ConnectionHandler) (tun.Device, *Net, error) {
func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handleLocal bool) (tun.Device, *Net, *stack.Stack, error) {
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
HandleLocal: handleLocal,
}
dev := &netTun{
events: make(chan tun.Event, 10),
packets: make(chan *buf.Buffer, netTunQueue),
closed: make(chan struct{}),
mtu: mtu,
ep: channel.New(1024, uint32(mtu), ""),
stack: stack.New(opts),
events: make(chan tun.Event, 10),
incomingPacket: make(chan *buffer.View),
closed: make(chan struct{}),
dnsServers: dnsServers,
mtu: mtu,
}
stack, err := xtun.NewStack(context.Background(), xtun.StackOptions{Tun: dev, MTU: uint32(mtu), Addresses: localAddresses, Remote: true}, handler)
if err != nil {
return nil, nil, err
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default
tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt)
if tcpipErr != nil {
return nil, nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
dev.stack = stack
tnet := &Net{
DialContextTCPAddrPort: stack.DialTCP,
DialUDPAddrPort: dev.DialUDPAddrPort,
dnsServers: dnsServers,
dev.notifyHandle = dev.ep.AddNotify(dev)
tcpipErr = dev.stack.CreateNIC(1, dev.ep)
if tcpipErr != nil {
return nil, nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr)
}
for _, ip := range localAddresses {
var protoNumber tcpip.NetworkProtocolNumber
if ip.Is4() {
tnet.hasV4 = true
protoNumber = ipv4.ProtocolNumber
} else if ip.Is6() {
tnet.hasV6 = true
protoNumber = ipv6.ProtocolNumber
}
protoAddr := tcpip.ProtocolAddress{
Protocol: protoNumber,
AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
}
tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{})
if tcpipErr != nil {
return nil, nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr)
}
if ip.Is4() {
dev.hasV4 = true
} else if ip.Is6() {
dev.hasV6 = true
}
}
if dev.hasV4 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
}
if dev.hasV6 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
}
tnet := &Net{
DialContextTCPAddrPort: dev.DialContextTCPAddrPort,
DialUDPAddrPort: dev.DialUDPAddrPort,
dnsServers: dev.dnsServers,
hasV4: dev.hasV4,
hasV6: dev.hasV6,
}
dev.events <- tun.EventUp
return dev, tnet, nil
return dev, tnet, dev.stack, nil
}
func (tun *netTun) Name() (string, error) {
@@ -89,62 +128,73 @@ func (tun *netTun) Events() <-chan tun.Event {
}
func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
var view *buffer.View
select {
case packet := <-tun.packets:
sizes[0] = copy(buf[0][offset:], packet.Bytes())
packet.Release()
return 1, nil
case view = <-tun.incomingPacket:
case <-tun.closed:
return 0, os.ErrClosed
}
n, err := view.Read(buf[0][offset:])
view.Release()
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (tun *netTun) Write(buf [][]byte, offset int) (int, error) {
for _, buf := range buf {
tun.stack.Input(buf[offset:])
packet := buf[offset:]
if len(packet) == 0 {
continue
}
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
switch packet[0] >> 4 {
case 4:
tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
case 6:
tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
default:
return 0, syscall.EAFNOSUPPORT
}
}
return len(buf), nil
}
// WritePacket implements xtun.Tun. The packet is in the hands of the stack only until this returns, so it is copied
// for Read. The stack does not wait for the one who reads the device, unless that one is far behind: it answers
// what it is written inside Write, and whoever writes would wait with it and lose what comes meanwhile
func (tun *netTun) WritePacket(header, payload []byte) error {
packet := buf.NewWithSize(int32(len(header) + len(payload)))
packet.Write(header)
packet.Write(payload)
func (tun *netTun) WriteNotify() {
pkt := tun.ep.Read()
if pkt == nil {
return
}
view := pkt.ToView()
pkt.DecRef()
select {
case tun.packets <- packet:
return nil
case tun.incomingPacket <- view:
case <-tun.closed:
packet.Release()
return os.ErrClosed
view.Release()
}
}
// ReadPacket implements xtun.Tun. It is not called: the packets are handed to the stack by Write
func (tun *netTun) ReadPacket([]byte) ([]byte, error) {
<-tun.closed
return nil, os.ErrClosed
}
// Start implements xtun.Tun
func (tun *netTun) Start() error {
return nil
}
// Index implements xtun.Tun
func (tun *netTun) Index() (int, error) {
return 0, nil
}
func (tun *netTun) Close() error {
tun.closeOnce.Do(func() {
// before the stack ends its connections: nobody may be reading anymore what it writes for that
close(tun.closed)
tun.stack.Close()
tun.stack.RemoveNIC(1)
tun.stack.Close()
tun.ep.RemoveNotify(tun.notifyHandle)
tun.ep.Close()
if tun.events != nil {
close(tun.events)
})
}
// we don't close incomingPacket, because WriteNotify may be mid-send on it (DNS lookup) and would panic.
if tun.closed != nil {
close(tun.closed)
}
return nil
}
@@ -156,8 +206,17 @@ func (tun *netTun) BatchSize() int {
return 1
}
func (tun *netTun) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) {
fa, pn := convertToFullAddr(addr)
return gonet.DialContextTCP(ctx, tun.stack, fa, pn)
}
func (tun *netTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) {
conn, err := tun.stack.DialUDP()
var pn tcpip.NetworkProtocolNumber = ipv6.ProtocolNumber
if raddr.IsValid() || raddr.Port() > 0 {
_, pn = convertToFullAddr(raddr)
}
conn, err := gonet.DialUDP(tun.stack, nil, nil, pn)
if err != nil {
return nil, err
}
@@ -175,6 +234,20 @@ type Net struct {
cache cache
}
func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) {
var protoNumber tcpip.NetworkProtocolNumber
if endpoint.Addr().Is4() {
protoNumber = ipv4.ProtocolNumber
} else {
protoNumber = ipv6.ProtocolNumber
}
return tcpip.FullAddress{
NIC: 1,
Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()),
Port: endpoint.Port(),
}, protoNumber
}
var (
errNoSuchHost = errors.New("no such host")
errLameReferral = errors.New("lame referral")
@@ -378,8 +451,6 @@ func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Q
return dnsmessage.Parser{}, dnsmessage.Header{}, err
}
}
// what is opened through the stack of Xray takes no deadline, it ends when it is closed
stop := context.AfterFunc(ctx, func() { c.Close() })
var p dnsmessage.Parser
var h dnsmessage.Header
if useUDP {
@@ -387,15 +458,8 @@ func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Q
} else {
p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq)
}
stop()
c.Close()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
// as from a connection that takes a deadline
err = &net.OpError{Op: "read", Net: c.RemoteAddr().Network(), Err: os.ErrDeadlineExceeded}
} else if ctx.Err() != nil {
err = ctx.Err()
}
if err == context.Canceled {
err = errCanceled
} else if err == context.DeadlineExceeded {
-310
View File
@@ -1,310 +0,0 @@
package wireguard
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"errors"
"io"
gonet "net"
"net/netip"
"strings"
"testing"
"time"
"github.com/xtls/xray-core/common/net"
"golang.org/x/net/dns/dnsmessage"
"golang.zx2c4.com/wireguard/tun"
)
type closeHandler struct{}
func (closeHandler) HandleConnection(conn net.Conn, _ net.Destination) { conn.Close() }
// onesSum is the Internet checksum of b on top of initial, not inverted.
func onesSum(b []byte, initial uint32) uint16 {
sum := initial
for ; len(b) >= 2; b = b[2:] {
sum += uint32(binary.BigEndian.Uint16(b))
}
if len(b) == 1 {
sum += uint32(b[0]) << 8
}
for sum>>16 != 0 {
sum = sum>>16 + sum&0xffff
}
return uint16(sum)
}
// The inbound answers ICMP echo requests from peers locally, the way the TUN
// inbound does: ICMP is not proxied, but ping and connectivity checks through
// the tunnel get a reply instead of timing out. The request goes in through
// Write and the reply comes out of Read, as the WireGuard device uses them.
func TestNetTUNEchoReply(t *testing.T) {
for _, test := range []struct {
src, dst string
request byte
reply byte
}{
{"10.66.0.2", "1.1.1.1", 8, 0},
{"fd00::2", "2606:4700:4700::1111", 128, 129},
} {
dev, _, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.1"), netip.MustParseAddr("fd00::1")}, nil, 1420, closeHandler{})
if err != nil {
t.Fatal(err)
}
src, dst := netip.MustParseAddr(test.src), netip.MustParseAddr(test.dst)
payload := []byte("xray wireguard ping")
message := append([]byte{test.request, 0, 0, 0, 0x12, 0x34, 0, 7}, payload...)
// The stack drops what has a wrong checksum, as what comes through a tunnel has come a long way.
var pkt []byte
if src.Is4() {
pkt = make([]byte, 20, 20+len(message))
pkt[0] = 0x45
binary.BigEndian.PutUint16(pkt[2:], uint16(20+len(message)))
pkt[8], pkt[9] = 64, 1
copy(pkt[12:], src.AsSlice())
copy(pkt[16:], dst.AsSlice())
binary.BigEndian.PutUint16(pkt[10:], ^onesSum(pkt, 0))
binary.BigEndian.PutUint16(message[2:], ^onesSum(message, 0))
} else {
pkt = make([]byte, 40, 40+len(message))
pkt[0] = 0x60
binary.BigEndian.PutUint16(pkt[4:], uint16(len(message)))
pkt[6], pkt[7] = 58, 64
copy(pkt[8:], src.AsSlice())
copy(pkt[24:], dst.AsSlice())
binary.BigEndian.PutUint16(message[2:], ^onesSum(message, uint32(onesSum(pkt[8:40], uint32(len(message))+58))))
}
pkt = append(pkt, message...)
damaged := bytes.Clone(pkt)
damaged[len(damaged)-1] ^= 1
// The stack answers inside Write, the reply waits for whoever reads the device.
got := make(chan []byte, 1)
const offset = 16
go func() {
b := make([]byte, 2048)
sizes := make([]int, 1)
if _, err := dev.Read([][]byte{b}, sizes, offset); err == nil {
got <- b[offset : offset+sizes[0]]
}
}()
if _, err := dev.Write([][]byte{append(make([]byte, offset), damaged...), append(make([]byte, offset), pkt...)}, offset); err != nil {
t.Fatal(err)
}
var reply []byte
select {
case reply = <-got:
case <-time.After(2 * time.Second):
t.Fatal("no echo reply from the stack")
}
header := len(pkt) - len(message)
if len(reply) != len(pkt) || reply[0] != pkt[0] {
t.Fatalf("reply of %d bytes, version byte %#x", len(reply), reply[0])
}
var replySrc, replyDst netip.Addr
pseudo := uint32(0)
if src.Is4() {
replySrc, replyDst = netip.AddrFrom4([4]byte(reply[12:16])), netip.AddrFrom4([4]byte(reply[16:20]))
if onesSum(reply[:20], 0) != 0xffff || reply[9] != 1 {
t.Fatal("bad ipv4 header in the reply")
}
} else {
replySrc, replyDst = netip.AddrFrom16([16]byte(reply[8:24])), netip.AddrFrom16([16]byte(reply[24:40]))
if reply[6] != 58 {
t.Fatalf("reply protocol %d, want icmpv6", reply[6])
}
pseudo = uint32(onesSum(reply[8:40], uint32(len(message))+58))
}
if replySrc != dst || replyDst != src {
t.Fatalf("reply addresses %v -> %v, want %v -> %v", replySrc, replyDst, dst, src)
}
echo := reply[header:]
if echo[0] != test.reply || echo[1] != 0 {
t.Fatalf("reply type %d code %d, want echo reply", echo[0], echo[1])
}
// A reply to the damaged request, which came first, would not have this payload.
if !bytes.Equal(echo[4:], message[4:]) {
t.Fatalf("reply ident, seq and payload %x, want %x", echo[4:], message[4:])
}
if onesSum(echo, pseudo) != 0xffff {
t.Fatal("bad icmp checksum in the reply")
}
dev.Close()
dev.Close()
if _, err := dev.Read([][]byte{make([]byte, 2048)}, make([]int, 1), 0); err == nil {
t.Fatal("read from a closed device")
}
}
}
// testServer answers what comes through the device of a server: names over UDP port 53, except those that
// start with "silent", and everything else with what it was sent.
type testServer struct{}
func (testServer) HandleConnection(conn net.Conn, dest net.Destination) {
defer conn.Close()
b := make([]byte, 4096)
for {
n, err := conn.Read(b)
if err != nil {
return
}
answer := b[:n]
if dest.Network == net.Network_UDP && dest.Port == 53 {
var parser dnsmessage.Parser
header, err := parser.Start(b[:n])
if err != nil {
continue
}
question, err := parser.Question()
if err != nil || strings.HasPrefix(question.Name.String(), "silent") {
continue
}
builder := dnsmessage.NewBuilder(nil, dnsmessage.Header{ID: header.ID, Response: true, RecursionAvailable: true})
builder.StartQuestions()
builder.Question(question)
builder.StartAnswers()
builder.AResource(dnsmessage.ResourceHeader{Name: question.Name, Class: dnsmessage.ClassINET, TTL: 60}, dnsmessage.AResource{A: [4]byte{192, 0, 2, 1}})
if answer, err = builder.Finish(); err != nil {
continue
}
}
if _, err := conn.Write(answer); err != nil {
return
}
}
}
// A client and a server, each with a stack on its device and the two devices joined as a tunnel joins them:
// names are resolved, and TCP and UDP go through, as the outbound and the inbound use them.
func TestNetTUNClientAndServer(t *testing.T) {
server, _, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.1")}, nil, 1420, testServer{})
if err != nil {
t.Fatal(err)
}
client, tnet, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.2")}, []netip.Addr{netip.MustParseAddr("10.66.0.53")}, 1420, nil)
if err != nil {
t.Fatal(err)
}
defer server.Close()
defer client.Close()
// As in a tunnel, what reads a device does not wait for the other end to take the packet: a stack answers
// inside Write, and its answer has to be read meanwhile.
for _, pair := range [][2]tun.Device{{client, server}, {server, client}} {
packets := make(chan []byte, 1024)
go func() {
defer close(packets)
bufs, sizes := [][]byte{make([]byte, 2048)}, make([]int, 1)
for {
if _, err := pair[0].Read(bufs, sizes, 0); err != nil {
return
}
select {
case packets <- bytes.Clone(bufs[0][:sizes[0]]):
default:
}
}
}()
go func() {
for packet := range packets {
pair[1].Write([][]byte{packet}, 0)
}
}()
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if addrs, err := tnet.LookupContextHost(ctx, "xray.example"); err != nil || len(addrs) != 1 || addrs[0] != "192.0.2.1" {
t.Fatal("resolved to ", addrs, ", error: ", err)
}
// a server that does not answer is not waited for longer than the one who asks wants to wait
short, cancelShort := context.WithTimeout(ctx, 200*time.Millisecond)
start := time.Now()
var timeout *gonet.DNSError
if addrs, err := tnet.LookupContextHost(short, "silent.example"); !errors.As(err, &timeout) || !timeout.IsTimeout || time.Since(start) > 3*time.Second {
t.Fatal("resolved to ", addrs, " after ", time.Since(start), ", error: ", err)
}
cancelShort()
sent := make([]byte, 1<<20)
rand.Read(sent)
for _, network := range []string{"tcp", "udp"} {
conn, err := tnet.DialContext(ctx, network, "xray.example:7")
if err != nil {
t.Fatal(err)
}
size := len(sent)
if network == "udp" {
// more than the tunnel takes in one packet
size = 3000
}
go conn.Write(sent[:size])
received := make([]byte, size)
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
done := make(chan error, 1)
go func() {
_, err := io.ReadFull(conn, received)
done <- err
}()
select {
case err := <-done:
if err != nil || !bytes.Equal(received, sent[:size]) {
t.Fatal(network, ": received something else than what was sent, error: ", err)
}
case <-ctx.Done():
t.Fatal(network, ": what was sent did not come back")
}
conn.Close()
}
}
// A packet of the stack is in its hands only until WritePacket returns, Read gets it as it was then.
// A device that is closed is not waited for, also not by a stack that has something to send when it is closed.
func TestNetTUNWritePacket(t *testing.T) {
dev, tnet, err := CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.66.0.2")}, nil, 1420, nil)
if err != nil {
t.Fatal(err)
}
b, sizes := make([]byte, 16), make([]int, 1)
for i := range 1000 {
header, payload := []byte{byte(i), 1, 2, 3}, []byte{4, 5, 6, byte(i)}
go func() {
dev.(*netTun).WritePacket(header, payload)
clear(header)
clear(payload)
}()
if _, err := dev.Read([][]byte{b}, sizes, 0); err != nil || !bytes.Equal(b[:sizes[0]], []byte{byte(i), 1, 2, 3, 4, 5, 6, byte(i)}) {
t.Fatal("read ", b[:sizes[0]], " of packet ", i, ", error: ", err)
}
}
// nobody reads now, and no more can wait to be read: a connection waits to send its SYN
for range netTunQueue {
dev.(*netTun).WritePacket([]byte{1}, nil)
}
dialed := make(chan error, 1)
go func() {
_, err := tnet.DialContextTCPAddrPort(context.Background(), netip.MustParseAddrPort("192.0.2.1:80"))
dialed <- err
}()
time.Sleep(50 * time.Millisecond)
closed := make(chan struct{})
go func() {
dev.Close()
close(closed)
}()
select {
case <-closed:
case <-time.After(5 * time.Second):
t.Fatal("the device is not closed while a packet waits to be read")
}
if err := <-dialed; err == nil {
t.Fatal("a connection through a closed device")
}
}
@@ -1,18 +1,16 @@
package tun
package wireguard
import (
"runtime"
"testing"
"github.com/xtls/xray-core/common/buf"
)
const benchBatch = 64
// Raw cost of queueing and draining a small burst, as one flow's reader does.
func BenchmarkQueueBurstChan(b *testing.B) {
ch := make(chan *buf.Buffer, udpQueueLimit)
p := &buf.Buffer{}
ch := make(chan *packet, udpQueueLimit)
p := &packet{}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for j := 0; j < benchBatch; j++ {
@@ -26,7 +24,7 @@ func BenchmarkQueueBurstChan(b *testing.B) {
func BenchmarkQueueBurstPacketQueue(b *testing.B) {
q := newPacketQueue(udpQueueLimit)
p := &buf.Buffer{}
p := &packet{}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for j := 0; j < benchBatch; j++ {
@@ -41,8 +39,8 @@ func BenchmarkQueueBurstPacketQueue(b *testing.B) {
// Producer and consumer on different goroutines; the producer yields when the
// queue is full instead of spinning, like a blocking channel send would.
func BenchmarkQueueStreamChan(b *testing.B) {
ch := make(chan *buf.Buffer, udpQueueLimit)
p := &buf.Buffer{}
ch := make(chan *packet, udpQueueLimit)
p := &packet{}
done := make(chan struct{})
go func() {
for range ch {
@@ -60,7 +58,7 @@ func BenchmarkQueueStreamChan(b *testing.B) {
func BenchmarkQueueStreamPacketQueue(b *testing.B) {
q := newPacketQueue(udpQueueLimit)
p := &buf.Buffer{}
p := &packet{}
done := make(chan struct{})
go func() {
for {
+18 -16
View File
@@ -21,13 +21,13 @@ import (
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
xtun "github.com/xtls/xray-core/proxy/tun"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"golang.org/x/crypto/curve25519"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
type Server struct {
@@ -43,9 +43,10 @@ type Server struct {
uplinkCounter stats.Counter
downlinkCounter stats.Counter
tun tun.Device
dev *device.Device
mu sync.Mutex
tun tun.Device
stack *stack.Stack
dev *device.Device
mu sync.Mutex
pub [32]byte
users *sync.Map
@@ -94,6 +95,11 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
return nil, err
}
tun, _, stack, err := CreateNetTUN(localAddresses, nil, int(conf.Mtu), false)
if err != nil {
return nil, err
}
pri := common.Must2(ParseKey(conf.SecretKey))
var pub [32]byte
curve25519.ScalarBaseMult(&pub, pri)
@@ -120,15 +126,15 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
tun: tun,
stack: stack,
pub: pub,
users: users,
}
// The stack hands its connections to s from the first packet on, which the device delivers after Start -> dev.Up.
tun, _, err := CreateNetTUN(localAddresses, nil, int(conf.Mtu), s)
if err != nil {
return nil, err
}
s.tun = tun
// Install the stack's protocol handlers before the device can deliver packets to it (Start -> dev.Up).
CreateForwarder(stack, s.HandleConnection)
CreateICMPEchoResponder(stack)
return s, nil
}
@@ -255,7 +261,7 @@ func (s *Server) Start() error {
if s.src.Address.Family().IsDomain() {
return errors.New("address is domain")
}
listenFunc := func(context.Context) (net.PacketConn, error) {
listenFunc := func() (net.PacketConn, error) {
var pktConn net.PacketConn
var err error
if s.streamSettings.FinalMask != nil {
@@ -373,13 +379,9 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
})
errors.LogInfo(ctx, "processing from ", source, " to ", dest)
writer := buf.NewWriter(conn)
if dest.Network == net.Network_TCP {
writer = &xtun.TCPWriter{Writer: writer}
}
link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: writer,
Writer: buf.NewWriter(conn),
}
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err))
-71
View File
@@ -1,71 +0,0 @@
package wireguard
import (
"context"
"io"
"net/netip"
"sync"
"sync/atomic"
"testing"
"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/features/routing"
"github.com/xtls/xray-core/transport"
)
// stackConn is a TCP connection as the stack hands it over: it is its own buf.Writer.
type stackConn struct {
net.Conn
closed atomic.Int32
}
func (c *stackConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c *stackConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
buf.ReleaseMulti(mb)
return nil
}
func (c *stackConn) Close() error {
c.closed.Add(1)
return nil
}
func (c *stackConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IP{10, 0, 0, 2}, Port: 1234}
}
// closingDispatcher does what an outbound does when the other side has no more to send.
type closingDispatcher struct {
routing.Dispatcher
conn *stackConn
closed int32
}
func (d *closingDispatcher) DispatchLink(_ context.Context, _ net.Destination, link *transport.Link) error {
common.Close(link.Writer)
d.closed = d.conn.closed.Load()
return nil
}
func TestServerKeepsConnectionOpenWhenWriterCloses(t *testing.T) {
conn := &stackConn{}
dispatcher := &closingDispatcher{conn: conn}
users := &sync.Map{}
users.Store([32]byte{1}, &protocol.MemoryUser{
Email: "love@xray.com",
Account: &MemoryAccount{AllowedIPs: []netip.Prefix{netip.MustParsePrefix("10.0.0.2/32")}},
})
s := &Server{ctx: context.Background(), dispatcher: dispatcher, users: users}
s.HandleConnection(conn, net.TCPDestination(net.ParseAddress("192.0.2.1"), 443))
if dispatcher.closed != 0 {
t.Fatal("the connection was closed with its writer, while the peer may still be sending")
}
if got := conn.closed.Load(); got != 1 {
t.Fatalf("connection closed %d times, want 1", got)
}
}
+346
View File
@@ -1,12 +1,27 @@
package wireguard
import (
"context"
"fmt"
"io"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
)
func CalculateInterfaceName(name string) (tunName string) {
@@ -33,3 +48,334 @@ func CalculateInterfaceName(name string) (tunName string) {
tunName = fmt.Sprintf("%s%d", tunName, tunIndex)
return
}
func CreateForwarder(gstack *stack.Stack, handler func(conn net.Conn, dest net.Destination)) {
gstack.SetPromiscuousMode(1, true)
gstack.SetSpoofing(1, true)
tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) {
go func(r *tcp.ForwarderRequest) {
var wq waiter.Queue
id := r.ID()
ep, err := r.CreateEndpoint(&wq)
if err != nil {
errors.LogError(context.Background(), err.String())
r.Complete(true)
return
}
options := ep.SocketOptions()
options.SetKeepAlive(false)
options.SetReuseAddress(true)
options.SetReusePort(true)
handler(gonet.NewTCPConn(&wq, ep), net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)))
ep.Close()
r.Complete(false)
}(r)
})
gstack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
manager := &udpManager{
stack: gstack,
handler: handler,
m: make(map[string]*udpConn),
}
gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
data := pkt.Data().AsRange().ToSlice()
// if len(data) == 0 {
// return false
// }
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
if srcIP == nil || dstIP == nil {
panic(id)
}
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
manager.feed(src, dst, data)
return true
})
}
type udpManager struct {
stack *stack.Stack
handler func(conn net.Conn, dest net.Destination)
m map[string]*udpConn
mutex sync.RWMutex
}
func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte) {
m.mutex.RLock()
uc, ok := m.m[src.NetAddr()]
if ok {
if !uc.queue.push(&packet{p: data, dest: &dst}) {
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full")
}
m.mutex.RUnlock()
return
}
m.mutex.RUnlock()
m.mutex.Lock()
defer m.mutex.Unlock()
uc, ok = m.m[src.NetAddr()]
if !ok {
uc = &udpConn{
queue: newPacketQueue(udpQueueLimit),
src: src,
dst: dst,
}
uc.writeFunc = m.writeRawUDPPacket
uc.closeFunc = func() {
m.mutex.Lock()
m.close(uc)
m.mutex.Unlock()
}
m.m[src.NetAddr()] = uc
go m.handler(uc, dst)
}
if !uc.queue.push(&packet{p: data, dest: &dst}) {
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full 2")
}
}
func (m *udpManager) close(uc *udpConn) {
if !uc.closed {
uc.closed = true
uc.queue.close()
delete(m.m, uc.src.NetAddr())
}
}
func (m *udpManager) writeRawUDPPacket(payload []byte, src net.Destination, dst net.Destination) error {
udpLen := header.UDPMinimumSize + len(payload)
srcIP := tcpip.AddrFromSlice(src.Address.IP())
dstIP := tcpip.AddrFromSlice(dst.Address.IP())
// build packet with appropriate IP header size
isIPv4 := dst.Address.Family().IsIPv4()
ipHdrSize := header.IPv6MinimumSize
ipProtocol := header.IPv6ProtocolNumber
if isIPv4 {
ipHdrSize = header.IPv4MinimumSize
ipProtocol = header.IPv4ProtocolNumber
}
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize,
Payload: buffer.MakeWithData(payload),
})
defer pkt.DecRef()
// Build UDP header
udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
udpHdr.Encode(&header.UDPFields{
SrcPort: uint16(src.Port),
DstPort: uint16(dst.Port),
Length: uint16(udpLen),
})
// Calculate and set UDP checksum
xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen))
udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum)))
// Build IP header
if isIPv4 {
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
ipHdr.Encode(&header.IPv4Fields{
TotalLength: uint16(header.IPv4MinimumSize + udpLen),
TTL: 64,
Protocol: uint8(header.UDPProtocolNumber),
SrcAddr: srcIP,
DstAddr: dstIP,
})
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
} else {
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
ipHdr.Encode(&header.IPv6Fields{
PayloadLength: uint16(udpLen),
TransportProtocol: header.UDPProtocolNumber,
HopLimit: 64,
SrcAddr: srcIP,
DstAddr: dstIP,
})
}
// dispatch the packet
err := m.stack.WriteRawPacket(1, ipProtocol, buffer.MakeWithView(pkt.ToView()))
if err != nil {
return errors.New("failed to write raw udp packet back to stack err ", err)
}
return nil
}
type packet struct {
p []byte
dest *net.Destination
}
type udpConn struct {
queue *packetQueue
src net.Destination
dst net.Destination
writeFunc func(payload []byte, src net.Destination, dst net.Destination) error
closeFunc func()
closed bool
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
for {
q, ok := c.queue.pop()
if !ok {
return nil, io.EOF
}
b := buf.New()
if _, err := b.Write(q.p); err != nil {
errors.LogErrorInner(context.Background(), err, "drop packet to ", q.dest, " with size ", len(q.p))
b.Release()
continue
}
b.UDP = q.dest
return buf.MultiBuffer{b}, nil
}
}
func (c *udpConn) Read(p []byte) (int, error) {
q, ok := c.queue.pop()
if !ok {
return 0, io.EOF
}
n := copy(p, q.p)
if n != len(q.p) {
return 0, io.ErrShortBuffer
}
return n, nil
}
func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
for i, b := range mb {
dst := c.dst
if b.UDP != nil {
if b.UDP.Address.Family().IsDomain() {
errors.LogError(context.Background(), "impossible domain packet ", b.UDP, " reply via original target ", dst)
} else {
dst = *b.UDP
}
}
err := c.writeFunc(b.Bytes(), dst, c.src)
if err != nil {
buf.ReleaseMulti(mb[i:])
return err
}
b.Release()
}
return nil
}
func (c *udpConn) Write(p []byte) (int, error) {
err := c.writeFunc(p, c.dst, c.src)
if err != nil {
return 0, err
}
return len(p), nil
}
func (c *udpConn) Close() error {
c.closeFunc()
return nil
}
func (c *udpConn) LocalAddr() net.Addr {
return c.dst.RawNetAddr()
}
func (c *udpConn) RemoteAddr() net.Addr {
return c.src.RawNetAddr()
}
func (c *udpConn) SetDeadline(t time.Time) error {
return nil
}
func (c *udpConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *udpConn) SetWriteDeadline(t time.Time) error {
return nil
}
// udpQueueLimit bounds the packets waiting for one UDP flow; more are dropped.
const udpQueueLimit = 1024
// packetQueue holds the packets waiting for one UDP flow. Unlike a buffered
// channel of the same bound it only allocates for packets actually queued, so
// the many idle flows kept until the idle timeout cost next to nothing.
type packetQueue struct {
mu sync.Mutex
items []*packet
limit int
notify chan struct{}
closed bool
}
func newPacketQueue(limit int) *packetQueue {
return &packetQueue{limit: limit, notify: make(chan struct{}, 1)}
}
// push queues p and reports whether it was accepted.
func (q *packetQueue) push(p *packet) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed || len(q.items) >= q.limit {
return false
}
q.items = append(q.items, p)
select {
case q.notify <- struct{}{}:
default:
}
return true
}
// pop blocks until a packet is queued or the queue is closed and drained.
func (q *packetQueue) pop() (*packet, bool) {
for {
q.mu.Lock()
if len(q.items) > 0 {
p := q.items[0]
q.items[0] = nil
q.items = q.items[1:]
if len(q.items) == 0 {
q.items = nil
}
q.mu.Unlock()
return p, true
}
if q.closed {
q.mu.Unlock()
return nil, false
}
q.mu.Unlock()
<-q.notify
}
}
func (q *packetQueue) close() {
q.mu.Lock()
defer q.mu.Unlock()
if !q.closed {
q.closed = true
close(q.notify)
}
}
@@ -1,48 +1,48 @@
package tun
package wireguard
import (
"testing"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net"
)
// BenchmarkUDPNewSession measures what one new UDP flow costs the
// BenchmarkUDPManagerNewSession measures what one new UDP flow costs the
// inbound while it stays open: QUIC and DNS open many short flows, and each
// one lives until the connection idle timeout.
func BenchmarkUDPNewSession(b *testing.B) {
u := newUdpConnectionHandler(func(conn net.Conn, dest net.Destination) {}, nil)
func BenchmarkUDPManagerNewSession(b *testing.B) {
m := &udpManager{
handler: func(conn net.Conn, dest net.Destination) {},
m: make(map[string]*udpConn),
}
dst := net.UDPDestination(net.ParseAddress("1.1.1.1"), 443)
payload := make([]byte, 1200)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
src := net.UDPDestination(net.IPAddress([]byte{10, byte(i >> 16), byte(i >> 8), byte(i)}), net.Port(1024+i%60000))
u.HandlePacket(src, dst, &buf.Buffer{})
m.feed(src, dst, payload)
}
}
func TestPacketQueueOrderAndClose(t *testing.T) {
q := newPacketQueue(udpQueueLimit)
for i := 0; i < 3; i++ {
b := buf.New()
b.WriteByte(byte(i))
if !q.push(b) {
if !q.push(&packet{p: []byte{byte(i)}}) {
t.Fatalf("push %d rejected", i)
}
}
for i := 0; i < 3; i++ {
b, ok := q.pop()
if !ok || b.Byte(0) != byte(i) {
t.Fatalf("pop %d: got %v, %v", i, b, ok)
p, ok := q.pop()
if !ok || p.p[0] != byte(i) {
t.Fatalf("pop %d: got %v, %v", i, p, ok)
}
b.Release()
}
q.close()
if _, ok := q.pop(); ok {
t.Fatal("pop after close returned a packet")
}
if q.push(&buf.Buffer{}) {
if q.push(&packet{}) {
t.Fatal("push after close accepted")
}
}
@@ -50,11 +50,11 @@ func TestPacketQueueOrderAndClose(t *testing.T) {
func TestPacketQueueLimit(t *testing.T) {
q := newPacketQueue(udpQueueLimit)
for i := 0; i < udpQueueLimit; i++ {
if !q.push(&buf.Buffer{}) {
if !q.push(&packet{}) {
t.Fatalf("push %d rejected below the limit", i)
}
}
if q.push(&buf.Buffer{}) {
if q.push(&packet{}) {
t.Fatal("push above the limit accepted")
}
}
@@ -80,7 +80,7 @@ func TestPacketQueueCloseUnblocksReader(t *testing.T) {
func TestPacketQueueDropsDrainedStorage(t *testing.T) {
q := newPacketQueue(udpQueueLimit)
for i := 0; i < 100; i++ {
q.push(&buf.Buffer{})
q.push(&packet{})
}
for i := 0; i < 100; i++ {
q.pop()
+48 -21
View File
@@ -25,6 +25,10 @@ import (
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"golang.org/x/sync/errgroup"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"github.com/xtls/xray-core/app/log"
"github.com/xtls/xray-core/app/proxyman"
@@ -60,31 +64,54 @@ const (
masqueAuthorization = "Basic dUBleGFtcGxlLmNvbTpw"
)
// masqueEcho answers what comes to the echo port of the server, TCP and UDP alike
type masqueEcho struct{}
func (masqueEcho) HandleConnection(conn net.Conn, dest net.Destination) {
defer conn.Close()
if dest.Port != masqueEchoPort {
return
}
b := make([]byte, 2048)
for {
n, err := conn.Read(b)
if err != nil {
return
}
if _, err := conn.Write(xor(b[:n])); err != nil {
return
}
}
}
func startMasqueServer(t *testing.T, h2 bool) (net.Port, [32]byte) {
dev, _, err := wireguard.CreateNetTUN([]netip.Addr{masqueServerV4, masqueServerV6}, nil, transmasque.MinPacketSize, masqueEcho{})
dev, _, gstack, err := wireguard.CreateNetTUN([]netip.Addr{masqueServerV4, masqueServerV6}, nil, transmasque.MinPacketSize, false)
common.Must(err)
t.Cleanup(func() { dev.Close() })
for _, addr := range []netip.Addr{masqueServerV4, masqueServerV6} {
proto := ipv4.ProtocolNumber
if addr.Is6() {
proto = ipv6.ProtocolNumber
}
local := tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(addr.AsSlice()), Port: masqueEchoPort}
l, err := gonet.ListenTCP(gstack, local, proto)
common.Must(err)
go func() {
for {
c, err := l.Accept()
if err != nil {
return
}
go func() {
defer c.Close()
b := make([]byte, 2048)
for {
n, err := c.Read(b)
if err != nil {
return
}
if _, err := c.Write(xor(b[:n])); err != nil {
return
}
}
}()
}
}()
u, err := gonet.DialUDP(gstack, &local, nil, proto)
common.Must(err)
go func() {
b := make([]byte, 2048)
for {
n, addr, err := u.ReadFrom(b)
if err != nil {
return
}
u.WriteTo(xor(b[:n]), addr)
}
}()
}
var current atomic.Pointer[connectip.Conn]
go func() {
bufs := [][]byte{make([]byte, transmasque.MinPacketSize)}
+43 -86
View File
@@ -1,17 +1,14 @@
package scenarios
import (
"encoding/hex"
"testing"
"time"
//"time"
"github.com/xtls/xray-core/app/log"
"github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common"
clog "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
@@ -20,18 +17,9 @@ import (
"github.com/xtls/xray-core/proxy/wireguard"
"github.com/xtls/xray-core/testing/servers/tcp"
"github.com/xtls/xray-core/testing/servers/udp"
"golang.org/x/crypto/curve25519"
"golang.org/x/sync/errgroup"
//"golang.org/x/sync/errgroup"
)
func wireguardPublicKey(private string) string {
key, err := wireguard.ParseKey(private)
common.Must(err)
var public [32]byte
curve25519.ScalarBaseMult(&public, key)
return hex.EncodeToString(public[:])
}
func TestWireguard(t *testing.T) {
tcpServer := tcp.Server{
MsgProcessor: xor,
@@ -40,44 +28,18 @@ func TestWireguard(t *testing.T) {
common.Must(err)
defer tcpServer.Close()
udpServer := udp.Server{
MsgProcessor: xor,
}
udpDest, err := udpServer.Start()
common.Must(err)
defer udpServer.Close()
serverPrivate, _ := conf.ParseWireGuardKey("EGs4lTSJPmgELx6YiJAmPR2meWi6bY+e9rTdCipSj10=")
serverPublic, _ := conf.ParseWireGuardKey("osAMIyil18HeZXGGBDC9KpZoM+L2iGyXWVSYivuM9B0=")
clientPrivate, _ := conf.ParseWireGuardKey("CPQSpgxgdQRZa5SUbT3HLv+mmDVHLW5YR/rQlzum/2I=")
clientPublic, _ := conf.ParseWireGuardKey("MmLJ5iHFVVBp7VsB0hxfpQ0wEzAbT2KQnpQpj0+RtBw=")
// What the client asks for is somewhere else, the stack of the server takes nothing that is for this machine.
redirect := func(tag string, dest net.Destination) *core.OutboundHandlerConfig {
return &core.OutboundHandlerConfig{
Tag: tag,
ProxySettings: serial.ToTypedMessage(&freedom.Config{
DestinationOverride: &freedom.DestinationOverride{
Server: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(dest.Address),
Port: uint32(dest.Port),
},
},
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}
}
serverPort := udp.PickPort()
serverConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Warning,
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
}),
serial.ToTypedMessage(&router.Config{
Rule: []*router.RoutingRule{
{Networks: []net.Network{net.Network_TCP}, TargetTag: &router.RoutingRule_Tag{Tag: "tcp"}},
{Networks: []net.Network{net.Network_UDP}, TargetTag: &router.RoutingRule_Tag{Tag: "udp"}},
},
}),
},
Inbound: []*core.InboundHandlerConfig{
{
@@ -86,65 +48,60 @@ func TestWireguard(t *testing.T) {
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{
IsClient: false,
Endpoint: []string{"10.0.0.1", "fd00::1"},
Mtu: 1420,
SecretKey: serverPrivate,
Users: []*protocol.User{{
Email: "love@xray.com",
Account: serial.ToTypedMessage(&wireguard.PeerConfig{
PublicKey: wireguardPublicKey(clientPrivate),
AllowedIps: []string{"10.0.0.2/32", "fd00::2/128"},
}),
IsClient: false,
NoKernelTun: false,
Endpoint: []string{"10.0.0.1"},
Mtu: 1420,
SecretKey: serverPrivate,
Peers: []*wireguard.PeerConfig{{
PublicKey: serverPublic,
AllowedIps: []string{"0.0.0.0/0", "::0/0"},
}},
}),
},
},
Outbound: []*core.OutboundHandlerConfig{
redirect("tcp", dest),
redirect("udp", udpDest),
{
ProxySettings: serial.ToTypedMessage(&freedom.Config{
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{}),
},
},
}
clientPort := tcp.PickPort()
client6Port := tcp.PickPort()
clientUDPPort := udp.PickPort()
dokodemoInbound := func(port net.Port, dest net.Destination) *core.InboundHandlerConfig {
return &core.InboundHandlerConfig{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(port)}},
Listen: net.NewIPOrDomain(net.LocalHostIP),
}),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{dest.Network},
}),
}
}
clientConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Warning,
ErrorLogLevel: clog.Severity_Debug,
ErrorLogType: log.LogType_Console,
}),
},
Inbound: []*core.InboundHandlerConfig{
dokodemoInbound(clientPort, net.TCPDestination(net.ParseAddress("192.0.2.1"), 7)),
dokodemoInbound(client6Port, net.TCPDestination(net.ParseAddress("2001:db8::1"), 7)),
dokodemoInbound(clientUDPPort, net.UDPDestination(net.ParseAddress("192.0.2.1"), 7)),
{
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(&wireguard.DeviceConfig{
IsClient: true,
NoKernelTun: true,
Endpoint: []string{"10.0.0.2", "fd00::2"},
NoKernelTun: false,
Endpoint: []string{"10.0.0.2"},
Mtu: 1420,
SecretKey: clientPrivate,
Peers: []*wireguard.PeerConfig{{
Endpoint: "127.0.0.1:" + serverPort.String(),
PublicKey: wireguardPublicKey(serverPrivate),
PublicKey: clientPublic,
AllowedIps: []string{"0.0.0.0/0", "::0/0"},
}},
}),
@@ -157,13 +114,13 @@ func TestWireguard(t *testing.T) {
common.Must(err)
defer CloseAllServers(servers)
var errg errgroup.Group
for range 3 {
errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*20))
}
errg.Go(testTCPConn(client6Port, 1024*1024, time.Second*20))
errg.Go(testUDPConn(clientUDPPort, 1024, time.Second*5))
if err := errg.Wait(); err != nil {
t.Error(err)
}
// FIXME: for some reason wg server does not receive
// var errg errgroup.Group
// for i := 0; i < 1; i++ {
// errg.Go(testTCPConn(clientPort, 1024, time.Second*2))
// }
// if err := errg.Wait(); err != nil {
// t.Error(err)
// }
}
@@ -255,7 +255,6 @@ type Config struct {
ResetMin int64 `protobuf:"varint,1,opt,name=reset_min,json=resetMin,proto3" json:"reset_min,omitempty"`
ResetMax int64 `protobuf:"varint,2,opt,name=reset_max,json=resetMax,proto3" json:"reset_max,omitempty"`
Items []*Item `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
Script string `protobuf:"bytes,4,opt,name=script,proto3" json:"script,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -311,13 +310,6 @@ func (x *Config) GetItems() []*Item {
return nil
}
func (x *Config) GetScript() string {
if x != nil {
return x.Script
}
return ""
}
var File_transport_internet_finalmask_noise_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" +
@@ -345,12 +337,11 @@ const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" +
"\x06packet\x18\x05 \x01(\fR\x06packet\x12\x1b\n" +
"\tdelay_min\x18\x06 \x01(\x03R\bdelayMin\x12\x1b\n" +
"\tdelay_max\x18\a \x01(\x03R\bdelayMax\x12L\n" +
"\bsegments\x18\b \x03(\v20.xray.transport.internet.finalmask.noise.SegmentR\bsegments\"\x9f\x01\n" +
"\bsegments\x18\b \x03(\v20.xray.transport.internet.finalmask.noise.SegmentR\bsegments\"\x87\x01\n" +
"\x06Config\x12\x1b\n" +
"\treset_min\x18\x01 \x01(\x03R\bresetMin\x12\x1b\n" +
"\treset_max\x18\x02 \x01(\x03R\bresetMax\x12C\n" +
"\x05items\x18\x03 \x03(\v2-.xray.transport.internet.finalmask.noise.ItemR\x05items\x12\x16\n" +
"\x06script\x18\x04 \x01(\tR\x06scriptB\x97\x01\n" +
"\x05items\x18\x03 \x03(\v2-.xray.transport.internet.finalmask.noise.ItemR\x05itemsB\x97\x01\n" +
"+com.xray.transport.internet.finalmask.noiseP\x01Z<github.com/xtls/xray-core/transport/internet/finalmask/noise\xaa\x02'Xray.Transport.Internet.Finalmask.Noiseb\x06proto3"
var (
@@ -37,5 +37,4 @@ message Config {
int64 reset_min = 1;
int64 reset_max = 2;
repeated Item items = 3;
string script = 4;
}
+6 -35
View File
@@ -1,7 +1,6 @@
package noise
import (
"context"
"crypto/rand"
"encoding/binary"
"net"
@@ -11,7 +10,6 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
)
const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -19,27 +17,17 @@ const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
type noiseConn struct {
net.PacketConn
config *Config
script *scriptEngine
noise []scriptPacket
m map[string]time.Time
mu sync.Mutex
counter atomic.Uint32
}
func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
conn := &noiseConn{
return &noiseConn{
PacketConn: raw,
config: c,
m: make(map[string]time.Time),
}
if c.Script != "" {
engine, err := c.engine()
if err != nil {
return nil, errors.New("failed to initialize the noise script ", c.Script).Base(err)
}
conn.script = engine
}
return conn, nil
}, nil
}
func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
@@ -109,30 +97,13 @@ func (c *noiseConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
t := c.m[addr.String()]
if t.IsZero() || (c.config.ResetMax > 0 && time.Now().After(t)) {
c.sendNoise(addr)
for _, item := range c.config.Items {
c.PacketConn.WriteTo(c.buildPacket(item), addr)
time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond)
}
}
c.m[addr.String()] = time.Now().Add(time.Duration(crypto.RandBetween(c.config.ResetMin, c.config.ResetMax)) * time.Second)
return c.PacketConn.WriteTo(p, addr)
}
func (c *noiseConn) sendNoise(addr net.Addr) {
if c.script != nil {
packets, err := c.script.packets(addr.String(), c.noise[:0])
if err != nil {
errors.LogWarningInner(context.Background(), err, "the noise script produced no packets")
return
}
c.noise = packets
for _, packet := range packets {
c.PacketConn.WriteTo(packet.packet, addr)
time.Sleep(packet.delay)
}
return
}
for _, item := range c.config.Items {
c.PacketConn.WriteTo(c.buildPacket(item), addr)
time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond)
}
}
-148
View File
@@ -1,148 +0,0 @@
package noise
import (
"crypto/rand"
"time"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
xlua "github.com/xtls/xray-core/common/lua"
lua "github.com/yuin/gopher-lua"
)
const (
valueLimit = 65535
packetLimit = 64
)
const planKey = "xray.finalmask.noise.plan"
type noisePlan struct {
packets []scriptPacket
bytes int
open bool
}
func RegisterLua(L *lua.LState) {
L.PreloadModule("xray.finalmask.noise", func(L *lua.LState) int {
module := L.CreateTable(0, 2)
module.RawSetString("Random", L.NewFunction(func(L *lua.LState) int {
size := L.CheckNumber(1)
if !(size >= 0 && size <= valueLimit) {
L.RaiseError("noise.Random requires a size of 0 to %d", valueLimit)
return 0
}
buf := make([]byte, int(size))
if _, err := rand.Read(buf); err != nil {
L.RaiseError("noise.Random failed: %v", err)
return 0
}
xlua.PushString(L, string(buf))
return 1
}))
module.RawSetString("RandomBetween", L.NewFunction(func(L *lua.LState) int {
from, to := L.CheckNumber(1), L.CheckNumber(2)
if !(from >= 0 && from <= to && to <= valueLimit) {
L.RaiseError("noise.RandomBetween requires 0 <= from <= to <= %d", valueLimit)
return 0
}
xlua.PushNumber(L, crypto.RandBetween(int64(from), int64(to)+1))
return 1
}))
L.Push(module)
return 1
})
}
func registerPlan(L *lua.LState) {
meta := L.CreateTable(0, 3)
meta.RawSetString("__index", meta)
meta.RawSetString("Add", L.NewFunction(planAdd))
meta.RawSetString("AddRandom", L.NewFunction(planAddRandom))
data := L.NewUserData()
data.Value = &noisePlan{}
data.Metatable = meta
L.SetField(L.G.Registry, planKey, data)
}
func planAdd(L *lua.LState) int {
packet, ok := L.Get(2).(lua.LString)
if !ok {
L.RaiseError("Add requires a packet string")
return 0
}
plan := planFor(L, len(packet))
plan.packets = append(plan.packets, scriptPacket{packet: []byte(packet), delay: planDelay(L, 3)})
plan.bytes += len(packet)
return 0
}
func planAddRandom(L *lua.LState) int {
size := L.CheckNumber(2)
if !(size >= 0 && size <= valueLimit) {
L.RaiseError("AddRandom requires a size of 0 to %d", valueLimit)
return 0
}
plan := planFor(L, int(size))
packet := make([]byte, int(size))
if _, err := rand.Read(packet); err != nil {
L.RaiseError("AddRandom failed: %v", err)
return 0
}
plan.packets = append(plan.packets, scriptPacket{packet: packet, delay: planDelay(L, 3)})
plan.bytes += int(size)
return 0
}
func planFor(L *lua.LState, size int) *noisePlan {
plan, err := xlua.ReadUserData[*noisePlan](L.Get(1), "the plan HandleNoise was given is missing")
if err != nil {
L.RaiseError("%s", err.Error())
return nil
}
if !plan.open {
L.RaiseError("the plan only takes packets while HandleNoise runs")
return nil
}
if len(plan.packets) >= packetLimit {
L.RaiseError("the plan takes at most %d packets per call", packetLimit)
return nil
}
if plan.bytes+size > valueLimit {
L.RaiseError("the plan takes at most %d bytes of noise per call", valueLimit)
return nil
}
return plan
}
func planDelay(L *lua.LState, index int) time.Duration {
switch value := L.Get(index).(type) {
case *lua.LNilType:
return 0
case lua.LNumber:
if !(value >= 0 && value <= valueLimit) {
L.RaiseError("the delay must be 0 to %d milliseconds", valueLimit)
return 0
}
return time.Duration(value) * time.Millisecond
default:
L.RaiseError("the delay must be a number of milliseconds")
return 0
}
}
func luaPlan(L *lua.LState) (*lua.LUserData, *noisePlan, error) {
data, ok := L.GetField(L.G.Registry, planKey).(*lua.LUserData)
if !ok {
return nil, nil, errors.New("noise script has no plan")
}
plan, ok := data.Value.(*noisePlan)
if !ok {
return nil, nil, errors.New("noise script has no plan")
}
return data, plan, nil
}
@@ -1,70 +0,0 @@
package noise
import "testing"
func benchmarkEngine(b *testing.B, script string) *scriptEngine {
b.Helper()
engine, err := newScriptEngine(writeScript(b, script))
if err != nil {
b.Fatal(err)
}
b.Cleanup(engine.pool.Close)
return engine
}
func benchmarkPackets(b *testing.B, engine *scriptEngine, want int) {
b.Helper()
b.ReportAllocs()
var buf []scriptPacket
for i := 0; i < b.N; i++ {
packets, err := engine.packets("192.0.2.1:443", buf[:0])
if err != nil {
b.Fatal(err)
}
if len(packets) != want {
b.Fatalf("got %d packets, want %d", len(packets), want)
}
buf = packets
}
}
func BenchmarkLuaNoiseHook(b *testing.B) {
benchmarkPackets(b, benchmarkEngine(b, `function HandleNoise(plan, address) end`), 0)
}
func BenchmarkLuaNoisePackets(b *testing.B) {
b.Run("add", func(b *testing.B) {
benchmarkPackets(b, benchmarkEngine(b, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add(noise.Random(64))
plan:Add(noise.Random(64), 0)
end
`), 2)
})
b.Run("addrandom", func(b *testing.B) {
benchmarkPackets(b, benchmarkEngine(b, `
function HandleNoise(plan, address)
plan:AddRandom(64)
plan:AddRandom(64, 0)
end
`), 2)
})
b.Run("static", func(b *testing.B) {
conn := &noiseConn{config: &Config{Items: []*Item{
{RandMin: 64, RandMax: 64, RandRangeMax: 255},
{RandMin: 64, RandMax: 64, RandRangeMax: 255},
}}}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, item := range conn.config.Items {
if len(conn.buildPacket(item)) != 64 {
b.Fatal("unexpected packet size")
}
}
}
})
}
@@ -1,107 +0,0 @@
package noise
import (
"context"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
xlua "github.com/xtls/xray-core/common/lua"
lua "github.com/yuin/gopher-lua"
)
const (
scriptExecutionTimeout = time.Second
scriptInitTimeout = 120 * time.Second
)
type scriptEngine struct {
pool *xlua.Pool
}
type scriptPacket struct {
packet []byte
delay time.Duration
}
// TODO: close the pool of a mask whose inbound or outbound the API removed.
var (
scriptsMu sync.Mutex
scripts = make(map[*Config]*scriptEngine)
)
func (c *Config) engine() (*scriptEngine, error) {
scriptsMu.Lock()
defer scriptsMu.Unlock()
if engine := scripts[c]; engine != nil {
return engine, nil
}
engine, err := newScriptEngine(c.Script)
if err != nil {
return nil, err
}
scripts[c] = engine
return engine, nil
}
func newScriptEngine(script string) (*scriptEngine, error) {
program, err := xlua.Compile(script)
if err != nil {
return nil, err
}
pool, err := xlua.NewPool(context.Background(), scriptExecutionTimeout, program.NewStateFactory(
scriptInitTimeout,
256,
func(L *lua.LState) {
log.RegisterLua(L)
RegisterLua(L)
registerPlan(L)
},
func(L *lua.LState) error {
if L.GetGlobal("HandleNoise").Type() != lua.LTFunction {
return errors.New("noise script must define HandleNoise(...)")
}
return nil
}))
if err != nil {
return nil, err
}
return &scriptEngine{pool: pool}, nil
}
func (e *scriptEngine) packets(addr string, into []scriptPacket) ([]scriptPacket, error) {
var noise []scriptPacket
var reported error
if err := e.pool.WithState(nil, 0, func(L *lua.LState) error {
var err error
noise, reported, err = callHook(L, addr, into)
return err
}); err != nil {
return nil, err
}
return noise, reported
}
func callHook(L *lua.LState, addr string, into []scriptPacket) (packets []scriptPacket, reported error, err error) {
data, plan, err := luaPlan(L)
if err != nil {
return nil, nil, err
}
hook := L.GetGlobal("HandleNoise")
if hook.Type() != lua.LTFunction {
return nil, nil, errors.New("noise script must define HandleNoise(...)")
}
plan.packets, plan.bytes, plan.open = into[:0], 0, true
defer func() { plan.packets, plan.open = nil, false }()
if err := L.CallByParam(lua.P{Fn: hook, NRet: 1, Protect: true}, data, lua.LString(addr)); err != nil {
return nil, nil, err
}
return plan.packets, xlua.ReadError(L.Get(-1), "noise script error must be an error or a string"), nil
}
@@ -1,725 +0,0 @@
package noise
import (
"context"
"net"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
xlua "github.com/xtls/xray-core/common/lua"
lua "github.com/yuin/gopher-lua"
)
func newScriptConn(t *testing.T, script string) (*noiseConn, *fakePacketConn) {
t.Helper()
raw := &fakePacketConn{}
conn, err := NewConnClient(&Config{Script: writeScript(t, script)}, raw)
require.NoError(t, err)
return conn.(*noiseConn), raw
}
func newScriptConnError(t *testing.T, script string) error {
t.Helper()
_, err := NewConnClient(&Config{Script: writeScript(t, script)}, &fakePacketConn{})
return err
}
func TestScriptSendsStrings(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("first")
plan:Add("second")
end
`)
n, err := conn.WriteTo([]byte("payload"), &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 443})
require.NoError(t, err)
require.Equal(t, 7, n)
require.Equal(t, [][]byte{[]byte("first"), []byte("second"), []byte("payload")}, raw.packets())
}
func TestScriptSendsTables(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("a")
plan:Add("bb", 0)
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("a"), []byte("bb"), []byte("p")}, raw.packets())
}
func TestScriptDelaysPackets(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("x", 60)
end
`)
start := time.Now()
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.GreaterOrEqual(t, time.Since(start), 60*time.Millisecond)
require.Equal(t, [][]byte{[]byte("x"), []byte("p")}, raw.packets())
}
func TestScriptReceivesAddress(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add(address)
end
`)
addr := &net.UDPAddr{IP: net.IPv4(203, 0, 113, 7), Port: 8443}
_, err := conn.WriteTo([]byte("p"), addr)
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte(addr.String()), []byte("p")}, raw.packets())
}
func TestScriptSendsNoNoise(t *testing.T) {
for name, script := range map[string]string{
"sends nothing": "function HandleNoise(plan, address) end",
"returns nil": "function HandleNoise(plan, address) return nil end",
"returns a zero": "function HandleNoise(plan, address) return 0 end",
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets())
})
}
}
func TestScriptRandom(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
local size = noise.RandomBetween(20, 20)
plan:Add(noise.Random(size))
plan:Add(noise.Random(0))
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
packets := raw.packets()
require.Len(t, packets, 3)
require.Len(t, packets[0], 20)
require.Empty(t, packets[1])
}
func TestScriptRandomBetweenStaysInRange(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
for i = 1, 64 do
plan:Add(string.rep("x", noise.RandomBetween(1, 3)))
end
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
packets := raw.packets()
require.Len(t, packets, 65)
sizes := make(map[int]bool)
for _, packet := range packets[:64] {
require.GreaterOrEqual(t, len(packet), 1)
require.LessOrEqual(t, len(packet), 3)
sizes[len(packet)] = true
}
require.Len(t, sizes, 3, "every size in the range should show up")
}
func TestScriptKeepsStateBetweenPeers(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
local sent = 0
function HandleNoise(plan, address)
sent = sent + 1
plan:Add(string.rep("x", sent))
end
`)
for i := 1; i <= 3; i++ {
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: i})
require.NoError(t, err)
}
require.Equal(t, [][]byte{
[]byte("x"), []byte("p"),
[]byte("xx"), []byte("p"),
[]byte("xxx"), []byte("p"),
}, raw.packets())
}
func TestScriptRunsOncePerPeerUntilReset(t *testing.T) {
script := `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("n") end`
raw := &fakePacketConn{}
conn, err := NewConnClient(&Config{Script: writeScript(t, script), ResetMin: 0, ResetMax: 0}, raw)
require.NoError(t, err)
addr := &net.UDPAddr{Port: 1}
for i := 0; i < 3; i++ {
_, err := conn.WriteTo([]byte("p"), addr)
require.NoError(t, err)
}
require.Equal(t, [][]byte{[]byte("n"), []byte("p"), []byte("p"), []byte("p")}, raw.packets())
}
func TestScriptRunsAgainAfterReset(t *testing.T) {
script := `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("n") end`
raw := &fakePacketConn{}
conn, err := NewConnClient(&Config{Script: writeScript(t, script), ResetMin: 0, ResetMax: 1}, raw)
require.NoError(t, err)
noiseConn := conn.(*noiseConn)
addr := &net.UDPAddr{Port: 1}
_, err = noiseConn.WriteTo([]byte("p"), addr)
require.NoError(t, err)
noiseConn.m[addr.String()] = time.Now().Add(-time.Second)
_, err = noiseConn.WriteTo([]byte("p"), addr)
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("n"), []byte("p"), []byte("n"), []byte("p")}, raw.packets())
}
func TestScriptFailureStillSendsPayload(t *testing.T) {
for name, script := range map[string]string{
"raises": `function HandleNoise(plan, address) error("boom") end`,
"reports an error": `function HandleNoise(plan, address) return "nope" end`,
"packet is a bool": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add(true) end`,
"packet is a number": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add(5) end`,
"no packet": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add() end`,
"delay is negative": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("x", -1) end`,
"delay is too big": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("x", 65536) end`,
"delay is not a number": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("x", "soon") end`,
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
n, err := conn.WriteTo([]byte("payload"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, 7, n)
require.Equal(t, [][]byte{[]byte("payload")}, raw.packets())
})
}
}
func TestScriptFailsToLoad(t *testing.T) {
for name, script := range map[string]string{
"no HandleNoise": `function Other() end`,
"HandleNoise is data": `HandleNoise = 1`,
"syntax error": `function HandleNoise(`,
"top level error": `error("boom")`,
} {
t.Run(name, func(t *testing.T) {
require.Error(t, newScriptConnError(t, script))
})
}
}
func TestScriptNameInErrors(t *testing.T) {
err := newScriptConnError(t, `function HandleNoise(`)
require.Error(t, err)
require.Contains(t, err.Error(), "noise.lua", "errors name the file the script came from")
}
func writeScript(t testing.TB, script string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "noise.lua")
require.NoError(t, os.WriteFile(path, []byte(script), 0o600))
return path
}
func TestScriptFromAFile(t *testing.T) {
raw := &fakePacketConn{}
conn, err := NewConnClient(&Config{Script: writeScript(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("from a file")
end
`)}, raw)
require.NoError(t, err)
_, err = conn.WriteTo([]byte("payload"), &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 443})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("from a file"), []byte("payload")}, raw.packets())
}
func TestScriptFileIsMissing(t *testing.T) {
_, err := NewConnClient(&Config{Script: filepath.Join(t.TempDir(), "absent.lua")}, &fakePacketConn{})
require.Error(t, err)
}
func TestConnReusesTheEngine(t *testing.T) {
script := `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("n") end`
config := &Config{Script: writeScript(t, script)}
first, err := NewConnClient(config, &fakePacketConn{})
require.NoError(t, err)
second, err := NewConnClient(config, &fakePacketConn{})
require.NoError(t, err)
require.Same(t, first.(*noiseConn).script, second.(*noiseConn).script, "two connections share one pool")
}
func TestScriptIsSharedPerConfig(t *testing.T) {
script := `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("n") end`
path := writeScript(t, script)
config := &Config{Script: path}
first, err := NewConnClient(config, &fakePacketConn{})
require.NoError(t, err)
second, err := NewConnClient(config, &fakePacketConn{})
require.NoError(t, err)
require.Same(t, first.(*noiseConn).script, second.(*noiseConn).script,
"the same config reuses its pool")
other, err := NewConnClient(&Config{Script: path}, &fakePacketConn{})
require.NoError(t, err)
require.NotSame(t, first.(*noiseConn).script, other.(*noiseConn).script,
"another config naming the same file gets its own pool")
}
func TestScriptStateIsNotSharedBetweenConfigs(t *testing.T) {
script := `
local noise = require("xray.finalmask.noise")
local calls = 0
function HandleNoise(plan, address)
calls = calls + 1
plan:Add(string.rep("x", calls))
end
`
path := writeScript(t, script)
for i := 0; i < 2; i++ {
raw := &fakePacketConn{}
conn, err := NewConnClient(&Config{Script: path}, raw)
require.NoError(t, err)
_, err = conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("x"), []byte("p")}, raw.packets())
}
}
func TestScriptReportsAnError(t *testing.T) {
for name, script := range map[string]string{
"after sending": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("n")
return "not today"
end`,
"without sending": `function HandleNoise(plan, address) return "not today" end`,
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets())
})
}
}
func TestScriptInstanceSurvivesABadReturn(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
local calls = 0
function HandleNoise(plan, address)
calls = calls + 1
if calls == 1 then
return "first call refused"
end
plan:Add("call-" .. calls)
end
`)
for i := 1; i <= 2; i++ {
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: i})
require.NoError(t, err)
}
require.Equal(t, [][]byte{[]byte("p"), []byte("call-2"), []byte("p")}, raw.packets(),
"a refused return value keeps the instance, so its counter survives")
}
func TestScriptInstanceIsDroppedAfterAnException(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
local calls = 0
function HandleNoise(plan, address)
calls = calls + 1
if calls == 1 then
error("boom")
end
plan:Add("call-" .. calls)
end
`)
for i := 1; i <= 2; i++ {
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: i})
require.NoError(t, err)
}
require.Equal(t, [][]byte{[]byte("p"), []byte("p")}, raw.packets(),
"an exception destroys the instance, so the next call gets a fresh counter and raises again")
}
func TestScriptCanLog(t *testing.T) {
conn, raw := newScriptConn(t, `
local log = require("xray.log")
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
log.Debug("noise for ", address)
plan:Add("n")
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("n"), []byte("p")}, raw.packets())
}
func TestScriptRandomRejectsBadArguments(t *testing.T) {
for name, script := range map[string]string{
"negative size": `function HandleNoise(plan, address) require("xray.finalmask.noise").Random(-1) end`,
"reversed range": `function HandleNoise(plan, address) require("xray.finalmask.noise").RandomBetween(5, 1) end`,
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets())
})
}
}
func TestScriptStaticNoiseStillWorks(t *testing.T) {
raw := &fakePacketConn{}
conn, err := NewConnClient(&Config{Items: []*Item{{Packet: []byte("static")}}}, raw)
require.NoError(t, err)
_, err = conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("static"), []byte("p")}, raw.packets())
require.Nil(t, conn.(*noiseConn).script)
}
func TestScriptSendsInOrder(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("first")
plan:Add("second")
plan:Add("third")
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("first"), []byte("second"), []byte("third"), []byte("p")}, raw.packets())
}
func TestScriptBinaryPacket(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("\0\1\255")
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{{0x00, 0x01, 0xff}, []byte("p")}, raw.packets())
}
func TestScriptOnServerConn(t *testing.T) {
script := `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("n") end`
raw := &fakePacketConn{}
conn, err := NewConnServer(&Config{Script: writeScript(t, script)}, raw)
require.NoError(t, err)
_, err = conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("n"), []byte("p")}, raw.packets())
}
func TestScriptDropsHandleNoise(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
HandleNoise = nil
plan:Add("once")
end
`)
for i := 1; i <= 2; i++ {
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: i})
require.NoError(t, err)
}
require.Equal(t, [][]byte{[]byte("once"), []byte("p"), []byte("p")}, raw.packets(),
"a state that lost HandleNoise sends no noise instead of failing the write")
}
func TestScriptValueLimits(t *testing.T) {
for name, script := range map[string]string{
"random too big": `function HandleNoise(plan, address) require("xray.finalmask.noise").Random(65536) end`,
"random bound too big": `function HandleNoise(plan, address) require("xray.finalmask.noise").RandomBetween(0, 65536) end`,
"random bound negative": `function HandleNoise(plan, address) require("xray.finalmask.noise").RandomBetween(-1, 1) end`,
"delay too big": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("x", 65536) end`,
"huge delay from a float": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("x", 1e18) end`,
"delay is not a number": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("x", 0 / 0) end`,
"random size is not a number": `function HandleNoise(plan, address) require("xray.finalmask.noise").Random(0 / 0) end`,
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets())
})
}
}
func TestScriptValueLimitsAccepted(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add(noise.Random(65524))
plan:Add("x", 0)
plan:Add(tostring(noise.RandomBetween(65535, 65535)))
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
packets := raw.packets()
require.Len(t, packets, 4)
require.Len(t, packets[0], 65524, "the plan's whole budget minus the two packets after it")
require.Equal(t, []byte("65535"), packets[2])
}
func TestScriptSendsManyPackets(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
for i = 1, 64 do
plan:Add("n")
end
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Len(t, raw.packets(), 65)
}
func TestScriptPlanLimits(t *testing.T) {
for name, script := range map[string]string{
"too many packets": `function HandleNoise(plan, address)
for i = 1, 65 do
plan:Add("n")
end
end`,
"too many bytes": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
for i = 1, 2 do
plan:Add(noise.Random(40000))
end
end`,
"a shared config cannot fill memory": `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
while true do
plan:Add(noise.Random(65535))
end
end`,
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets(), "the noise is dropped and the payload still goes out")
})
}
}
func TestScriptKeepsPacketsSentBeforeAnError(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("sent")
error("boom")
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets(),
"an exception drops everything the call had queued")
}
func TestPlanAddRandom(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:AddRandom(40)
plan:AddRandom(noise.RandomBetween(16, 16), 1)
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
packets := raw.packets()
require.Len(t, packets, 3)
require.Len(t, packets[0], 40)
require.Len(t, packets[1], 16)
require.NotEqual(t, packets[0][:16], packets[1], "every packet gets its own random bytes")
}
func TestPlanAddRandomLimits(t *testing.T) {
for name, script := range map[string]string{
"size too big": `function HandleNoise(plan, address) plan:AddRandom(65536) end`,
"negative size": `function HandleNoise(plan, address) plan:AddRandom(-1) end`,
"size is not a number": `function HandleNoise(plan, address) plan:AddRandom(0 / 0) end`,
"over the budget": `function HandleNoise(plan, address)
plan:AddRandom(40000)
plan:AddRandom(40000)
end`,
"bad delay": `function HandleNoise(plan, address) plan:AddRandom(8, -1) end`,
} {
t.Run(name, func(t *testing.T) {
conn, raw := newScriptConn(t, script)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets())
})
}
}
func TestPlanAddNeedsThePlan(t *testing.T) {
conn, raw := newScriptConn(t, `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
local send = plan.Add
send("not a plan", "x")
end
`)
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: 1})
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("p")}, raw.packets(), "an Add without its plan drops the noise")
}
func TestPlanTakesNoPacketsOutsideTheHook(t *testing.T) {
L := lua.NewState()
defer L.Close()
RegisterLua(L)
registerPlan(L)
data, _, err := luaPlan(L)
require.NoError(t, err)
err = L.CallByParam(lua.P{Fn: L.GetField(data.Metatable, "Add"), Protect: true}, data, lua.LString("too early"))
require.Error(t, err)
require.Contains(t, err.Error(), "while HandleNoise runs")
}
func TestRegisterLuaHasNoSend(t *testing.T) {
L := lua.NewState()
defer L.Close()
RegisterLua(L)
require.NoError(t, L.DoString(`
local noise = require("xray.finalmask.noise")
if noise.Add ~= nil then
error("Add should not be exported to other modules")
end
assert(#noise.Random(4) == 4)
assert(noise.RandomBetween(3, 3) == 3)
`))
}
func TestEngineWithoutAScript(t *testing.T) {
_, err := (&Config{}).engine()
require.Error(t, err)
}
func TestScriptUnderConcurrency(t *testing.T) {
script := `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add(noise.Random(8))
plan:Add(address, 1)
end
`
config := &Config{Script: writeScript(t, script)}
raw := &fakePacketConn{}
conn, err := NewConnClient(config, raw)
require.NoError(t, err)
const peers = 40
var wg sync.WaitGroup
for i := 0; i < peers; i++ {
wg.Add(1)
go func(port int) {
defer wg.Done()
_, err := conn.WriteTo([]byte("p"), &net.UDPAddr{Port: port})
require.NoError(t, err)
}(i + 1)
}
wg.Wait()
require.Len(t, raw.packets(), peers*3, "every peer gets two noise packets and its payload")
}
func TestEngineIsCreatedOnceUnderConcurrency(t *testing.T) {
script := `local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address) plan:Add("n") end`
config := &Config{Script: writeScript(t, script)}
engines := make([]*scriptEngine, 16)
var wg sync.WaitGroup
for i := range engines {
wg.Add(1)
go func(i int) {
defer wg.Done()
engine, err := config.engine()
require.NoError(t, err)
engines[i] = engine
}(i)
}
wg.Wait()
for _, engine := range engines {
require.Same(t, engines[0], engine)
}
}
func TestPacketsWithoutAPlan(t *testing.T) {
program, err := xlua.Compile(writeScript(t, `function HandleNoise(plan, address) end`))
require.NoError(t, err)
pool, err := xlua.NewPool(context.Background(), time.Second,
program.NewStateFactory(time.Minute, 256, RegisterLua, nil))
require.NoError(t, err)
defer pool.Close()
_, err = (&scriptEngine{pool: pool}).packets("192.0.2.1:443", nil)
require.Error(t, err)
require.Contains(t, err.Error(), "no plan")
}
-71
View File
@@ -6,21 +6,17 @@ import (
"encoding/binary"
"io"
gonet "net"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/transport/internet/finalmask"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original"
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
)
@@ -1047,70 +1043,3 @@ func TestSudokuBDD(t *testing.T) {
}
})
}
func TestNoiseScriptOverRealSockets(t *testing.T) {
path := filepath.Join(t.TempDir(), "noise.lua")
script := `
local noise = require("xray.finalmask.noise")
function HandleNoise(plan, address)
plan:Add("\22\3\1")
plan:Add(noise.Random(noise.RandomBetween(16, 16)), 1)
end
`
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
t.Fatal(err)
}
mask, err := (&conf.NoiseMask{Script: path}).Build()
if err != nil {
t.Fatal(err)
}
clientRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = clientRaw.Close() })
serverRaw, err := gonet.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = serverRaw.Close() })
client, err := mask.(*noise.Config).WrapPacketConnClient(clientRaw, nil, nil)
if err != nil {
t.Fatal(err)
}
payload := []byte("real-payload")
writeErr := make(chan error, 1)
go func() {
_, err := client.WriteTo(payload, serverRaw.LocalAddr())
writeErr <- err
}()
_ = serverRaw.SetDeadline(time.Now().Add(5 * time.Second))
var got [][]byte
for i := 0; i < 3; i++ {
buf := make([]byte, 64)
n, _, err := serverRaw.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
got = append(got, buf[:n])
}
if err := <-writeErr; err != nil {
t.Fatal(err)
}
if !bytes.Equal(got[0], []byte{0x16, 0x03, 0x01}) {
t.Fatalf("unexpected first noise packet: %x", got[0])
}
if len(got[1]) != 16 {
t.Fatalf("unexpected second noise packet size: got=%d want=16", len(got[1]))
}
if !bytes.Equal(got[2], payload) {
t.Fatalf("unexpected payload: %q", got[2])
}
}
+13 -48
View File
@@ -8,8 +8,6 @@ package xdns
import (
_ "github.com/xtls/xray-core/common/serial"
internet "github.com/xtls/xray-core/transport/internet"
tls "github.com/xtls/xray-core/transport/internet/tls"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -104,9 +102,6 @@ type ResolverProto struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"`
Worker int32 `protobuf:"varint,3,opt,name=worker,proto3" json:"worker,omitempty"`
TlsConfig *tls.Config `protobuf:"bytes,4,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"`
QuicConfig *internet.QuicParams `protobuf:"bytes,5,opt,name=quic_config,json=quicConfig,proto3" json:"quic_config,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -155,27 +150,6 @@ func (x *ResolverProto) GetAddr() string {
return ""
}
func (x *ResolverProto) GetWorker() int32 {
if x != nil {
return x.Worker
}
return 0
}
func (x *ResolverProto) GetTlsConfig() *tls.Config {
if x != nil {
return x.TlsConfig
}
return nil
}
func (x *ResolverProto) GetQuicConfig() *internet.QuicParams {
if x != nil {
return x.QuicConfig
}
return nil
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Domains []*DomainProto `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"`
@@ -240,22 +214,17 @@ var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescrip
const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" +
"\n" +
".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\x1a#transport/internet/tls/config.proto\x1a\x1ftransport/internet/config.proto\"\x8b\x01\n" +
".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\"\x8b\x01\n" +
"\vDomainProto\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" +
"\tlen_limit\x18\x02 \x01(\x05R\blenLimit\x12\x1f\n" +
"\vlabel_limit\x18\x03 \x01(\x05R\n" +
"labelLimit\x12\x14\n" +
"\x05types\x18\x04 \x03(\x05R\x05types\x12\x14\n" +
"\x05edns0\x18\x05 \x01(\x05R\x05edns0\"\xd9\x01\n" +
"\x05edns0\x18\x05 \x01(\x05R\x05edns0\"7\n" +
"\rResolverProto\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\x12\x16\n" +
"\x06worker\x18\x03 \x01(\x05R\x06worker\x12B\n" +
"\n" +
"tls_config\x18\x04 \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfig\x12D\n" +
"\vquic_config\x18\x05 \x01(\v2#.xray.transport.internet.QuicParamsR\n" +
"quicConfig\"\xcb\x01\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\"\xcb\x01\n" +
"\x06Config\x12M\n" +
"\adomains\x18\x01 \x03(\v23.xray.transport.internet.finalmask.xdns.DomainProtoR\adomains\x12S\n" +
"\tresolvers\x18\x02 \x03(\v25.xray.transport.internet.finalmask.xdns.ResolverProtoR\tresolvers\x12\x1d\n" +
@@ -277,22 +246,18 @@ func file_transport_internet_finalmask_xdns_config_proto_rawDescGZIP() []byte {
var file_transport_internet_finalmask_xdns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_transport_internet_finalmask_xdns_config_proto_goTypes = []any{
(*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto
(*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto
(*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config
(*tls.Config)(nil), // 3: xray.transport.internet.tls.Config
(*internet.QuicParams)(nil), // 4: xray.transport.internet.QuicParams
(*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto
(*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto
(*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config
}
var file_transport_internet_finalmask_xdns_config_proto_depIdxs = []int32{
3, // 0: xray.transport.internet.finalmask.xdns.ResolverProto.tls_config:type_name -> xray.transport.internet.tls.Config
4, // 1: xray.transport.internet.finalmask.xdns.ResolverProto.quic_config:type_name -> xray.transport.internet.QuicParams
0, // 2: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto
1, // 3: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
0, // 0: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto
1, // 1: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto
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_xdns_config_proto_init() }
@@ -7,8 +7,6 @@ option java_package = "com.xray.transport.internet.finalmask.xdns";
option java_multiple_files = true;
import "common/serial/typed_message.proto";
import "transport/internet/tls/config.proto";
import "transport/internet/config.proto";
message DomainProto {
string name = 1;
@@ -21,9 +19,6 @@ message DomainProto {
message ResolverProto {
string type = 1;
string addr = 2;
int32 worker = 3;
xray.transport.internet.tls.Config tls_config = 4;
xray.transport.internet.QuicParams quic_config = 5;
}
message Config {
@@ -20,8 +20,6 @@ func NewResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, err
return NewTCPResolver(config, dialer)
case "udp":
return NewUDPResolver(config, dialer)
case "https":
return NewDOHResolver(config, dialer)
default:
return nil, errors.New("unknown type")
}
@@ -1,301 +0,0 @@
package xdns
import (
"bytes"
"context"
gotls "crypto/tls"
"io"
"net/http"
"runtime"
"strconv"
"sync"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask"
"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/tls"
)
type DOHResolver struct {
ctx context.Context
cancel func()
client *http.Client
url string
time time.Time
udpAddr *net.UDPAddr
readCh chan []byte
sendCh chan []byte
closeCh chan struct{}
wg sync.WaitGroup
mu sync.Mutex
}
func NewDOHResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, error) {
if config.Worker < 1 {
return nil, errors.New("invalid worker")
}
if config.TlsConfig == nil {
return nil, errors.New("empty tls config")
}
ctx, cancel := context.WithCancel(context.Background())
var tr http.RoundTripper
var setaddr func(addr net.Addr)
switch {
case len(config.TlsConfig.NextProtocol) == 1 && config.TlsConfig.NextProtocol[0] == "h3":
quicParams := config.QuicConfig
if quicParams == nil {
quicParams = &internet.QuicParams{}
}
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,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
ChromeParrot: !quicParams.DisableChromeParrot,
}
tr = &http3.Transport{
TLSClientConfig: config.TlsConfig.GetTLSConfig(),
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
dest, err := net.ParseDestination("udp:" + addr)
if err != nil {
return nil, err
}
conn, err := dialer.DialUDP(dest)
if err != nil {
return nil, err
}
setaddr(conn.RemoteAddr())
tr := &quic.Transport{Conn: conn.(*net.PacketConnWrapper).PacketConn, DisableGSO: quicParams.DisableGSO}
if !quicParams.DisableChromeParrot {
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
tlsCfg.GetCertificate = nil
}
qc, err := tr.Dial(ctx, conn.RemoteAddr(), tlsCfg, cfg)
if err != nil {
return nil, err
}
context.AfterFunc(qc.Context(), func() { tr.Close(); conn.Close() })
switch quicParams.Congestion {
case "", "reno":
case "bbr":
congestion.UseBBR(qc, bbr.Profile(quicParams.BbrProfile))
case "force-brutal":
congestion.UseBrutal(qc, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
default:
panic(quicParams.Congestion)
}
return qc, nil
},
}
default:
tlsConfig := config.TlsConfig.GetTLSConfig()
tr = &http.Transport{
ForceAttemptHTTP2: true,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination("tcp:" + addr)
if err != nil {
return nil, err
}
conn, err := dialer.DialTCP(dest)
if err != nil {
return nil, err
}
setaddr(conn.RemoteAddr())
var tlsConn net.Conn
if fingerprint := tls.GetFingerprint(config.TlsConfig.Fingerprint); fingerprint != nil {
tlsConn = tls.UClient(conn, tlsConfig, fingerprint)
err = tlsConn.(*tls.UConn).HandshakeContext(ctx)
} else {
tlsConn = gotls.Client(conn, tlsConfig)
err = tlsConn.(*gotls.Conn).HandshakeContext(ctx)
}
if err != nil {
_ = conn.Close()
return nil, err
}
return tlsConn, nil
},
}
}
client := &http.Client{
Transport: tr,
}
r := &DOHResolver{
ctx: ctx,
cancel: cancel,
client: client,
url: config.Addr,
udpAddr: &net.UDPAddr{IP: []byte{0, 0, 0, 0}},
readCh: make(chan []byte),
sendCh: make(chan []byte),
closeCh: make(chan struct{}),
}
for range config.Worker {
r.wg.Add(1)
go r.worker()
}
setaddr = r.setaddr
return r, nil
}
func (r *DOHResolver) closed() bool {
select {
case <-r.closeCh:
return true
default:
return false
}
}
func (r *DOHResolver) setaddr(addr net.Addr) {
r.mu.Lock()
defer r.mu.Unlock()
switch v := addr.(type) {
case *net.TCPAddr:
r.udpAddr = &net.UDPAddr{IP: v.IP, Port: v.Port}
case *net.UDPAddr:
r.udpAddr = v
default:
panic(v)
}
}
func (r *DOHResolver) cooldown(t time.Time) bool {
r.mu.Lock()
defer r.mu.Unlock()
if t == (time.Time{}) {
return time.Now().Before(r.time)
}
if t.After(r.time) {
r.time = t
}
return true
}
func (r *DOHResolver) do(p []byte) error {
req := common.Must2(http.NewRequestWithContext(r.ctx, "POST", r.url, bytes.NewReader(p)))
req.Header.Set("Accept", "application/dns-message")
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("User-Agent", "")
resp, err := r.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
p := pool4K.Get().([]byte)
off := 0
for {
n, err := resp.Body.Read(p[off:])
if n > 0 {
off += n
}
if off == len(p) || err != nil {
if err != io.EOF {
pool4K.Put(p[:cap(p)])
return err
}
break
}
}
select {
case <-r.closeCh:
pool4K.Put(p[:cap(p)])
case r.readCh <- p[:off]:
}
return nil
case http.StatusTooManyRequests:
if value := resp.Header.Get("Retry-After"); value != "" {
if t, err := http.ParseTime(value); err == nil {
r.cooldown(t)
} else if i, err := strconv.ParseUint(value, 10, 32); err == nil {
r.cooldown(time.Now().Add(time.Duration(i) * time.Second))
}
}
}
return errors.New("code ", resp.StatusCode, " header ", resp.Header)
}
func (r *DOHResolver) worker() {
defer r.wg.Done()
var p []byte
for {
select {
case <-r.closeCh:
return
default:
select {
case <-r.closeCh:
return
case p = <-r.sendCh:
}
}
if r.cooldown(time.Time{}) {
continue
}
if err := r.do(p); err != nil {
errors.LogErrorInner(context.Background(), err, "do err")
}
}
}
func (r *DOHResolver) Addr() *net.UDPAddr {
r.mu.Lock()
defer r.mu.Unlock()
return r.udpAddr
}
func (r *DOHResolver) Read(p []byte) (n int, err error) {
packet, ok := <-r.readCh
if ok {
n = copy(p, packet)
pool4K.Put(packet[:cap(packet)])
return n, nil
}
return 0, io.ErrClosedPipe
}
func (r *DOHResolver) Send(p []byte) {
select {
case <-r.closeCh:
return
default:
b := make([]byte, len(p))
copy(b, p)
select {
case <-r.closeCh:
return
case r.sendCh <- b:
}
}
}
func (r *DOHResolver) Close() {
r.mu.Lock()
if r.closed() {
r.mu.Unlock()
return
}
close(r.closeCh)
r.mu.Unlock()
r.cancel()
r.wg.Wait()
r.client.CloseIdleConnections()
close(r.readCh)
// close(r.sendCh)
}
+1 -19
View File
@@ -22,12 +22,6 @@ 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 {
@@ -39,9 +33,6 @@ 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 {
@@ -49,20 +40,11 @@ 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()
}
-1
View File
@@ -257,7 +257,6 @@ func (c *client) tcp(ctx context.Context) (stat.Connection, error) {
remote: c.conn.RemoteAddr(),
client: true,
lock: make(chan struct{}, 1),
}, nil
}
-1
View File
@@ -134,7 +134,6 @@ 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:
+4 -9
View File
@@ -2,13 +2,12 @@ package reality
import (
"context"
"crypto/ecdh"
"crypto/mldsa"
"io"
"net"
"os"
"time"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/reality"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet"
@@ -24,6 +23,7 @@ func (c *Config) GetREALITYConfig() *reality.Config {
Dest: c.Dest,
Xver: byte(c.Xver),
PrivateKey: c.PrivateKey,
MinClientVer: c.MinClientVer,
MaxClientVer: c.MaxClientVer,
MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond,
@@ -33,14 +33,9 @@ func (c *Config) GetREALITYConfig() *reality.Config {
KeyLogWriter: KeyLogWriterFromConfig(c),
}
var err error
if config.PrivateKey, err = ecdh.X25519().NewPrivateKey(c.PrivateKey); err != nil {
panic(err)
}
if c.Mldsa65Seed != nil {
if config.Mldsa65Key, err = mldsa.NewPrivateKey(mldsa.MLDSA65(), c.Mldsa65Seed); err != nil {
panic(err)
}
_, key := mldsa65.NewKeyFromSeed((*[32]byte)(c.Mldsa65Seed))
config.Mldsa65Key = key.Bytes()
}
if c.LimitFallbackUpload != nil {
config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes
+10 -9
View File
@@ -5,9 +5,7 @@ import (
"context"
"crypto/ecdh"
"crypto/ed25519"
"crypto/hkdf"
"crypto/hmac"
"crypto/mldsa"
"crypto/sha256"
"crypto/sha512"
gotls "crypto/tls"
@@ -24,6 +22,7 @@ import (
"time"
"unsafe"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
utls "github.com/refraction-networking/utls"
"github.com/xtls/reality"
"github.com/xtls/xray-core/common/crypto"
@@ -32,6 +31,7 @@ import (
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/crypto/hkdf"
"golang.org/x/net/http2"
)
@@ -114,8 +114,8 @@ func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x50
if len(certs[0].Extensions) > 0 {
h.Write(c.HandshakeState.Hello.Raw)
h.Write(c.HandshakeState.ServerHello.Raw)
verify, _ := mldsa.NewPublicKey(mldsa.MLDSA65(), c.Config.Mldsa65Verify)
if mldsa.Verify(verify, h.Sum(nil), certs[0].Extensions[0].Value, nil) == nil {
verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify)
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) {
c.Verified = true
return nil
}
@@ -176,7 +176,7 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
}
publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey)
if err != nil {
return nil, errors.New("REALITY: nil publicKey").Base(err)
return nil, errors.New("REALITY: publicKey == nil")
}
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil {
@@ -185,11 +185,12 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
if ecdhe == nil {
return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.")
}
if uConn.AuthKey, err = ecdhe.ECDH(publicKey); err != nil {
return nil, errors.New("REALITY: nil sharedKey").Base(err)
uConn.AuthKey, _ = ecdhe.ECDH(publicKey)
if uConn.AuthKey == nil {
return nil, errors.New("REALITY: SharedKey == nil")
}
if uConn.AuthKey, err = hkdf.Key(sha256.New, uConn.AuthKey, hello.Random[:20], "REALITY", 32); err != nil {
return nil, errors.New("REALITY: nil authKey").Base(err)
if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil {
return nil, err
}
aead := crypto.NewAesGcm(uConn.AuthKey)
if config.Show {
-19
View File
@@ -28,22 +28,3 @@ 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,
}
}
+3 -3
View File
@@ -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 NewBindError(errors.New("failed to get interface ", config.Interface).Base(err))
return 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 NewBindError(errors.New("failed to set IPV6_BOUND_IF").Base(err))
return 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 NewBindError(errors.New("failed to set IP_BOUND_IF").Base(err))
return errors.New("failed to set IP_BOUND_IF").Base(err)
}
}
}
+1 -1
View File
@@ -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 NewBindError(errors.New("failed to set Interface").Base(err))
return errors.New("failed to set Interface").Base(err)
}
}
+5 -5
View File
@@ -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 NewBindError(errors.New("failed to find the interface").Base(err))
return 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 NewBindError(errors.New("failed to set IP_UNICAST_IF").Base(err))
return 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 NewBindError(errors.New("failed to set IP_MULTICAST_IF").Base(err))
return 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 NewBindError(errors.New("failed to set IPV6_UNICAST_IF").Base(err))
return 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 NewBindError(errors.New("failed to set IPV6_MULTICAST_IF").Base(err))
return errors.New("failed to set IPV6_MULTICAST_IF").Base(err)
}
}
}
+17 -31
View File
@@ -68,27 +68,19 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
}
}
var lc net.ListenConfig
if sockopt != nil || len(Controllers) > 0 {
lc.Control = func(network, address string, c syscall.RawConn) error {
for _, ctl := range Controllers {
if err := ctl(network, address, c); err != nil {
return errors.New("failed to apply external controller").Base(err)
}
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")
}
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
}
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")
}
}
})
}
packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String())
if err != nil {
@@ -136,22 +128,16 @@ 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 {
return errors.New("failed to apply external controller").Base(err)
errors.LogInfoInner(ctx, err, "failed to apply external controller")
}
}
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")
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 _, ok := sockErr.(*BindError); ok {
return sockErr
}
return controlErr
}
return nil
})
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
// grpcUtlsInfo contains the auth information for a TLS authenticated connection.
// It implements the AuthInfo interface.
type grpcUtlsInfo struct {
State gotls.ConnectionState
State utls.ConnectionState
credentials.CommonAuthInfo
// This API is experimental.
SPIFFEID *url.URL
-18
View File
@@ -154,24 +154,6 @@ func (c *UConn) NegotiatedProtocol() string {
return state.NegotiatedProtocol
}
func (c *UConn) ConnectionState() tls.ConnectionState {
s := c.UConn.ConnectionState()
var state tls.ConnectionState
state.HandshakeComplete = s.HandshakeComplete
state.Version = s.Version
state.NegotiatedProtocol = s.NegotiatedProtocol
state.DidResume = s.DidResume
state.ServerName = s.ServerName
state.CipherSuite = s.CipherSuite
state.PeerCertificates = s.PeerCertificates
state.VerifiedChains = s.VerifiedChains
state.SignedCertificateTimestamps = s.SignedCertificateTimestamps
state.OCSPResponse = s.OCSPResponse
state.TLSUnique = s.TLSUnique
state.ECHAccepted = s.ECHAccepted
return state
}
func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn {
utlsConn := utls.UClient(c, copyConfig(config), *fingerprint)
return &UConn{UConn: utlsConn}
+60 -131
View File
@@ -2,124 +2,44 @@ package xdrive
import (
"context"
"io"
gotls "crypto/tls"
"net/http"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/reality"
"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" {
next = t.secure
return t.secure.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)
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
return t.plain.RoundTrip(r)
}
// 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
func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
func newServiceClient(streamSettings *internet.MemoryStreamConfig, timeout time.Duration, maxConns int) *http.Client {
var (
tlsConfig *tls.Config
sockopt *internet.SocketConfig
fronting *net.Destination
tlsConfig *tls.Config
realityConfig *reality.Config
sockopt *internet.SocketConfig
fronting *net.Destination
)
if streamSettings != nil {
tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
realityConfig = reality.ConfigFromStreamSettings(streamSettings)
sockopt = streamSettings.SocketSettings
fronting = streamSettings.Destination
}
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
overHTTP2 := allowsHTTP2(tlsConfig, realityConfig)
dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
host, err := net.ParseDestination("tcp:" + addr)
@@ -152,40 +72,58 @@ func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Du
return conn, err
}
// http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
dialTLS := func(ctx context.Context, addr string) (net.Conn, error) {
conn, host, err := dial(ctx, addr)
if err != nil {
return nil, err
}
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)
if realityConfig != nil {
return reality.UClient(conn, realityConfig, ctx, 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
gotlsConfig := &gotls.Config{ServerName: host.Address.String()}
if tlsConfig != nil {
gotlsConfig = tlsConfig.GetTLSConfig(tls.WithDestination(host))
}
return tlsConn, nil
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
}
secure := func(connsPerHost int) *http.Transport {
return &http.Transport{
DialTLSContext: dialTLS,
ForceAttemptHTTP2: true,
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)
},
IdleConnTimeout: net.ConnIdleTimeout,
MaxIdleConns: maxConns,
MaxIdleConnsPerHost: maxConns,
MaxConnsPerHost: connsPerHost,
HTTP2: &http.HTTP2Config{SendPingTimeout: net.ChromeH2KeepAlivePeriod},
MaxConnsPerHost: maxConns,
}
}
@@ -198,27 +136,18 @@ func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Du
MaxIdleConnsPerHost: maxConns,
MaxConnsPerHost: maxConns,
},
secure: &secureTransport{h2: secure(1), h1: secure(maxConns)},
stall: stall,
secure: secure,
},
Timeout: timeout,
}
}
// 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)
func allowsHTTP2(tlsConfig *tls.Config, realityConfig *reality.Config) bool {
if realityConfig != nil {
return true
}
resp, err := t.h2.RoundTrip(r)
if err == nil && resp.ProtoMajor == 1 {
t.h1Hosts.Store(r.URL.Host, true)
if tlsConfig == nil {
return false
}
return resp, err
return len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "h2"
}
-407
View File
@@ -1,18 +1,11 @@
package xdrive
import (
"bytes"
"context"
gotls "crypto/tls"
"encoding/base64"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"slices"
"sync"
"sync/atomic"
"testing"
"time"
@@ -106,403 +99,3 @@ 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(&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(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
type tlsServer struct {
*httptest.Server
mu sync.Mutex
hello *gotls.ClientHelloInfo
conns 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) {
if state == http.StateNew {
s.conns.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(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(&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)
}
}
// 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(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(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(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(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(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()
}
}
type roundTrip func(*http.Request) (*http.Response, error)
func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
+2 -3
View File
@@ -26,8 +26,7 @@ const (
driveMaxAttempts = 8
driveMaxInflight = 32
driveInlineLimit = 12000
// driveTimeout ends a request that makes no progress for that long
driveTimeout = 60 * time.Second
driveTimeout = 60 * time.Second
)
var (
@@ -154,7 +153,7 @@ func jitter(backoff time.Duration) time.Duration {
if half <= 0 {
return backoff
}
return half + time.Duration(dice.RollInt63n(int64(half)))
return half + time.Duration(dice.Roll(int(half)))
}
func rateLimited(payload []byte) bool {
+1 -1
View File
@@ -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,
+1 -11
View File
@@ -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,
@@ -564,16 +564,6 @@ func TestDriveSharedClient(t *testing.T) {
}
}
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) {
drive := newFakeDrive(t)
storage := newDriveBackend(t)
+1 -1
View File
@@ -35,7 +35,7 @@ func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
switch config.Service {
case "local":
return newLocalStorage(config.RemoteFolder)
case "google-drive", "Google Drive":
case "Google Drive":
return sharedStorage(streamSettings, config, func() (Storage, error) {
return newDriveStorage(streamSettings, config)
})