Compare commits

...
Author SHA1 Message Date
风扇滑翔翼andGitHub ccf59c5fd8 Fix udp match 2026-10-19 19:00:44 +00:00
RPRXandGitHub 92ccc939aa Xray-core v26.10.17
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-10-17 15:23:56 +00:00
Artem LytkinandGitHub 95461cc148 XDRIVE transport: Require VLESS Multiplex+Encryption; "Google Drive"->"google-drive" (#7182)
https://github.com/XTLS/Xray-core/pull/6748#issuecomment-5752780799
2026-10-17 14:13:57 +00:00
Artem LytkinandGitHub 3834e504b8 XDRIVE transport: Prefer h2 (Chrome CH by default); End stalled requests; Other small changes (#7181)
https://github.com/XTLS/Xray-core/pull/7170#issuecomment-6204133548
2026-10-17 09:24:53 +00:00
0f127f934c XTLS Vision: Compatible with Go 1.27.3+ (#7176)
https://github.com/XTLS/Xray-core/pull/7176#issuecomment-6229087350

Fixes https://github.com/XTLS/Xray-core/issues/7162#issuecomment-6210430747

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-10-17 03:37:14 +00:00
0f3d884786 WireGuard outbound: Keep the device when its setup fails (#7151)
https://github.com/XTLS/Xray-core/pull/7151#issuecomment-6186150131

Fixes https://github.com/XTLS/Xray-core/pull/7151#issuecomment-6190189547

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-10-16 09:50:47 +00:00
da2248c516 Hysteria client: Reset stalled writes when the connection is closed (#7172)
https://github.com/XTLS/Xray-core/pull/7172#issuecomment-6208589708

Fixes https://github.com/XTLS/Xray-core/issues/7169#issuecomment-6204450876

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-10-16 15:25:17 +08:00
fe2ae23f42 Sockopt: Cancel dialing after failed to bind interface, external controllers included such as TUN inbound's (#7173)
https://github.com/XTLS/Xray-core/pull/7161#issuecomment-6185101741

Fixes https://github.com/XTLS/Xray-core/issues/7160#issuecomment-6200788453

---------

Co-authored-by: Gleb <a1212gleb@gmail.com>
2026-10-16 06:37:52 +00:00
d15966c76c Fixes: Regard int() as int32() by default, which should be a coding standard (#7155)
https://github.com/XTLS/Xray-core/pull/7155#issuecomment-6208455069

Fixes https://github.com/XTLS/Xray-core/pull/7148#issuecomment-6173633056

---------

Co-authored-by: Denis Orlov <freshcrowd@gmail.com>
Co-authored-by: Milomilo777 <117558067+Milomilo777@users.noreply.github.com>
2026-10-16 05:12:52 +00:00
206a24a9f0 Noise finalmask: Support Lua script as well (on main branch: inline scripts were disabled for now; sandbox-only for now) (#7124)
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6108165942
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6149363945
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6207934753

---------

Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-10-16 04:43:55 +00:00
MeowandRPRX ac5d8a83ba Lua: Sandbox by default (whitelisted functions); Disable inline scripts for now; Add more checks for security reasons (#7136)
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6158632073
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6170831601
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6184362904
2026-10-16 01:55:35 +00:00
Artem LytkinandGitHub 8703fdb101 WireGuard, MASQUE proxies: Use Xray-core's built-in lightweight TCP/IP stack instead of gVisor as well (#7156)
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6116258188
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6120742739
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6137264458
https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6202933664
https://github.com/XTLS/Xray-core/pull/7156#issuecomment-6203239736
2026-10-15 23:38:14 +00:00
LjhAUMEMandGitHub 10879a4a8b XDNS finalmask: Support DoH ("https://", tlsConfig, domain fronting, etc., quicConfig, Chrome Client Hello by default) with empty HTTP User-Agent (#7140)
https://github.com/XTLS/Xray-core/pull/7103#issuecomment-6118333973
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6161808702
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6167254749
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6185965142
https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6200775375
2026-10-15 22:41:19 +00:00
0888beb38a SS2022 proxy: Correct interrupt behavior and UDP conn change (#7143)
Fixes https://github.com/XTLS/Xray-core/pull/7142 and https://github.com/XTLS/Xray-core/pull/7143#issuecomment-6163926468

---------

Co-authored-by: ChownYourLife <260039878+ChownYourLife@users.noreply.github.com>
2026-10-14 11:05:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
34acded003 Bump golang.org/x/net from 0.60.0 to 0.61.0 (#7147)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.60.0 to 0.61.0.
- [Commits](https://github.com/golang/net/compare/v0.60.0...v0.61.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 10:57:32 +00:00
RPRXandGitHub 676a600edb Update github.com/xtls/reality to 20261014101318
https://github.com/XTLS/REALITY/commit/83f621788ca88b83179810b12baa7ae518aacb83
https://github.com/XTLS/REALITY/commit/14cefac9bd7a724a811b02759fa97aa86a1ec894
https://github.com/XTLS/REALITY/commit/3c98159dee388c2914a2b9e73ea57bc10023f9d3
2026-10-14 10:44:41 +00:00
108 changed files with 8263 additions and 2026 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ func luaServers(s *DNS) []luaDNSServer {
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) { func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
L.PreloadModule("xray.dns", func(L *lua.LState) int { L.PreloadModule("xray.dns", func(L *lua.LState) int {
pushIPs := xlua.NewSlicePusher[net.IP](L) pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
serverList := L.CreateTable(len(servers), 0) serverList := L.CreateTable(len(servers), 0)
for i, client := range servers { for i, client := range servers {
+1
View File
@@ -30,6 +30,7 @@ func newScriptEngine(script string) (*scriptEngine, error) {
func (e *scriptEngine) start(server *DNS) error { func (e *scriptEngine) start(server *DNS) error {
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory( pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
scriptExecutionTimeout*20, scriptExecutionTimeout*20,
256,
func(L *lua.LState) { func(L *lua.LState) {
geodata.RegisterLua(L) geodata.RegisterLua(L)
log.RegisterLua(L) log.RegisterLua(L)
+7
View File
@@ -23,6 +23,9 @@ import (
const idleTimeout = 30 * time.Second const idleTimeout = 30 * time.Second
// DO NOT EDIT. Ignored by geodata, rejected by Lua.
const luaGuardHeader = "\x12\x01\x00"
type stage struct { type stage struct {
target string target string
temp string temp string
@@ -170,6 +173,10 @@ func (d *downloader) downloadOne(asset *Asset) (stage, error) {
} }
}() }()
if _, err := temp.WriteString(luaGuardHeader); err != nil {
temp.Close()
return stage{}, err
}
if err := d.fetch(asset.Url, temp); err != nil { if err := d.fetch(asset.Url, temp); err != nil {
temp.Close() temp.Close()
return stage{}, err return stage{}, err
+73
View File
@@ -0,0 +1,73 @@
package geodata
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
commongeodata "github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua"
"github.com/yuin/gopher-lua/parse"
"google.golang.org/protobuf/proto"
)
func TestDownloadOneLuaGuard(t *testing.T) {
for _, tc := range []struct {
name string
body string
code string
}{
{"geodata", "\x0a\x03\x0a\x01A", "A"},
{"prefixed geodata", luaGuardHeader + "\x0a\x03\x0a\x01A", "A"},
{"Lua", "function HandleRoute() end", ""},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil {
t.Fatal(err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(tc.body))
}))
defer server.Close()
d := &downloader{ctx: context.Background(), httpClient: server.Client()}
staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"})
if err != nil {
t.Fatal(err)
}
tx, err := swapAll([]stage{staged})
if err != nil {
t.Fatal(err)
}
if err := tx.commit(); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(staged.target)
if err != nil || string(data) != luaGuardHeader+tc.body {
t.Fatalf("downloaded data = % x, %v", data, err)
}
program, err := xlua.Compile(staged.target)
var syntaxErr *parse.Error
if program != nil || !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
t.Fatalf("want Lua rejection at the first byte, got %v, %v", program, err)
}
if tc.code != "" {
var list commongeodata.GeoSiteList
if err := proto.Unmarshal(data, &list); err != nil {
t.Fatal(err)
}
if len(list.Entry) != 1 || list.Entry[0].Code != tc.code {
t.Fatalf("header changed geodata: %v", &list)
}
if _, err := commongeodata.ParseDomainRules([]string{"ext:" + filepath.Base(staged.target) + ":" + tc.code}, commongeodata.Domain_Full); err != nil {
t.Fatalf("geodata loader rejected the header: %v", err)
}
}
})
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
stats.Min = 0 stats.Min = 0
return stats return stats
} }
stats.Average = time.Duration(int(sum) / cnt) stats.Average = sum / time.Duration(cnt)
var std float64 var std float64
if cnt < 2 { if cnt < 2 {
// no enough data for standard deviation, we assume it's half of the average rtt // no enough data for standard deviation, we assume it's half of the average rtt
+1 -1
View File
@@ -62,7 +62,7 @@ func (r *Router) RegisterLua(L *lua.LState) {
} }
func registerLuaContext(L *lua.LState) { func registerLuaContext(L *lua.LState) {
pushIPs := xlua.NewSlicePusher[net.IP](L) pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
attributes := L.NewTypeMetatable(luaAttributesType) attributes := L.NewTypeMetatable(luaAttributesType)
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int { L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
values := L.CheckUserData(1).Value.(map[string]string) values := L.CheckUserData(1).Value.(map[string]string)
+1
View File
@@ -31,6 +31,7 @@ func newScriptEngine(script string) (*scriptEngine, error) {
func (e *scriptEngine) start(router *Router) error { func (e *scriptEngine) start(router *Router) error {
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory( pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
scriptExecutionTimeout*20, scriptExecutionTimeout*20,
256,
func(L *lua.LState) { func(L *lua.LState) {
geodata.RegisterLua(L) geodata.RegisterLua(L)
log.RegisterLua(L) log.RegisterLua(L)
+19 -14
View File
@@ -17,12 +17,12 @@ import (
) )
func checkFile(file, code string) error { func checkFile(file, code string) error {
r, err := filesystem.OpenAsset(file) r, size, err := filesystem.OpenAsset(file)
if err != nil { if err != nil {
return errors.New("failed to open ", file).Base(err) return errors.New("failed to open ", file).Base(err)
} }
defer r.Close() defer r.Close()
if _, err := find(r, []byte(code), false); err != nil { if _, err := find(r, []byte(code), false, size); err != nil {
return errors.New("failed to check code ", code, " from ", file).Base(err) return errors.New("failed to check code ", code, " from ", file).Base(err)
} }
return nil return nil
@@ -30,12 +30,12 @@ func checkFile(file, code string) error {
func loadFile(file, code string) ([]byte, error) { func loadFile(file, code string) ([]byte, error) {
runtime.GC() // peak mem runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file) r, size, err := filesystem.OpenAsset(file)
if err != nil { if err != nil {
return nil, errors.New("failed to open ", file).Base(err) return nil, errors.New("failed to open ", file).Base(err)
} }
defer r.Close() defer r.Close()
bs, err := find(r, []byte(code), true) bs, err := find(r, []byte(code), true, size)
if err != nil { if err != nil {
return nil, errors.New("failed to load code ", code, " from ", file).Base(err) return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
} }
@@ -60,13 +60,13 @@ func loadIP(file, code string) ([]*CIDR, error) {
// unmarshalling it into a []*Domain, so value is only valid during fn. // unmarshalling it into a []*Domain, so value is only valid during fn.
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error { func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
runtime.GC() // peak mem runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file) r, size, err := filesystem.OpenAsset(file)
if err != nil { if err != nil {
return errors.New("failed to open ", file).Base(err) return errors.New("failed to open ", file).Base(err)
} }
defer r.Close() defer r.Close()
br := bufio.NewReaderSize(r, 64*1024) br := bufio.NewReaderSize(r, 64*1024)
n, err := seek(br, []byte(code)) n, err := seek(br, []byte(code), size)
if err != nil { if err != nil {
return errors.New("failed to load code ", code, " from ", file).Base(err) return errors.New("failed to load code ", code, " from ", file).Base(err)
} }
@@ -114,6 +114,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
if shift == 63 && b > 1 {
break // Only one value bit fits in the tenth byte.
}
x |= (uint64(b) & 0x7F) << shift x |= (uint64(b) & 0x7F) << shift
if (b & 0x80) == 0 { if (b & 0x80) == 0 {
return x, nil return x, nil
@@ -123,9 +126,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
return 0, errors.New("varint overflow") return 0, errors.New("varint overflow")
} }
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) { func find(r io.Reader, code []byte, readBody bool, size int64) ([]byte, error) {
br := bufio.NewReaderSize(r, 64*1024) br := bufio.NewReaderSize(r, 64*1024)
bodyL, err := seek(br, code) bodyL, err := seek(br, code, size)
if err != nil || !readBody { if err != nil || !readBody {
return nil, err return nil, err
} }
@@ -137,7 +140,7 @@ func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
} }
// seek advances br to the body of the entry for code and returns the body length. // seek advances br to the body of the entry for code and returns the body length.
func seek(br *bufio.Reader, code []byte) (int, error) { func seek(br *bufio.Reader, code []byte, size int64) (int, error) {
codeL := len(code) codeL := len(code)
if codeL == 0 { if codeL == 0 {
return 0, errors.New("empty code") return 0, errors.New("empty code")
@@ -145,7 +148,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
need := 2 + codeL // TODO: if code too long need := 2 + codeL // TODO: if code too long
for { for {
if _, err := br.ReadByte(); err != nil { tag, err := br.ReadByte()
if err != nil {
return 0, err return 0, err
} }
@@ -153,10 +157,10 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
bodyL := int(x) if x == 0 || x > uint64(size) || x > uint64(^uint(0)>>1) {
if bodyL <= 0 { return 0, errors.New("invalid body length: ", x)
return 0, errors.New("invalid body length: ", bodyL)
} }
bodyL := int(x)
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single // Peek no more than the buffer holds: a code longer than the buffer cannot match a single
// length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does. // length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does.
@@ -167,7 +171,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
} }
return 0, err return 0, err
} }
if bodyL >= need && len(prefix) >= need && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) { if tag == 0x0a && bodyL >= need && len(prefix) >= need &&
prefix[0] == 0x0a && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
return bodyL, nil return bodyL, nil
} }
if _, err := br.Discard(bodyL); err != nil { if _, err := br.Discard(bodyL); err != nil {
+53
View File
@@ -1,6 +1,7 @@
package geodata package geodata
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -12,6 +13,58 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
func TestFindChecksEntryAndCodeTags(t *testing.T) {
for _, payload := range [][]byte{
{0x12, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as entry.
{0x08, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Wrong entry wire type.
{0x0a, 6, 0x1a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as code.
{0x0a, 6, 0x08, 4, 'T', 'E', 'S', 'T'}, // Wrong code wire type.
} {
if _, err := find(bytes.NewReader(payload), []byte("TEST"), true, int64(len(payload))); err == nil {
t.Fatalf("matched incorrect entry/code tags in %x", payload)
}
}
}
func TestLoadFileRejectsVarintOverflow(t *testing.T) {
payload := []byte{
0x0a,
0x86, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02, // 2^64 + 6
0x0a, 0x04, 'T', 'E', 'S', 'T',
}
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
if err := os.WriteFile(filepath.Join(dir, "overflow.dat"), payload, 0o600); err != nil {
t.Fatal(err)
}
if _, err := loadFile("overflow.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "varint overflow") {
t.Fatalf("want varint overflow, got %v", err)
}
}
func TestLoadFileRejectsOversizedEntry(t *testing.T) {
payload := []byte{
0x0a, 0x03, 0x0a, 0x01, 'A', // A valid entry before the oversized one.
0x0a, 0x80, 0x80, 0x80, 0x80, 0x04, // Entry length: 1 GiB.
0x0a, 0x04, 'T', 'E', 'S', 'T',
}
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
if err := os.WriteFile(filepath.Join(dir, "oversized.dat"), payload, 0o600); err != nil {
t.Fatal(err)
}
// Check without allocating the body first, so a regression fails safely.
if err := checkFile("oversized.dat", "TEST"); err == nil {
t.Fatal("accepted an entry length larger than the file")
}
if _, err := loadFile("oversized.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "invalid body length") {
t.Fatalf("want invalid body length, got %v", err)
}
if err := loadSite("oversized.dat", "TEST", "", func(Domain_Type, []byte) {}); err == nil || !strings.Contains(err.Error(), "invalid body length") {
t.Fatalf("want invalid body length, got %v", err)
}
}
type siteEntry struct { type siteEntry struct {
Type Domain_Type Type Domain_Type
Value string Value string
+2 -2
View File
@@ -44,8 +44,8 @@ func RegisterLua(L *lua.LState) {
"Match": luaIPMatch, "Match": luaIPMatch,
"AnyMatch": luaIPAnyMatch, "AnyMatch": luaIPAnyMatch,
"Matches": luaIPMatches, "Matches": luaIPMatches,
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[net.IP](L)), "FilterIPs": newLuaIPFilterIPs(xlua.NewReadOnlySlicePusher[net.IP](L)),
}) }, "SetReverse", "ToggleReverse")
return 1 return 1
})) }))
+6
View File
@@ -78,6 +78,7 @@ local geodata = require("xray.geodata")
local matcher = geodata.BuildIPMatcher("127.0.0.0/8") local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch) assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs) assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
assert(matcher.Reload == nil and matcher.reload == nil)
assert(matcher:match(ip)) assert(matcher:match(ip))
assert(matcher:anyMatch({ip}) and matcher:matches({ip})) assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
assert(not matcher:AnyMatch(nil)) assert(not matcher:AnyMatch(nil))
@@ -97,6 +98,11 @@ assert(matcher.missing == nil)
local domain = geodata.BuildDomainMatcher("full:example.com") local domain = geodata.BuildDomainMatcher("full:example.com")
assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny) assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny)
assert(domain.Reload == nil and domain.reload == nil)
for _, value in ipairs({matcher, domain}) do
assert(not pcall(function() return -value end))
assert(not pcall(function() return value ^ {} end))
end
assert(domain:matchAny("example.com")) assert(domain:matchAny("example.com"))
assert(#domain:Match("example.com") == 1) assert(#domain:Match("example.com") == 1)
assert(domain:match("example.com")[1] == 0) assert(domain:match("example.com")[1] == 0)
@@ -0,0 +1,19 @@
// DO NOT EDIT. Regression test for app/geodata's luaGuardHeader.
package lua
import (
"errors"
"strings"
"testing"
"github.com/yuin/gopher-lua/parse"
)
func TestGeodataLuaGuard(t *testing.T) {
_, err := parse.Parse(strings.NewReader("\x12\x01\x00function HandleRoute() end"), "geodata")
var syntaxErr *parse.Error
if !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
t.Fatalf("want Lua rejection at the first byte, got %v", err)
}
}
+95 -4
View File
@@ -1,6 +1,9 @@
package lua package lua
import ( import (
"net"
"reflect"
glua "github.com/yuin/gopher-lua" glua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
) )
@@ -9,7 +12,7 @@ import (
// The returned function wraps slices without reflection or metatable lookup, // The returned function wraps slices without reflection or metatable lookup,
// and pushes nil for nil slices. Use it with this state or its coroutines. // and pushes nil for nil slices. Use it with this state or its coroutines.
func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) { func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
metatable := luar.New(L, []T{}).(*glua.LUserData).Metatable metatable := luar.MT(L, []T(nil)).LTable
return func(L *glua.LState, values []T) { return func(L *glua.LState, values []T) {
if values == nil { if values == nil {
L.Push(glua.LNil) L.Push(glua.LNil)
@@ -22,21 +25,105 @@ func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
} }
} }
// NewReadOnlySlicePusher borrows slices without copying and recursively makes
// nested slice types read-only through their shared metatables. IP byte views
// and masks are also read-only. Other referenced elements retain their mutability.
// Element bindings are initialized on first indexing or iteration.
func NewReadOnlySlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
sliceMetatable := luar.MT(L, []T{})
if sliceMetatable.RawGetString("__readonly") != glua.LTrue {
readOnly := L.NewFunction(func(L *glua.LState) int {
L.RaiseError("slice is read-only")
return 0
})
setSliceReadOnly[[]T](L, readOnly)
}
return NewSlicePusher[T](L)
}
func setSliceReadOnly[S ~[]E, E any](L *glua.LState, readOnly *glua.LFunction) {
var visit func(reflect.Type)
visit = func(typ reflect.Type) {
if typ.Kind() != reflect.Slice {
return
}
metatable := luar.MT(L, reflect.Zero(typ).Interface())
if metatable.RawGetString("__readonly") == glua.LTrue {
return
}
// Mark before descending because named slice types can be recursive.
metatable.RawSetString("__readonly", glua.LTrue)
metatable.RawSetString("__newindex", readOnly)
// Reject explicitly so Lua cannot fall back to the other operand.
metatable.RawSetString("__add", readOnly)
if typ == reflect.TypeFor[net.IP]() {
// A nil receiver lets AppendText return its argument as a writable []byte.
methods := metatable.RawGetString("methods").(*glua.LTable)
appendText := methods.RawGetString("AppendText")
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
if method == appendText {
methods.RawSet(key, glua.LNil)
}
}
visit(reflect.TypeFor[net.IPMask]())
}
if element := typ.Elem(); element.Kind() == reflect.Slice {
index := metatable.RawGetString("__index").(*glua.LFunction)
call := metatable.RawGetString("__call").(*glua.LFunction)
initialize := func() {
visit(element)
metatable.RawSetString("__index", index)
metatable.RawSetString("__call", call)
}
metatable.RawSetString("__index", L.NewFunction(func(L *glua.LState) int {
initialize()
return index.GFunction(L)
}))
metatable.RawSetString("__call", L.NewFunction(func(L *glua.LState) int {
initialize()
return call.GFunction(L)
}))
}
}
visit(reflect.TypeFor[S]())
}
// DirectMethod handles a Lua call without luar's reflected method invocation. // DirectMethod handles a Lua call without luar's reflected method invocation.
// It returns the result count and whether it handled the arguments. On false, // It returns the result count and whether it handled the arguments. On false,
// it must leave the stack unchanged for the original luar wrapper. // it must leave the stack unchanged for the original luar wrapper.
type DirectMethod func(L *glua.LState) (nresults int, handled bool) type DirectMethod func(L *glua.LState) (nresults int, handled bool)
// PushMethods pushes a luar userdata exposing only the named Go methods and
// their aliases through reflected calls. The method list is fixed by the first
// binding of that Go type in each LState.
func PushMethods(L *glua.LState, value any, methods ...string) {
PushWithDirectMethods(L, value, nil, methods...)
}
// PushWithDirectMethods pushes a luar userdata with typed Go method bindings. // PushWithDirectMethods pushes a luar userdata with typed Go method bindings.
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup // Handled calls bypass luar's argument conversion and reflect.Call; method lookup
// uses the methods table directly instead of luar's reflected __index handler. // uses the methods table directly instead of luar's reflected __index handler.
// value must expose methods only. Bindings and their closures are installed once // Only directMethods and additionalMethods are exposed, including their aliases.
// per Go type per LState, outside the method-call hot path. // Bindings and their closures are installed once per Go type per LState, outside
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod) { // the method-call hot path.
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod, additionalMethods ...string) {
userdata := luar.New(L, value).(*glua.LUserData) userdata := luar.New(L, value).(*glua.LUserData)
metatable := userdata.Metatable.(*glua.LTable) metatable := userdata.Metatable.(*glua.LTable)
methods := metatable.RawGetString("methods").(*glua.LTable) methods := metatable.RawGetString("methods").(*glua.LTable)
if metatable.RawGetString("__index") != methods { if metatable.RawGetString("__index") != methods {
// Keep all aliases for the explicitly exposed methods.
exposed := make(map[glua.LValue]bool, len(directMethods)+len(additionalMethods))
for name := range directMethods {
exposed[methods.RawGetString(name)] = true
}
for _, name := range additionalMethods {
exposed[methods.RawGetString(name)] = true
}
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
if !exposed[method] {
methods.RawSet(key, glua.LNil)
}
}
for name, direct := range directMethods { for name, direct := range directMethods {
original := methods.RawGetString(name) original := methods.RawGetString(name)
fn := L.NewFunction(func(L *glua.LState) int { fn := L.NewFunction(func(L *glua.LState) int {
@@ -52,6 +139,10 @@ func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]D
} }
} }
} }
// Pointer operations would bypass the method-only API.
metatable.RawSetString("__newindex", glua.LNil)
metatable.RawSetString("__pow", glua.LNil)
metatable.RawSetString("__unm", glua.LNil)
metatable.RawSetString("__index", methods) metatable.RawSetString("__index", methods)
} }
L.Push(userdata) L.Push(userdata)
+183
View File
@@ -2,6 +2,7 @@ package lua
import ( import (
"net" "net"
"reflect"
"testing" "testing"
glua "github.com/yuin/gopher-lua" glua "github.com/yuin/gopher-lua"
@@ -47,6 +48,188 @@ assert(ok and result == true)
} }
} }
func TestReadOnlySlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
values := []int{3, 5}
NewReadOnlySlicePusher[int](L)(L, values)
L.SetGlobal("values", L.Get(-1))
L.Pop(1)
if err := L.DoString(`
assert(#values == 2 and values[1] == 3 and values[2] == 5)
assert(not pcall(function() values[1] = 7 end))
assert(not pcall(function() return values + 7 end))
local sum = 0
for _, value in values() do sum = sum + value end
assert(sum == 8)
`); err != nil {
t.Fatal(err)
}
got := L.GetGlobal("values").(*glua.LUserData).Value.([]int)
if &got[0] != &values[0] || values[0] != 3 || values[1] != 5 {
t.Fatal("read-only binding copied or modified the slice")
}
}
func TestReadOnlyIPSlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
ips := []net.IP{{192, 0, 2, 1}, net.ParseIP("2001:db8::1")}
NewReadOnlySlicePusher[net.IP](L)(L, ips)
L.SetGlobal("ips", L.Get(-1))
L.Pop(1)
readOnly := luar.MT(L, ips).RawGetString("__newindex")
NewReadOnlySlicePusher[net.IP](L)
if luar.MT(L, ips).RawGetString("__newindex") != readOnly {
t.Fatal("IP metatables were initialized more than once")
}
if err := L.DoString(`
local function readOnly(value)
assert(not pcall(function() value[1] = value[1] end))
assert(not pcall(function() return value + value[1] end))
end
assert(#ips == 2 and ips[1]:String() == "192.0.2.1")
readOnly(ips)
for _, ip in ips() do
readOnly(ip)
readOnly(ip:To16())
if ip:To4() then
readOnly(ip:To4())
readOnly(ip:DefaultMask())
end
assert(ip:Equal(ip:To16()) and type(ip:String()) == "string")
assert(ip.AppendText == nil and ip.appendText == nil)
local text, err = ip:MarshalText()
assert(not err)
text[1] = 88 -- Independently owned byte buffers remain writable.
assert(not pcall(function() return ips + text end))
end
`); err != nil {
t.Fatal(err)
}
got := L.GetGlobal("ips").(*glua.LUserData).Value.([]net.IP)
if &got[0] != &ips[0] || &got[0][0] != &ips[0][0] || &got[1][0] != &ips[1][0] {
t.Fatal("read-only binding copied the IP data")
}
for i, want := range []string{"192.0.2.1", "2001:db8::1"} {
if got[i].String() != want {
t.Fatalf("IP %d changed to %s; want %s", i, got[i], want)
}
}
}
func TestReadOnlyIPSlicePusherLazy(t *testing.T) {
for _, tc := range []struct{ name, access string }{
{"index", `local ip = ips[1]`},
{"iterate", `local _, ip = ips()()`},
{"coroutine", `local co = coroutine.create(function() return ips[1] end); local ok, ip = coroutine.resume(co); assert(ok)`},
} {
t.Run(tc.name, func(t *testing.T) {
L := glua.NewState()
defer L.Close()
methods := 0
luar.GetConfig(L).MethodNames = func(_ reflect.Type, method reflect.Method) []string {
methods++
return []string{method.Name}
}
NewReadOnlySlicePusher[net.IP](L)(L, []net.IP{{192, 0, 2, 1}})
L.SetGlobal("ips", L.Get(-1))
L.Pop(1)
if err := L.DoString(`assert(#ips == 1); assert(not pcall(function() ips[1] = nil end))`); err != nil {
t.Fatal(err)
}
if methods != 0 {
t.Fatal("IP methods initialized without reading an element")
}
source := tc.access + `
assert(ip:String() == "192.0.2.1" and ip.AppendText == nil)
assert(not pcall(function() ip[1] = 0 end))
assert(not pcall(function() ip:To4()[1] = 0 end))
assert(not pcall(function() ip:To16()[1] = 0 end))
assert(not pcall(function() ip:DefaultMask()[1] = 0 end))
`
if err := L.DoString(source); err != nil {
t.Fatal(err)
}
initialized := methods
if initialized == 0 {
t.Fatal("IP access did not initialize its methods")
}
if err := L.DoString(source); err != nil {
t.Fatal(err)
}
if methods != initialized {
t.Fatal("IP methods initialized more than once")
}
})
}
}
func TestReadOnlyNestedSlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
values := [][][]byte{{{3, 5}}}
NewReadOnlySlicePusher[[][]byte](L)(L, values)
L.SetGlobal("values", L.Get(-1))
L.Pop(1)
ips := [][]net.IP{{{192, 0, 2, 1}}}
NewReadOnlySlicePusher[[]net.IP](L)(L, ips)
L.SetGlobal("ips", L.Get(-1))
L.Pop(1)
if err := L.DoString(`
local function readOnly(value)
assert(not pcall(function() value[1] = value[1] end))
assert(not pcall(function() return value + value[1] end))
end
assert(values[1][1][1] == 3 and values[1][1][2] == 5)
readOnly(values)
for _, group in values() do
readOnly(group)
for _, bytes in group() do readOnly(bytes) end
end
readOnly(ips)
readOnly(ips[1])
local ip = ips[1][1]
assert(ip:String() == "192.0.2.1")
readOnly(ip)
readOnly(ip:To4())
readOnly(ip:To16())
readOnly(ip:DefaultMask())
assert(ip.AppendText == nil and ip.appendText == nil)
`); err != nil {
t.Fatal(err)
}
readOnly := luar.MT(L, ips[0]).RawGetString("__newindex")
NewReadOnlySlicePusher[net.IP](L)
if luar.MT(L, ips[0]).RawGetString("__newindex") != readOnly {
t.Fatal("nested slice metatable was initialized again")
}
got := L.GetGlobal("values").(*glua.LUserData).Value.([][][]byte)
if &got[0][0][0] != &values[0][0][0] || values[0][0][0] != 3 || values[0][0][1] != 5 {
t.Fatal("read-only binding copied or modified the nested slice")
}
}
type recursiveReadOnlySlice []recursiveReadOnlySlice
func TestReadOnlyRecursiveSlicePusher(t *testing.T) {
L := glua.NewState()
defer L.Close()
values := make(recursiveReadOnlySlice, 1)
values[0] = values
NewReadOnlySlicePusher[recursiveReadOnlySlice](L)(L, values)
L.SetGlobal("values", L.Get(-1))
L.Pop(1)
if err := L.DoString(`
local nested = values[1][1][1]
assert(#nested == 1)
assert(not pcall(function() nested[1] = nested end))
assert(not pcall(function() return nested + nested end))
`); err != nil {
t.Fatal(err)
}
}
func TestSlicePusherMetatablePerState(t *testing.T) { func TestSlicePusherMetatablePerState(t *testing.T) {
first := glua.NewState() first := glua.NewState()
defer first.Close() defer first.Close()
+117
View File
@@ -0,0 +1,117 @@
package lua
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"sync"
"github.com/xtls/xray-core/common/errors"
)
var privilegedKey struct {
sync.Once
publicKey ed25519.PublicKey
}
// SetPrivilegedPublicKey configures the trusted Ed25519 public key once at
// startup, before compiling scripts. An empty value disables authorization.
// Nonempty values must be unpadded Base64URL encodings of exactly 32 bytes.
func SetPrivilegedPublicKey(value string) error {
var publicKey ed25519.PublicKey
if value != "" {
decoded, err := decodePrivilegedValue(value, ed25519.PublicKeySize)
if err != nil {
return errors.New("invalid Lua privileged public key").Base(err)
}
publicKey = ed25519.PublicKey(decoded)
}
privilegedKey.Do(func() {
privilegedKey.publicKey = publicKey
})
return nil
}
func decodePrivilegedValue(value string, size int) ([]byte, error) {
encoding := base64.RawURLEncoding.Strict()
if len(value) != encoding.EncodedLen(size) {
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
}
decoded, err := encoding.DecodeString(value)
if err != nil || len(decoded) != size || encoding.EncodeToString(decoded) != value {
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
}
return decoded, nil
}
func verifyPrivilegedSignature(code []byte) (bool, error) {
_, end, value, found := privilegedSignatureLine(code)
if !found {
return false, nil
}
publicKey := privilegedKey.publicKey
if len(publicKey) == 0 {
return false, errors.New("Lua privileged signature requires --lua-privileged-pubkey")
}
signature, err := decodePrivilegedValue(string(bytes.TrimSpace(value)), ed25519.SignatureSize)
if err != nil {
return false, errors.New("invalid Lua privileged signature").Base(err)
}
digest := sha256.Sum256(bytes.TrimSpace(code[end:]))
if !ed25519.Verify(publicKey, digest[:], signature) {
return false, errors.New("Lua privileged signature verification failed")
}
return true, nil
}
// SignPrivilegedScript signs the script body, then prepends or replaces the signature line.
func SignPrivilegedScript(code []byte, seed string) ([]byte, error) {
decoded, err := decodePrivilegedValue(seed, ed25519.SeedSize)
if err != nil {
return nil, errors.New("invalid Ed25519 private key seed").Base(err)
}
start, end, _, found := privilegedSignatureLine(code)
body := code
if found {
body = code[end:]
}
digest := sha256.Sum256(bytes.TrimSpace(body))
signature := ed25519.Sign(ed25519.NewKeyFromSeed(decoded), digest[:])
header := []byte("-- @xray:privileged-signature=" + base64.RawURLEncoding.EncodeToString(signature))
if !found {
if newline := bytes.IndexByte(code, '\n'); newline > 0 && code[newline-1] == '\r' {
header = append(header, '\r')
}
header = append(header, '\n')
return append(header, code...), nil
}
signed := make([]byte, 0, start+len(header)+len(code)-end)
signed = append(signed, code[:start]...)
signed = append(signed, header...)
return append(signed, code[end:]...), nil
}
func privilegedSignatureLine(code []byte) (start, end int, value []byte, found bool) {
for rest := code; len(rest) != 0; {
start = len(code) - len(rest)
var line []byte
line, rest, _ = bytes.Cut(rest, []byte{'\n'})
line = bytes.TrimSuffix(line, []byte{'\r'})
if len(bytes.TrimSpace(line)) == 0 {
continue
}
end = start + len(line)
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
if !found {
return 0, 0, nil, false
}
value, found = bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
return start, end, value, found
}
return 0, 0, nil, false
}
+85
View File
@@ -0,0 +1,85 @@
package lua
// TODO: Re-enable these tests and their helper when Compile supports privileged signature authorization again.
/*
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/hex"
"strings"
"testing"
)
func privilegedSeed(t *testing.T) string {
t.Helper()
seed, err := hex.DecodeString(privilegedTestSeed)
if err != nil {
t.Fatal(err)
}
return base64.RawURLEncoding.EncodeToString(seed)
}
func TestSignPrivilegedScriptPreservesSource(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
seed := privilegedSeed(t)
for _, tc := range []struct {
name, prefix, header, body, newline string
}{
{name: "unsigned", body: privilegedFixtureBody, newline: "\n"},
{name: "unsigned boundary whitespace", body: " \t\n\n" + privilegedFixtureBody + " \t\n", newline: "\n"},
{name: "unsigned CRLF", body: strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n"), newline: "\r\n"},
{name: "replace invalid header", header: privilegedSignatureHeader + "%", body: "\n" + privilegedFixtureBody, newline: "\n"},
{name: "replace indented header", prefix: " \t\r\n\n", header: " \t--\t @xray:privileged-signature=% \t", body: "\r\n \t\r\n" + privilegedFixtureBody + "\r\n", newline: "\r\n"},
{name: "replace valid header", header: privilegedSignatureHeader + privilegedFixtureSignature, body: "\n" + privilegedFixtureBody, newline: "\n"},
{name: "empty script", newline: "\n"},
{name: "header without newline", header: privilegedSignatureHeader + "%"},
{name: "ordinary comment before marker", body: "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", newline: "\n"},
} {
t.Run(tc.name, func(t *testing.T) {
code := []byte(tc.prefix + tc.header + tc.body)
original := bytes.Clone(code)
signed, err := SignPrivilegedScript(code, seed)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(code, original) {
t.Fatal("signing modified the input buffer")
}
body := tc.body
if tc.header != "" {
body = strings.TrimPrefix(body, tc.newline)
}
header := privilegedSignatureHeader + testSignature(t, body)
want := header + tc.newline + tc.body
if tc.header != "" {
want = tc.prefix + header + tc.body
}
if string(signed) != want {
t.Fatalf("signed source = %q, want %q", signed, want)
}
program, err := Compile(writeTestScript(t, string(signed)))
if err != nil || program == nil || !program.privileged {
t.Fatalf("signed source Compile = %v, %v; want privileged program", program, err)
}
again, err := SignPrivilegedScript(signed, seed)
if err != nil || !bytes.Equal(again, signed) {
t.Fatalf("resigning changed the source: %q, %v", again, err)
}
})
}
}
func TestSignPrivilegedScriptRejectsInvalidSeed(t *testing.T) {
seed := privilegedSeed(t)
for _, value := range []string{
"", "%", seed + "=",
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SeedSize-1)),
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PrivateKeySize)),
} {
if signed, err := SignPrivilegedScript([]byte(privilegedFixtureBody), value); signed != nil || err == nil {
t.Fatalf("SignPrivilegedScript with invalid seed = %q, %v; want rejection", signed, err)
}
}
}
*/
+178 -5
View File
@@ -3,11 +3,14 @@ package lua
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"os" "os"
"regexp"
"strings" "strings"
"time" "time"
"github.com/xtls/xray-core/common/base64" "github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
glua "github.com/yuin/gopher-lua" glua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse" "github.com/yuin/gopher-lua/parse"
@@ -16,6 +19,7 @@ import (
// Program holds immutable bytecode that can be run by independent LStates. // Program holds immutable bytecode that can be run by independent LStates.
type Program struct { type Program struct {
proto *glua.FunctionProto proto *glua.FunctionProto
privileged bool
} }
// LStateFactory returns a fully initialized state or nil and an error. // LStateFactory returns a fully initialized state or nil and an error.
@@ -27,10 +31,17 @@ func Compile(script string) (*Program, error) {
var code []byte var code []byte
var err error var err error
name := "[inline]" name := "[inline]"
privileged := false
if strings.HasPrefix(script, base64.GzipPrefix) || strings.HasPrefix(script, base64.Prefix) { if strings.HasPrefix(script, base64.GzipPrefix) || strings.HasPrefix(script, base64.Prefix) {
// TODO: Restore inline script support and re-enable the related tests.
return nil, errors.New("currently unavailable")
code, err = base64.Decode(script) code, err = base64.Decode(script)
} else if name, err = platform.ResolveLuaFile(script); err == nil { } else if name, err = platform.ResolveLuaFile(script); err == nil {
code, err = os.ReadFile(name) code, err = os.ReadFile(name)
// TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries.
// if err == nil {
// privileged, err = verifyPrivilegedSignature(code)
// }
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -44,14 +55,26 @@ func Compile(script string) (*Program, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Program{proto: proto}, nil return &Program{proto: proto, privileged: privileged}, nil
} }
// NewState creates a state, runs register, executes the program under ctx, and // NewState creates a state, runs register, executes the program under ctx, and
// runs validate. It removes the initialization context before returning a state // runs validate. It removes the initialization context before returning a state
// owned by the caller. // owned by the caller.
func (p *Program) NewState(ctx context.Context, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) { // Choose registrySize for the script type's expected stack usage, balancing
L := glua.NewState() // per-state memory usage against the number of states retained in a pool.
func (p *Program) NewState(ctx context.Context, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
options := glua.Options{
RegistrySize: registrySize,
RegistryMaxSize: glua.RegistrySize,
RegistryGrowStep: registrySize,
}
var L *glua.LState
if p.privileged {
L = glua.NewState(options)
} else {
L = newSandboxedState(options)
}
valid := false valid := false
defer func() { defer func() {
if !valid { if !valid {
@@ -63,6 +86,10 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
if register != nil { if register != nil {
register(L) register(L)
} }
if !p.privileged {
// Hide package internals to prevent data snooping and sandbox escape.
L.SetGlobal(glua.LoadLibName, glua.LNil)
}
L.Push(L.NewFunctionFromProto(p.proto)) L.Push(L.NewFunctionFromProto(p.proto))
// Execute the Lua script's top level. // Execute the Lua script's top level.
if err := L.PCall(0, 0, nil); err != nil { if err := L.PCall(0, 0, nil); err != nil {
@@ -77,11 +104,157 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
return L, nil return L, nil
} }
func newSandboxedState(options glua.Options) *glua.LState {
options.SkipOpenLibs = true
L := glua.NewState(options)
for _, lib := range []struct {
name string
open glua.LGFunction
}{
{glua.LoadLibName, glua.OpenPackage},
{glua.BaseLibName, glua.OpenBase},
{glua.TabLibName, glua.OpenTable},
{glua.StringLibName, glua.OpenString},
{glua.MathLibName, glua.OpenMath},
{glua.OsLibName, glua.OpenOs},
} {
L.Push(L.NewFunction(lib.open))
L.Push(glua.LString(lib.name))
L.Call(1, 0)
}
for _, name := range []string{
"dofile", "loadfile", "load", "loadstring", "getfenv", "setfenv",
"getmetatable", "setmetatable", "rawget", "rawset",
"collectgarbage", "module", "newproxy", "print", "_printregs",
} {
L.SetGlobal(name, glua.LNil)
}
// Prevent printing memory addresses that reveal the memory layout.
toString := func(value glua.LValue) glua.LString {
result := "<opaque>"
switch value.Type() {
case glua.LTNil, glua.LTBool, glua.LTNumber, glua.LTString:
result = value.String()
case glua.LTUserData:
switch native := value.(*glua.LUserData).Value.(type) {
case error: // Errors can also implement fmt.Stringer; redact them first.
result = redactMemoryAddresses(native.Error())
case fmt.Stringer:
result = native.String()
}
}
return glua.LString(result)
}
L.SetGlobal("tostring", L.NewFunction(func(L *glua.LState) int {
L.Push(toString(L.CheckAny(1)))
return 1
}))
// Prevent printing memory addresses that reveal the memory layout.
stringLib := L.GetGlobal(glua.StringLibName).(*glua.LTable)
stringLib.RawSetString("dump", glua.LNil)
format := stringLib.RawGetString("format")
stringLib.RawSetString("format", L.NewFunction(func(L *glua.LState) int {
nargs := L.GetTop()
for i := 2; i <= nargs; i++ {
value := L.Get(i)
switch value.Type() {
case glua.LTBool, glua.LTNumber, glua.LTString:
continue
}
L.Replace(i, toString(value))
}
L.Insert(format, 1)
L.Call(nargs, 1)
return 1
}))
// Redact addresses before caught errors become readable by the script.
redactError := func(value glua.LValue) glua.LValue {
if message, ok := value.(glua.LString); ok {
if redacted := redactMemoryAddresses(string(message)); redacted != string(message) {
return glua.LString(redacted)
}
}
return value
}
for _, name := range []string{"pcall", "xpcall"} {
call := L.GetGlobal(name)
L.SetGlobal(name, L.NewFunction(func(L *glua.LState) int {
nargs := L.GetTop()
if name == "xpcall" {
L.CheckFunction(1)
handler := L.CheckFunction(2)
L.Replace(2, L.NewFunction(func(L *glua.LState) int {
L.Replace(1, redactError(L.Get(1)))
L.Insert(handler, 1)
L.Call(1, 1)
return 1
}))
}
L.Insert(call, 1)
L.Call(nargs, glua.MultRet)
if L.Get(1) == glua.LFalse {
L.Replace(2, redactError(L.Get(2)))
}
return L.GetTop()
}))
}
// Prevent seed output from compromising cryptographic security.
L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil)
// Keep only time functions, including when os is accessed through require.
osLib := L.GetGlobal(glua.OsLibName).(*glua.LTable)
for key, _ := osLib.Next(glua.LNil); key != glua.LNil; key, _ = osLib.Next(key) {
switch key {
case glua.LString("clock"), glua.LString("date"), glua.LString("difftime"), glua.LString("time"):
default:
osLib.RawSet(key, glua.LNil)
}
}
// Prevent module injection through the global package table.
module := L.GetGlobal(glua.LoadLibName).(*glua.LTable)
preload := module.RawGetString("preload").(*glua.LTable)
loaders := L.CreateTable(1, 0)
loaders.RawSetInt(1, L.NewFunction(func(L *glua.LState) int {
name := L.CheckString(1)
loader := preload.RawGetString(name)
if loader == glua.LNil {
loader = glua.LString("no preloaded module: " + name)
}
L.Push(loader)
return 1
}))
module.RawSetString("loaders", loaders)
L.SetField(L.Get(glua.RegistryIndex), "_LOADERS", loaders)
for _, name := range []string{"loadlib", "seeall", "path", "cpath", "config"} {
module.RawSetString(name, glua.LNil)
}
return L
}
var memoryAddressPattern = regexp.MustCompile(`0[xX][0-9a-fA-F]+`)
func redactMemoryAddresses(message string) string {
if !strings.Contains(message, "0x") && !strings.Contains(message, "0X") {
return message
}
return memoryAddressPattern.ReplaceAllString(message, "<address>")
}
// NewStateFactory returns a factory that gives each state an initialization timeout. // NewStateFactory returns a factory that gives each state an initialization timeout.
func (p *Program) NewStateFactory(initTimeout time.Duration, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory { // Choose registrySize for the script type's expected stack usage, balancing
// per-state memory usage against the number of states retained in a pool.
func (p *Program) NewStateFactory(initTimeout time.Duration, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
return func(ctx context.Context) (*glua.LState, error) { return func(ctx context.Context) (*glua.LState, error) {
initCtx, cancel := context.WithTimeout(ctx, initTimeout) initCtx, cancel := context.WithTimeout(ctx, initTimeout)
defer cancel() defer cancel()
return p.NewState(initCtx, register, validate) return p.NewState(initCtx, registrySize, register, validate)
} }
} }
+1001 -15
View File
File diff suppressed because it is too large Load Diff
+33 -16
View File
@@ -26,20 +26,20 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
var procFile string var procFiles []string
switch network { switch network {
case "tcp": case "tcp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFile = "/proc/net/tcp" procFiles = []string{"/proc/net/tcp"}
} else { } else {
procFile = "/proc/net/tcp6" procFiles = []string{"/proc/net/tcp6"}
} }
case "udp": case "udp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFile = "/proc/net/udp" procFiles = []string{"/proc/net/udp", "/proc/net/udp6"}
} else { } else {
procFile = "/proc/net/udp6" procFiles = []string{"/proc/net/udp6", "/proc/net/udp"}
} }
default: default:
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
@@ -50,12 +50,17 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return 0, "", "", errors.New("failed to format address: ", err) return 0, "", "", errors.New("failed to format address: ", err)
} }
inode, err := findInodeInFile(procFile, targetHexAddr) portFallback := ""
if network == "udp" {
portFallback = fmt.Sprintf(":%04X", srcPort)
}
inode, err := findInodeInFiles(procFiles, targetHexAddr, portFallback)
if err != nil { if err != nil {
return 0, "", "", errors.New("could not search in ", procFile).Base(err) return 0, "", "", errors.New("could not search in ", strings.Join(procFiles, " and ")).Base(err)
} }
if inode == "" { if inode == "" {
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile) return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", strings.Join(procFiles, " or "))
} }
pidStr, err := findPidByInode(inode) pidStr, err := findPidByInode(inode)
@@ -102,31 +107,43 @@ func formatLittleEndianString(addr net.IP, port Port) (string, error) {
return fmt.Sprintf("%s:%s", ipHex, portHex), nil return fmt.Sprintf("%s:%s", ipHex, portHex), nil
} }
func findInodeInFile(filePath, targetHexAddr string) (string, error) { func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) {
onlyMatchedPort := ""
for i, filePath := range filePaths {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if i > 0 && os.IsNotExist(err) {
continue
}
return "", err return "", err
} }
defer file.Close() defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() fields := strings.Fields(scanner.Text())
fields := strings.Fields(line)
if len(fields) < 10 { if len(fields) < 10 {
continue continue
} }
localAddress := fields[1] localAddress := fields[1]
if localAddress == targetHexAddr { if localAddress == targetHexAddr {
inode := fields[9] return fields[9], nil
return inode, nil }
if portFallback != "" && strings.HasSuffix(localAddress, portFallback) {
wildcardIP := strings.TrimSuffix(localAddress, portFallback)
if (len(wildcardIP) == 8 || len(wildcardIP) == 32) && strings.Trim(wildcardIP, "0") == "" {
onlyMatchedPort = fields[9]
}
}
}
if err := scanner.Err(); err != nil {
return "", err
} }
} }
return "", scanner.Err() return onlyMatchedPort, nil
} }
func findPidByInode(inode string) (string, error) { func findPidByInode(inode string) (string, error) {
+5 -4
View File
@@ -34,12 +34,13 @@ func ReadAsset(file string) ([]byte, error) {
return ReadFile(path) return ReadFile(path)
} }
func OpenAsset(file string) (io.ReadCloser, error) { func OpenAsset(file string) (io.ReadCloser, int64, error) {
path, _, err := getAssetFileLocation(file) path, info, err := getAssetFileLocation(file)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
return NewFileReader(path) r, err := NewFileReader(path)
return r, info.Size(), err
} }
func StatAsset(file string) (os.FileInfo, error) { func StatAsset(file string) (os.FileInfo, error) {
+17 -2
View File
@@ -93,7 +93,7 @@ func GetConfDirPath() string {
return configPath return configPath
} }
// ResolveLuaFile finds a local Lua script and returns its absolute path. // ResolveLuaFile finds a local .lua script and returns its resolved absolute path.
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. // Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
func ResolveLuaFile(path string) (string, error) { func ResolveLuaFile(path string) (string, error) {
if path == "" { if path == "" {
@@ -113,7 +113,22 @@ func ResolveLuaFile(path string) (string, error) {
} }
} }
} }
return resolveFile(paths) path, err := resolveFile(paths)
if err != nil {
return "", err
}
return resolveLuaTarget(path)
}
func resolveLuaTarget(path string) (string, error) {
path, err := filepath.EvalSymlinks(path)
if err != nil {
return "", fmt.Errorf("failed to resolve Lua file symlinks: %w", err)
}
if filepath.Ext(path) != ".lua" {
return "", errors.New("Lua file extension must be .lua")
}
return path, nil
} }
func resolveFile(paths []string) (string, error) { func resolveFile(paths []string) (string, error) {
+37
View File
@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -88,6 +89,10 @@ func TestResolveLuaFile(t *testing.T) {
for _, path := range paths[:3] { for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600)) common.Must(os.WriteFile(path, nil, 0o600))
} }
for i, path := range paths {
paths[i], err = filepath.EvalSymlinks(path)
common.Must(err)
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] { if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2]) t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
} }
@@ -115,3 +120,35 @@ func TestResolveLuaFile(t *testing.T) {
} }
} }
} }
func TestResolveLuaFileSymlink(t *testing.T) {
for _, tc := range []struct {
link, target string
valid bool
}{
{"alias.lua", "data.dat", false},
{"alias.dat", "script.lua", true},
} {
t.Run(tc.link, func(t *testing.T) {
dir, err := filepath.EvalSymlinks(t.TempDir())
common.Must(err)
target := filepath.Join(dir, tc.target)
common.Must(os.WriteFile(target, nil, 0o600))
link := filepath.Join(dir, tc.link)
if err := os.Symlink(tc.target, link); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("cannot create symlinks: %v", err)
}
t.Fatal(err)
}
got, err := ResolveLuaFile(link)
if tc.valid {
if err != nil || got != target {
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target)
}
} else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err)
}
})
}
}
+6 -2
View File
@@ -175,9 +175,13 @@ func init() {
switch v := input.(type) { switch v := input.(type) {
case cmdarg.Arg: case cmdarg.Arg:
r, err := confloader.LoadConfig(v[0]) r, err := confloader.LoadConfig(v[0])
common.Must(err) if err != nil {
return nil, err
}
data, err := buf.ReadAllToBytes(r) data, err := buf.ReadAllToBytes(r)
common.Must(err) if err != nil {
return nil, err
}
return loadProtobufConfig(data) return loadProtobufConfig(data)
case io.Reader: case io.Reader:
data, err := buf.ReadAllToBytes(v) data, err := buf.ReadAllToBytes(v)
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 10 Version_y byte = 10
Version_z byte = 10 Version_z byte = 17
) )
var ( var (
+10 -13
View File
@@ -4,7 +4,6 @@ go 1.27
require ( require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
@@ -20,21 +19,20 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
github.com/yuin/gopher-lua v1.1.2 github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.58.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.60.0 golang.org/x/net v0.61.0
golang.org/x/sync v0.23.0 golang.org/x/sync v0.24.0
golang.org/x/sys v0.48.0 golang.org/x/sys v0.49.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1 golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0 google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12 google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3 h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11 layeh.com/gopher-luar v1.0.11
lukechampine.com/blake3 v1.4.1 lukechampine.com/blake3 v1.4.1
@@ -42,13 +40,12 @@ require (
) )
require ( require (
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.2.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gopacket v1.1.19 // indirect github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.20.0 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect github.com/pion/dtls/v3 v3.1.5 // indirect
@@ -58,9 +55,9 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.42.0 // indirect golang.org/x/text v0.43.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.51.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
) )
+20 -20
View File
@@ -1,12 +1,10 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= 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= github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
@@ -33,8 +31,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/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 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
@@ -81,8 +79,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 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 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws= github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= 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/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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 v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= 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= 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-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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
@@ -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-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-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.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.24.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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 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/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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+137 -7
View File
@@ -9,12 +9,15 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"net/url" "net/url"
"os"
"regexp" "regexp"
"strings" "strings"
googleuuid "github.com/google/uuid" googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors" "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/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/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -28,6 +31,7 @@ import (
"github.com/xtls/xray-core/transport/internet/finalmask/xdns" "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/xicmp"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc" "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" "github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -306,9 +310,24 @@ type NoiseItem struct {
type NoiseMask struct { type NoiseMask struct {
Reset Int32Range `json:"reset"` Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"` Noise []NoiseItem `json:"noise"`
Script string `json:"script"`
} }
func (c *NoiseMask) Build() (proto.Message, error) { 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)) noiseSlice := make([]*noise.Item, 0, len(c.Noise))
for _, item := range c.Noise { for _, item := range c.Noise {
if len(item.Packet) > 0 && item.Rand.To > 0 { if len(item.Packet) > 0 && item.Rand.To > 0 {
@@ -800,6 +819,9 @@ type XDNSDomain struct {
type XDNSResolver struct { type XDNSResolver struct {
Addrs []string `json:"addrs"` Addrs []string `json:"addrs"`
Worker int32 `json:"worker"`
TlsConfig *TLSConfig `json:"tlsConfig"`
QuicConfig *QuicParamsConfig `json:"quicConfig"`
} }
type XDNS struct { type XDNS struct {
@@ -834,6 +856,26 @@ func (c *XDNS) Build() (proto.Message, error) {
} }
} }
for i := range c.Resolvers { 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 { for j := range c.Resolvers[i].Addrs {
var u *url.URL var u *url.URL
var e error var e error
@@ -845,18 +887,21 @@ func (c *XDNS) Build() (proto.Message, error) {
if e != nil { if e != nil {
return nil, e return nil, e
} }
var addr string
switch u.Scheme { switch u.Scheme {
case "tcp", "udp": case "tcp", "udp":
default: host := u.Hostname()
return nil, errors.New("invalid protocol") port := u.Port()
}
var host, port string
host = u.Hostname()
port = u.Port()
if port == "" { if port == "" {
port = "53" port = "53"
} }
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: net.JoinHostPort(host, port)}) 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})
} }
} }
if c.ExtraPoll < 0 || c.ExtraPoll > 3 { if c.ExtraPoll < 0 || c.ExtraPoll > 3 {
@@ -1149,6 +1194,91 @@ type QuicParamsConfig struct {
DisableStatelessReset bool `json:"disableStatelessReset"` 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 { type FinalMask struct {
Tcp []Mask `json:"tcp"` Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"` Udp []Mask `json:"udp"`
@@ -2,6 +2,9 @@ package conf
import ( import (
"encoding/json" "encoding/json"
"os"
"path/filepath"
"strconv"
"testing" "testing"
"github.com/xtls/xray-core/transport/internet/finalmask/noise" "github.com/xtls/xray-core/transport/internet/finalmask/noise"
@@ -134,3 +137,68 @@ func TestNoiseExpFromJSON(t *testing.T) {
t.Errorf("item 3 = %+v", items[3]) 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")
}
})
}
}
+18 -85
View File
@@ -1,13 +1,15 @@
package conf package conf
import ( import (
"os" "context"
"slices"
"strings" "strings"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/tls"
) )
type TransportProtocol string type TransportProtocol string
@@ -102,6 +104,14 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
tm := serial.ToTypedMessage(ts) tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm) config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type config.SecurityType = tm.Type
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": case "reality":
if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" { if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" {
return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.") return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.")
@@ -228,102 +238,25 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
if c.FinalMask != nil { if c.FinalMask != nil {
for _, mask := range c.FinalMask.Tcp { for _, mask := range c.FinalMask.Tcp {
u, err := mask.Build(true) proto, err := mask.Build(true)
if err != nil { if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
} }
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u)) config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(proto))
} }
for _, mask := range c.FinalMask.Udp { for _, mask := range c.FinalMask.Udp {
u, err := mask.Build(false) proto, err := mask.Build(false)
if err != nil { if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
} }
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u)) config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(proto))
} }
if c.FinalMask.QuicParams != nil { if c.FinalMask.QuicParams != nil {
profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile) proto, err := c.FinalMask.QuicParams.Build()
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 { if err != nil {
return nil, err return nil, err
} }
down, err := c.FinalMask.QuicParams.BrutalDown.Bps() config.QuicParams = proto.(*internet.QuicParams)
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,
}
} }
} }
+5 -1
View File
@@ -1004,6 +1004,10 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
switch c.Service { switch c.Service {
case "local": case "local":
case "Google Drive": case "Google Drive":
errors.PrintDeprecatedFeatureWarning(`"service": "Google Drive"`, `"service": "google-drive"`)
c.Service = "google-drive"
fallthrough
case "google-drive":
if len(c.Secrets) != 3 { if len(c.Secrets) != 3 {
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken") return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
} }
@@ -1012,7 +1016,7 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
return nil, errors.New(`service "template" needs a "template" object`) return nil, errors.New(`service "template" needs a "template" object`)
} }
default: default:
return nil, errors.New("unsupported service") return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`)
} }
config := &xdrive.Config{ config := &xdrive.Config{
RemoteFolder: c.RemoteFolder, RemoteFolder: c.RemoteFolder,
+150 -1
View File
@@ -5,9 +5,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/xtls/xray-core/common/log"
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/xdrive"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -321,8 +323,25 @@ func TestXDriveRejectsUnknownService(t *testing.T) {
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err) 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 { if _, err := config.Build(); err == nil {
t.Fatal("Build accepted an unsupported service") t.Errorf("%s: accepted two secrets", service)
}
} }
} }
@@ -355,6 +374,56 @@ 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) { func TestXDriveTemplateNeedsTemplate(t *testing.T) {
config := new(XDriveConfig) config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
@@ -364,3 +433,83 @@ func TestXDriveTemplateNeedsTemplate(t *testing.T) {
t.Fatal("Build accepted a template service without a template") 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,7 +17,11 @@ import (
core "github.com/xtls/xray-core/core" core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/freedom" "github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/proxy/masque" "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" "github.com/xtls/xray-core/transport/internet"
"google.golang.org/protobuf/proto"
) )
var ( var (
@@ -209,6 +213,9 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
if _, ok := ts.(*masque.ServerConfig); !ok && receiverSettings.StreamSettings != nil && receiverSettings.StreamSettings.ProtocolName == "masque" { 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") 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{ return &core.InboundHandlerConfig{
Tag: c.Tag, Tag: c.Tag,
@@ -262,6 +269,54 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
return nil 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. // Build implements Buildable.
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.ProxySettings != nil { if c.ProxySettings != nil {
@@ -340,6 +395,9 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if err != nil { if err != nil {
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err) 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 { if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err return nil, err
} }
+4
View File
@@ -4,6 +4,8 @@ import (
"github.com/xtls/xray-core/main/commands/all/api" "github.com/xtls/xray-core/main/commands/all/api"
"github.com/xtls/xray-core/main/commands/all/base64" "github.com/xtls/xray-core/main/commands/all/base64"
"github.com/xtls/xray-core/main/commands/all/convert" "github.com/xtls/xray-core/main/commands/all/convert"
// TODO: Restore this import and lua.CmdLua when Lua privileged signature authorization is re-enabled.
// "github.com/xtls/xray-core/main/commands/all/lua"
"github.com/xtls/xray-core/main/commands/all/tls" "github.com/xtls/xray-core/main/commands/all/tls"
"github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/commands/base"
) )
@@ -14,9 +16,11 @@ func init() {
api.CmdAPI, api.CmdAPI,
base64.CmdBase64, base64.CmdBase64,
convert.CmdConvert, convert.CmdConvert,
// lua.CmdLua,
tls.CmdTLS, tls.CmdTLS,
cmdUUID, cmdUUID,
cmdX25519, cmdX25519,
// cmdEd25519,
cmdWG, cmdWG,
cmdMLDSA65, cmdMLDSA65,
cmdMLKEM768, cmdMLKEM768,
+53
View File
@@ -0,0 +1,53 @@
package all
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdEd25519 = &base.Command{
UsageLine: `{{.Exec}} ed25519 [-i "private key seed (Base64URL)"]`,
Short: "Generate an Ed25519 key pair",
Long: `
Generate an Ed25519 key pair, or restore it from a 32-byte private key seed
with -i. PrivateKey (seed) and PublicKey use unpadded Base64URL.
`,
}
var inputEd25519 = cmdEd25519.Flag.String("i", "", "Ed25519 private key seed in unpadded Base64URL.")
func init() {
cmdEd25519.Run = executeEd25519
}
func executeEd25519(cmd *base.Command, args []string) {
if len(args) != 0 {
cmd.Usage()
}
var privateKey ed25519.PrivateKey
if *inputEd25519 == "" {
_, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
base.Fatalf("failed to generate Ed25519 key: %s", err)
}
privateKey = key
} else {
encoding := base64.RawURLEncoding.Strict()
seed, err := encoding.DecodeString(*inputEd25519)
if err != nil || len(seed) != ed25519.SeedSize || encoding.EncodeToString(seed) != *inputEd25519 {
base.Fatalf("invalid Ed25519 private key seed: expected 32 bytes in unpadded Base64URL")
}
privateKey = ed25519.NewKeyFromSeed(seed)
}
if _, err := fmt.Printf("PrivateKey: %s\nPublicKey: %s\n",
base64.RawURLEncoding.EncodeToString(privateKey.Seed()),
base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))); err != nil {
base.Fatalf("failed to write Ed25519 keys: %s", err)
}
}
+13
View File
@@ -0,0 +1,13 @@
package lua
import "github.com/xtls/xray-core/main/commands/base"
// CmdLua holds the Lua script commands.
var CmdLua = &base.Command{
UsageLine: "{{.Exec}} lua",
Short: "Manage Lua scripts",
Long: "{{.Exec}} {{.LongName}} provides tools for local Lua scripts.",
Commands: []*base.Command{
cmdSign,
},
}
+98
View File
@@ -0,0 +1,98 @@
package lua
import (
"os"
"path/filepath"
xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdSign = &base.Command{
UsageLine: "{{.Exec}} lua sign [-key seed] [-o output-file] <input-file>",
Short: "Sign a local Lua script for privileged execution",
Long: `
Sign a local Lua file using a 32-byte Ed25519 private key seed in unpadded
Base64URL. Use {{.Exec}} ed25519 to generate a key pair.
Write back to the input file, or use -o to write another file (- for stdout).
`,
}
var (
signKey = cmdSign.Flag.String("key", "", "Ed25519 private key seed in unpadded Base64URL.")
signOutput = cmdSign.Flag.String("o", "", "Output file, or - for stdout. Default: input file.")
)
func init() {
cmdSign.Run = executeSign
}
func executeSign(cmd *base.Command, args []string) {
if len(args) != 1 {
cmd.Usage()
}
info, err := os.Stat(args[0])
if err != nil {
base.Fatalf("failed to inspect Lua file: %s", err)
}
if !info.Mode().IsRegular() {
base.Fatalf("Lua input is not a regular file: %s", args[0])
}
code, err := os.ReadFile(args[0])
if err != nil {
base.Fatalf("failed to read Lua file: %s", err)
}
signed, err := xlua.SignPrivilegedScript(code, *signKey)
if err != nil {
base.Fatalf("failed to sign Lua file: %s", err)
}
if *signOutput == "-" {
if _, err := os.Stdout.Write(signed); err != nil {
base.Fatalf("failed to write signed Lua file: %s", err)
}
return
}
output := *signOutput
if output == "" {
output = args[0]
}
if err := writeSignedFile(output, signed, info.Mode().Perm()); err != nil {
base.Fatalf("failed to write signed Lua file: %s", err)
}
}
func writeSignedFile(path string, code []byte, mode os.FileMode) error {
if info, err := os.Stat(path); err == nil {
if !info.Mode().IsRegular() {
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
}
mode = info.Mode().Perm()
path, err = filepath.EvalSymlinks(path)
if err != nil {
return err
}
} else if !os.IsNotExist(err) {
return err
}
file, err := os.CreateTemp(filepath.Dir(path), ".xray-lua-sign-*")
if err != nil {
return err
}
defer os.Remove(file.Name())
defer file.Close()
if _, err := file.Write(code); err != nil {
return err
}
if err := file.Chmod(mode); err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
return os.Rename(file.Name(), path)
}
+3 -3
View File
@@ -1,11 +1,11 @@
package all package all
import ( import (
"crypto/mldsa"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/commands/base"
) )
@@ -39,8 +39,8 @@ func executeMLDSA65(cmd *base.Command, args []string) {
} else { } else {
rand.Read(seed[:]) rand.Read(seed[:])
} }
pub, _ := mldsa65.NewKeyFromSeed(&seed) key, _ := mldsa.NewPrivateKey(mldsa.MLDSA65(), seed[:])
fmt.Printf("Seed: %v\nVerify: %v\n", fmt.Printf("Seed: %v\nVerify: %v\n",
base64.RawURLEncoding.EncodeToString(seed[:]), base64.RawURLEncoding.EncodeToString(seed[:]),
base64.RawURLEncoding.EncodeToString(pub.Bytes())) base64.RawURLEncoding.EncodeToString(key.PublicKey().Bytes()))
} }
+2
View File
@@ -4,6 +4,8 @@ package base
type CommandEnvHolder struct { type CommandEnvHolder struct {
// Executable name of current binary // Executable name of current binary
Exec string Exec string
// Full name of the command being executed
CommandName string
// commands column width of current command // commands column width of current command
CommandsWidth int CommandsWidth int
} }
+1
View File
@@ -61,6 +61,7 @@ BigCmdLoop:
} }
buildCommandText(cmd) buildCommandText(cmd)
CommandEnv.CommandName = cmdName
cmd.Run(cmd, args) cmd.Run(cmd, args)
Exit() Exit()
return return
+35 -1
View File
@@ -8,12 +8,14 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/main/commands/base"
"github.com/xtls/xray-core/main/confloader" "github.com/xtls/xray-core/main/confloader"
) )
@@ -31,7 +33,12 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
default: default:
data, err = os.ReadFile(arg) file, openErr := os.Open(arg)
if openErr != nil {
return nil, openErr
}
defer file.Close()
data, err = readConfigFile(file)
} }
if err != nil { if err != nil {
@@ -41,6 +48,33 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
return return
} }
func readConfigFile(file *os.File) ([]byte, error) {
if base.CommandEnv.CommandName != "run" {
return io.ReadAll(file)
}
info, err := file.Stat()
if err != nil {
return nil, err
}
if info.Mode().IsRegular() {
path, err := filepath.Abs(file.Name())
if err != nil {
return nil, err
}
path, err = filepath.EvalSymlinks(path)
if err != nil {
return nil, err
}
switch strings.ToLower(filepath.Ext(path)) {
case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf":
default:
return nil, errors.New("unsupported config file extension: ", path)
}
}
return io.ReadAll(file)
}
// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL // FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL
// or a Unix socket HTTP endpoint. // or a Unix socket HTTP endpoint.
// //
+10 -4
View File
@@ -36,10 +36,6 @@ The -confdir=dir flag sets a dir with multiple json config
The -format=json flag sets the format of config files. The -format=json flag sets the format of config files.
Default "auto". Default "auto".
The config root env object sets process environment variables after all config
files are parsed. Variables needed to locate or parse config files must be set
in the process environment before Xray starts.
The -test flag tells Xray to test config files only, The -test flag tells Xray to test config files only,
without launching the server. without launching the server.
@@ -47,6 +43,9 @@ The -dump flag tells Xray to print the merged config.
`, `,
} }
// The --lua-privileged-pubkey=key flag sets the trusted Ed25519 public key
// for privileged local Lua scripts, encoded as unpadded Base64URL.
func init() { func init() {
cmdRun.Run = executeRun // break init loop cmdRun.Run = executeRun // break init loop
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
@@ -58,6 +57,8 @@ var (
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.") dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.") test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
format = cmdRun.Flag.String("format", "auto", "Format of input file.") format = cmdRun.Flag.String("format", "auto", "Format of input file.")
// TODO: Restore this flag and its help text when Lua privileged signature authorization is re-enabled.
// luaPrivilegedPublicKey = cmdRun.Flag.String("lua-privileged-pubkey", "", "Unpadded Base64URL Ed25519 public key for privileged local Lua scripts.")
/* We have to do this here because Golang's Test will also need to parse flag, before /* We have to do this here because Golang's Test will also need to parse flag, before
* main func in this file is run. * main func in this file is run.
@@ -214,6 +215,11 @@ func getConfigFormat() string {
} }
func startXray() (core.Server, error) { func startXray() (core.Server, error) {
// TODO: Restore public key initialization when Lua privileged signature authorization is re-enabled.
// if err := lua.SetPrivilegedPublicKey(*luaPrivilegedPublicKey); err != nil {
// return nil, errors.New("failed to configure Lua privileged authorization").Base(err)
// }
configFiles := getConfigFilePath(true) configFiles := getConfigFilePath(true)
c, err := core.LoadConfig(getConfigFormat(), configFiles) c, err := core.LoadConfig(getConfigFormat(), configFiles)
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"context"
"flag"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func TestRunConfigCommands(t *testing.T) {
if os.Getenv("XRAY_TEST_CONFIG_COMMAND") == "1" {
os.Args = append([]string{os.Args[0]}, flag.Args()...)
main()
return
}
dir := t.TempDir()
config := filepath.Join(dir, "config.yaml")
script := filepath.Join(dir, "config.lua")
binary := filepath.Join(dir, "config.pb")
message := filepath.Join(dir, "user.tmsg")
for path, body := range map[string]string{
config: "{}",
script: "{}",
binary: "",
message: `{"type":"xray.app.dispatcher.Config","value":""}`,
} {
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
file, err := os.Open(script)
if err != nil {
t.Fatal(err)
}
defer file.Close()
for _, tc := range []struct {
name string
args []string
code int
input io.Reader
}{
{"format override", []string{"run", "-test", "-format", "json", "-c", config}, 0, nil},
{"invalid extension", []string{"run", "-test", "-format", "json", "-c", script}, 23, nil},
{"default command", []string{"-test", "-format", "json", "-c", script}, 23, nil},
{"protobuf config", []string{"run", "-test", "-c", binary}, 0, nil},
{"protobuf error", []string{"run", "-test", "-format", "protobuf", "-c", script}, 23, nil},
{"merged source", []string{"run", "-test", "-format", "json", "-c", config, "-c", script}, 23, nil},
{"dump", []string{"run", "-dump", "-c", config}, 0, nil},
{"stdin pipe", []string{"run", "-test", "-c", "stdin:"}, 0, nil},
{"stdin file", []string{"run", "-test", "-c", "stdin:"}, 0, file},
{"convert json", []string{"convert", "json", message}, 0, nil},
{"convert pb", []string{"convert", "pb", "-debug", config}, 0, nil},
{"convert pb stdin", []string{"convert", "pb", "-debug", "stdin:"}, 0, file},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
args := append([]string{"-test.run=^TestRunConfigCommands$", "--"}, tc.args...)
cmd := exec.CommandContext(ctx, os.Args[0], args...)
cmd.Env = append(os.Environ(), "XRAY_TEST_CONFIG_COMMAND=1")
cmd.Stdin = strings.NewReader("{}")
if tc.input != nil {
if _, err := file.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
cmd.Stdin = tc.input
}
output, err := cmd.CombinedOutput()
if cmd.ProcessState == nil {
t.Fatal(err)
}
if code := cmd.ProcessState.ExitCode(); code != tc.code {
t.Fatalf("exit code = %d, want %d: %v\n%s", code, tc.code, err, output)
}
if tc.code == 23 && !strings.Contains(string(output), "failed to load config files") {
t.Fatalf("missing config error:\n%s", output)
}
})
}
}
+1 -1
View File
@@ -327,7 +327,7 @@ func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr,
dns = remoteDNS dns = remoteDNS
} }
dev, tnet, _, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, true) dev, tnet, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+13 -9
View File
@@ -22,6 +22,7 @@ import (
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing" "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/proxy/wireguard"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
@@ -166,17 +167,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if mtu == 0 { if mtu == 0 {
mtu = masque.MinPacketSize mtu = masque.MinPacketSize
} }
dev, _, gstack, err := wireguard.CreateNetTUN(local, nil, mtu, false)
if err != nil {
return nil, err
}
s := &Server{ s := &Server{
validator: users, validator: users,
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher), dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
ctx: core.ToBackgroundDetachedContext(ctx), ctx: core.ToBackgroundDetachedContext(ctx),
mtu: mtu, mtu: mtu,
dev: dev,
pools: pools, pools: pools,
local: local, local: local,
tunnels: make(map[netip.Addr]*serverTunnel), tunnels: make(map[netip.Addr]*serverTunnel),
@@ -187,7 +182,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if content := session.ContentFromContext(ctx); content != nil { if content := session.ContentFromContext(ctx); content != nil {
s.sniffing = content.SniffingRequest s.sniffing = content.SniffingRequest
} }
wireguard.CreateForwarder(gstack, s.handleConnection) dev, _, err := wireguard.CreateNetTUN(local, nil, mtu, s)
if err != nil {
return nil, err
}
s.dev = dev
return s, nil return s, nil
} }
@@ -500,7 +499,8 @@ func packetDestination(packet []byte) (netip.Addr, bool) {
return netip.Addr{}, false return netip.Addr{}, false
} }
func (s *Server) handleConnection(conn net.Conn, dest net.Destination) { // HandleConnection implements tun.ConnectionHandler
func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
defer conn.Close() defer conn.Close()
source := net.DestinationFromAddr(conn.RemoteAddr()) source := net.DestinationFromAddr(conn.RemoteAddr())
addr, _ := netip.AddrFromSlice(source.Address.IP()) addr, _ := netip.AddrFromSlice(source.Address.IP())
@@ -534,9 +534,13 @@ func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
}) })
errors.LogInfo(ctx, "processing from ", source, " to ", dest) 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{ link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: buf.NewWriter(conn), Writer: writer,
} }
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil { if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err)) errors.LogError(ctx, errors.New("connection closed").Base(err))
+57
View File
@@ -7,13 +7,17 @@ import (
"net/netip" "net/netip"
"os" "os"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
) )
@@ -326,3 +330,56 @@ func TestPacketDestination(t *testing.T) {
require.False(t, ok) 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)
}
}
+9 -6
View File
@@ -180,14 +180,14 @@ type VisionReader struct {
isUplink bool isUplink bool
conn net.Conn conn net.Conn
input *bytes.Reader input *bytes.Reader
rawInput *bytes.Buffer rawInput **bytes.Buffer
ob *session.Outbound ob *session.Outbound
// internal // internal
directReadCounter stats.Counter 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{ return &VisionReader{
Reader: reader, Reader: reader,
trafficState: trafficState, trafficState: trafficState,
@@ -261,12 +261,15 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() { if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, inputBuffer) 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 = bytes.Reader{} // release memory
w.input = nil w.input = nil
*w.rawInput = bytes.Buffer{} // release memory
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 = nil w.rawInput = nil
if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil { if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil {
+7 -1
View File
@@ -169,12 +169,18 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
} }
sessionItem.Unlock() sessionItem.Unlock()
} }
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) { 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) return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
}) })
if err != nil { if err != nil {
continue continue
} }
}
payloadBuf := buf.New() payloadBuf := buf.New()
payloadBuf.Write(decoded.Payload) payloadBuf.Write(decoded.Payload)
+7 -1
View File
@@ -336,12 +336,18 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
sessionItem.Unlock() sessionItem.Unlock()
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) { 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) return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
}) })
if err != nil { if err != nil {
continue continue
} }
}
pBuf := buf.New() pBuf := buf.New()
pBuf.Write(decoded.Payload) pBuf.Write(decoded.Payload)
+7 -1
View File
@@ -253,11 +253,17 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
} }
sessionItem.Unlock() sessionItem.Unlock()
} }
link, err := sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil) 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 { if err != nil {
b.Release() b.Release()
continue continue
} }
}
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b}) _ = 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)) return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
} }
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
return errors.New("connection ends").Base(err) 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 return nil
} }
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
return errors.New("connection ends").Base(err) 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 return DecodedUDPPacket{}, ErrBadHeaderType
} }
epoch := binary.BigEndian.Uint64(bodyPlain[1:9]) epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch)))) diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
if diff > 30 { if diff > 30 {
return DecodedUDPPacket{}, ErrBadTimestamp return DecodedUDPPacket{}, ErrBadTimestamp
} }
+2 -2
View File
@@ -49,12 +49,12 @@ func (f *SlidingWindow) Add(counter uint64) {
if counter > f.last { if counter > f.last {
lastBlockIndex := f.last >> swBlockBitLog lastBlockIndex := f.last >> swBlockBitLog
diff := int(blockIndex - lastBlockIndex) diff := blockIndex - lastBlockIndex
if diff > swRingBlocks { if diff > swRingBlocks {
diff = swRingBlocks diff = swRingBlocks
} }
for i := 0; i < diff; i++ { for i := 0; i < int(diff); i++ {
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
f.ring[lastBlockIndex] = 0 f.ring[lastBlockIndex] = 0
} }
+5 -18
View File
@@ -18,13 +18,9 @@ import (
) )
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) { func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
if s.currentConn.Load() == nil {
s.currentConn.Store(conn) s.currentConn.Store(conn)
}
if s.timer != nil {
s.timer.Update() s.timer.Update()
} }
}
func (s *ServerUDPSession) WriteToClient(b []byte) error { func (s *ServerUDPSession) WriteToClient(b []byte) error {
connVal := s.currentConn.Load() connVal := s.currentConn.Load()
@@ -57,19 +53,16 @@ func (s *ServerUDPSession) EnsureLink(
policyManager policy.Manager, policyManager policy.Manager,
responseEncoder func(dest net.Destination, payload []byte) ([]byte, error), responseEncoder func(dest net.Destination, payload []byte) ([]byte, error),
) (*transport.Link, error) { ) (*transport.Link, error) {
s.UpdateConn(conn)
if link := s.link.Load(); link != nil {
return link, nil
}
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if link := s.link.Load(); link != nil { if link := s.link.Load(); link != nil {
s.UpdateConn(conn)
return link, nil return link, nil
} }
s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx) sessCtx, cancel := context.WithCancel(ctx)
inbound := session.InboundFromContext(sessCtx) inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil { if inbound != nil && s.User != nil {
@@ -94,7 +87,6 @@ func (s *ServerUDPSession) EnsureLink(
return nil, err return nil, err
} }
s.link.Store(link)
sessionPolicy := policyManager.ForLevel(level) sessionPolicy := policyManager.ForLevel(level)
s.timer = signal.CancelAfterInactivity(sessCtx, func() { s.timer = signal.CancelAfterInactivity(sessCtx, func() {
if s.manager != nil { if s.manager != nil {
@@ -103,6 +95,7 @@ func (s *ServerUDPSession) EnsureLink(
s.Close() s.Close()
cancel() cancel()
}, sessionPolicy.Timeouts.ConnectionIdle) }, sessionPolicy.Timeouts.ConnectionIdle)
s.link.Store(link)
go handleUDPResponse(s, link, dest, responseEncoder) go handleUDPResponse(s, link, dest, responseEncoder)
return link, nil return link, nil
@@ -118,19 +111,13 @@ func ResetTCPConn(conn net.Conn) {
} }
func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) { func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) {
defer func() { defer s.timer.SetTimeout(0)
if s.timer != nil {
s.timer.SetTimeout(0)
}
}()
for { for {
resMb, err := link.Reader.ReadMultiBuffer() resMb, err := link.Reader.ReadMultiBuffer()
if err != nil { if err != nil {
return return
} }
if s.timer != nil {
s.timer.Update() s.timer.Update()
}
for i, rb := range resMb { for i, rb := range resMb {
b := rb.Bytes() b := rb.Bytes()
if encode != nil { if encode != nil {
+12 -4
View File
@@ -11,6 +11,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/antireplay" "github.com/xtls/xray-core/common/antireplay"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -253,7 +254,7 @@ func ReadClientRequestHeaderWithFixed(reader *StreamReader, fixedChunk []byte) (
} }
epoch := binary.BigEndian.Uint64(plainFixed[1:9]) epoch := binary.BigEndian.Uint64(plainFixed[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch)))) diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
if diff > 30 { if diff > 30 {
return nil, ErrBadTimestamp return nil, ErrBadTimestamp
} }
@@ -430,7 +431,7 @@ func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt [
} }
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9]) serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))) diff := math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))
if diff > 30 { if diff > 30 {
return nil, ErrBadTimestamp return nil, ErrBadTimestamp
} }
@@ -629,6 +630,7 @@ 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 { func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.Reader, writer buf.Writer, link *transport.Link) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
defer timer.SetTimeout(0)
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
requestDone := func() error { requestDone := func() error {
@@ -644,6 +646,12 @@ func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)) return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
} }
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer))
return task.Run(ctx, requestDone, responseDoneAndCloseWriter) 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
} }
+18 -12
View File
@@ -56,20 +56,21 @@ func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
return nil return nil
} }
// tunTCPWriter is the writing side of a TCP connection for the dispatcher. It has no Close: the outbound closes // TCPWriter is the writing side of a TCP connection of the stack for the dispatcher. It has no Close: the outbound
// its writer when the other side has no more to send, and the system may still be sending // closes its writer when the other side has no more to send, and the system may still be sending
type tunTCPWriter struct { type TCPWriter struct {
writer buf.Writer Writer buf.Writer
counter stats.Counter // Counter counts what is written, if there is one
Counter stats.Counter
} }
func (w *tunTCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { func (w *TCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
size := mb.Len() size := mb.Len()
if err := w.writer.WriteMultiBuffer(mb); err != nil { if err := w.Writer.WriteMultiBuffer(mb); err != nil {
return err return err
} }
if w.counter != nil { if w.Counter != nil {
w.counter.Add(int64(size)) w.Counter.Add(int64(size))
} }
return nil return nil
} }
@@ -143,7 +144,8 @@ func (t *Handler) Start() error {
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil") errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
return errors.New("iface not found") return errors.New("iface not found")
} }
return c.Control(func(fd uintptr) { var controlErr, sockErr error
controlErr = c.Control(func(fd uintptr) {
addrPort, _ := netip.ParseAddrPort(address) addrPort, _ := netip.ParseAddrPort(address)
// skip loopback // skip loopback
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") { if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
@@ -151,9 +153,13 @@ func (t *Handler) Start() error {
} }
err := setinterface(network, address, fd, iface) err := setinterface(network, address, fd, iface)
if err != nil { if err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface") sockErr = errors.New("[tun] falied to set interface").Base(err)
} }
}) })
if sockErr != nil {
return sockErr
}
return controlErr
}) })
} }
@@ -243,7 +249,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn), Counter: t.uplinkCounter} reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn), Counter: t.uplinkCounter}
writer := buf.NewWriter(conn) writer := buf.NewWriter(conn)
if destination.Network != net.Network_UDP { if destination.Network != net.Network_UDP {
writer = &tunTCPWriter{writer: writer, counter: t.downlinkCounter} writer = &TCPWriter{Writer: writer, Counter: t.downlinkCounter}
} else if t.downlinkCounter != nil { } else if t.downlinkCounter != nil {
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter} writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
} }
-106
View File
@@ -1,106 +0,0 @@
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
@@ -1,174 +0,0 @@
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)
}
+231 -2
View File
@@ -2,8 +2,14 @@ package tun
import ( import (
"encoding/binary" "encoding/binary"
"math"
"math/bits" "math/bits"
"net/netip" "net/netip"
"slices"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
) )
const ( const (
@@ -16,6 +22,19 @@ const (
protoTCP = 6 protoTCP = 6
protoUDP = 17 protoUDP = 17
protoICMPv6 = 58 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 // packet is an IP packet with its header read
@@ -24,9 +43,15 @@ type packet struct {
src, dst netip.Addr src, dst netip.Addr
// payload is what follows the IP header // payload is what follows the IP header
payload []byte 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. Fragments are not accepted: what the system sends to the tun fits its MTU // parsePacket reads the IP header of b
func parsePacket(b []byte) (p packet, ok bool) { func parsePacket(b []byte) (p packet, ok bool) {
if len(b) < ipv4HeaderSize { if len(b) < ipv4HeaderSize {
return p, false return p, false
@@ -35,9 +60,12 @@ func parsePacket(b []byte) (p packet, ok bool) {
case 4: case 4:
size := int(b[0]&0xf) * 4 size := int(b[0]&0xf) * 4
total := int(binary.BigEndian.Uint16(b[2:])) total := int(binary.BigEndian.Uint16(b[2:]))
if size < ipv4HeaderSize || total < size || total > len(b) || binary.BigEndian.Uint16(b[6:])&0x3fff != 0 { if size < ipv4HeaderSize || total < size || total > len(b) {
return p, false 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.proto = b[9]
p.src = netip.AddrFrom4([4]byte(b[12:16])) p.src = netip.AddrFrom4([4]byte(b[12:16]))
p.dst = netip.AddrFrom4([4]byte(b[16:20])) p.dst = netip.AddrFrom4([4]byte(b[16:20]))
@@ -65,12 +93,55 @@ func parsePacket(b []byte) (p packet, ok bool) {
} }
p.proto, p.payload = p.payload[0], p.payload[size:] 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: default:
return p, false return p, false
} }
return p, true 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 // 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 { func putIPHeader(b []byte, proto byte, src, dst netip.Addr) int {
if src.Is4() { if src.Is4() {
@@ -159,3 +230,161 @@ func checksum(s uint64) uint16 {
s = s>>16 + s&0xffff s = s>>16 + s&0xffff
return ^uint16(s) 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
}
+288 -24
View File
@@ -3,7 +3,12 @@ package tun
import ( import (
"context" "context"
"encoding/binary" "encoding/binary"
"io"
"math"
"math/rand/v2"
gonet "net"
"net/netip" "net/netip"
"slices"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -15,8 +20,16 @@ import (
// Stack interface implement ip protocol stack, bridging raw network packets and data streams // Stack interface implement ip protocol stack, bridging raw network packets and data streams
type Stack interface { type Stack interface {
// Start makes the stack read the packets of its tun
Start() error Start() error
Close() 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 // StackOptions for the stack implementation
@@ -24,6 +37,12 @@ type StackOptions struct {
Tun Tun Tun Tun
MTU uint32 MTU uint32
IdleTimeout time.Duration 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 // flow is what the packets of one connection have in common, src is the side of the system
@@ -33,7 +52,8 @@ type flow struct {
// stack turns the packets that the system sends to the tun into connections and back. // 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 // 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 // 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
type stack struct { type stack struct {
ctx context.Context ctx context.Context
tun Tun tun Tun
@@ -42,32 +62,58 @@ type stack struct {
segmenter segmenter segmenter segmenter
// window is the window of every connection // window is the window of every connection
window int32 window int32
// handle is given every new connection // 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 func(conn net.Conn, destination net.Destination) handle func(conn net.Conn, destination net.Destination)
udp *udpConnectionHandler udp *udpConnectionHandler
// addresses are what connections are opened from
addresses []netip.Addr
mu sync.Mutex mu sync.Mutex
conns map[flow]*tcpConn conns map[flow]*tcpConn
// sockets are the ports that DialUDP has opened
sockets map[netip.AddrPort]*udpSocket
closed bool closed bool
// opening is how many of conns wait for the answer to their SYN-ACK // opening is how many of conns wait for the answer to their SYN or SYN-ACK
opening atomic.Int32 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 // NewStack builds new ip stack. Without a handler it takes no connections, only opens them
func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) { func NewStack(ctx context.Context, options StackOptions, handler ConnectionHandler) (Stack, error) {
s := &stack{ s := &stack{
ctx: ctx, ctx: ctx,
tun: options.Tun, tun: options.Tun,
mtu: int(options.MTU), mtu: int(options.MTU),
window: tcpWindow, window: tcpWindow,
handle: handler.HandleConnection, addresses: options.Addresses,
verify: options.Remote,
stamps: options.Remote,
conns: make(map[flow]*tcpConn), conns: make(map[flow]*tcpConn),
sockets: make(map[netip.AddrPort]*udpSocket),
} }
if t, ok := options.Tun.(segmenter); ok && t.Segments() { if t, ok := options.Tun.(segmenter); ok && t.Segments() {
s.segmenter = t s.segmenter = t
s.window = tcpLargeWindow 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 return s, nil
} }
@@ -85,6 +131,8 @@ func (s *stack) Close() error {
for _, c := range s.conns { for _, c := range s.conns {
conns = append(conns, c) conns = append(conns, c)
} }
sockets := s.sockets
s.sockets = nil
s.mu.Unlock() s.mu.Unlock()
for _, c := range conns { for _, c := range conns {
c.mu.Lock() c.mu.Lock()
@@ -93,6 +141,9 @@ func (s *stack) Close() error {
} }
c.mu.Unlock() c.mu.Unlock()
} }
for _, u := range sockets {
u.queue.close()
}
return nil return nil
} }
@@ -111,9 +162,29 @@ func (s *stack) read() {
} }
return 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) p, ok := parsePacket(b)
if !ok { if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() {
continue // 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 { switch p.proto {
case protoTCP: case protoTCP:
@@ -124,6 +195,26 @@ func (s *stack) read() {
s.handleICMP(p) 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
}
}
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) { func (s *stack) handleTCP(p packet) {
@@ -154,7 +245,7 @@ func (s *stack) handleTCP(p packet) {
if c != nil && c.input(seg) { if c != nil && c.input(seg) {
return return
} }
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn { if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn || s.handle == nil {
if seg.flags&tcpRst == 0 { if seg.flags&tcpRst == 0 {
s.refuse(f, seg) s.refuse(f, seg)
} }
@@ -164,7 +255,8 @@ func (s *stack) handleTCP(p packet) {
if s.opening.Load() >= tcpOpening { if s.opening.Load() >= tcpOpening {
return return
} }
c = newTCPConn(s, f, seg) c = newTCPConn(s, f)
c.synchronize(seg)
s.mu.Lock() s.mu.Lock()
if s.closed { if s.closed {
s.mu.Unlock() s.mu.Unlock()
@@ -174,7 +266,7 @@ func (s *stack) handleTCP(p packet) {
s.mu.Unlock() s.mu.Unlock()
s.opening.Add(1) s.opening.Add(1)
c.mu.Lock() c.mu.Lock()
c.sendSynAck() c.sendSyn()
c.arm(tcpTick) c.arm(tcpTick)
c.mu.Unlock() c.mu.Unlock()
} }
@@ -224,51 +316,189 @@ func (s *stack) handleUDP(p packet) {
if size < udpHeaderSize || size > len(u) { if size < udpHeaderSize || size > len(u) {
return 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 // the packet waits for its reader, longer than read keeps it
b := buf.NewWithSize(int32(size - udpHeaderSize)) b := buf.NewWithSize(int32(size - udpHeaderSize))
b.Write(u[udpHeaderSize:size]) b.Write(u[udpHeaderSize:size])
src := net.UDPDestination(net.IPAddress(p.src.AsSlice()), net.Port(binary.BigEndian.Uint16(u))) src := net.UDPDestination(net.IPAddress(p.src.AsSlice()), net.Port(binary.BigEndian.Uint16(u)))
dst := net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:]))) if socket == nil {
s.udp.HandlePacket(src, dst, b) 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()
}
} }
// writeUDP writes a packet with the payload from src to dst to the tun // 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 { func (s *stack) writeUDP(payload []byte, src net.Destination, dst net.Destination) error {
srcIP, _ := netip.AddrFromSlice(src.Address.IP()) srcIP, _ := netip.AddrFromSlice(src.Address.IP())
dstIP, _ := netip.AddrFromSlice(dst.Address.IP()) dstIP, _ := netip.AddrFromSlice(dst.Address.IP())
srcIP, dstIP = srcIP.Unmap(), dstIP.Unmap() return s.sendUDP(payload, netip.AddrPortFrom(srcIP, uint16(src.Port)), netip.AddrPortFrom(dstIP, uint16(dst.Port)))
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() { }
return errors.New("failed to write udp packet from ", src, " to ", dst)
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)
} }
var b [ipv6HeaderSize + udpHeaderSize]byte var b [ipv6HeaderSize + udpHeaderSize]byte
n := putIPHeader(b[:], protoUDP, srcIP, dstIP) n := putIPHeader(b[:], protoUDP, srcIP, dstIP)
u := b[n : n+udpHeaderSize] u := b[n : n+udpHeaderSize]
binary.BigEndian.PutUint16(u, uint16(src.Port)) binary.BigEndian.PutUint16(u, src.Port())
binary.BigEndian.PutUint16(u[2:], uint16(dst.Port)) binary.BigEndian.PutUint16(u[2:], dst.Port())
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(payload))) binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(payload)))
setIPLength(b[:n], n+udpHeaderSize+len(payload))
check := transportChecksum(b[:n], protoUDP, u, payload) check := transportChecksum(b[:n], protoUDP, u, payload)
if check == 0 { if check == 0 {
// zero stands for no checksum // zero stands for no checksum
check = 0xffff check = 0xffff
} }
binary.BigEndian.PutUint16(u[6:], check) binary.BigEndian.PutUint16(u[6:], check)
return s.tun.WritePacket(b[:n+udpHeaderSize], payload) return s.write(b[:n+udpHeaderSize], payload)
} }
// handleICMP answers an echo request as its destination would // 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
func (s *stack) handleICMP(p packet) { func (s *stack) handleICMP(p packet) {
m := p.payload m := p.payload
request, reply := byte(8), byte(0) request, reply := byte(8), byte(0)
if p.proto == protoICMPv6 { if p.proto == protoICMPv6 {
request, reply = 128, 129 request, reply = 128, 129
} }
if len(m) < 8 || m[0] != request || m[1] != 0 { 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
return return
} }
var b [ipv6HeaderSize]byte var b [ipv6HeaderSize]byte
n := putIPHeader(b[:], p.proto, p.dst, p.src) n := putIPHeader(b[:], p.proto, p.dst, p.src)
setIPLength(b[:n], n+len(m))
// the reply is the request with another type // the reply is the request with another type
m[0], m[2], m[3] = reply, 0, 0 m[0], m[2], m[3] = reply, 0, 0
if p.proto == protoICMPv6 { if p.proto == protoICMPv6 {
@@ -276,5 +506,39 @@ func (s *stack) handleICMP(p packet) {
} else { } else {
binary.BigEndian.PutUint16(m[2:], checksum(sum(m, 0))) binary.BigEndian.PutUint16(m[2:], checksum(sum(m, 0)))
} }
_ = s.tun.WritePacket(b[:n], m) _ = 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)
}
} }
+1452 -128
View File
File diff suppressed because it is too large Load Diff
+284 -38
View File
@@ -1,10 +1,12 @@
package tun package tun
import ( import (
"context"
"encoding/binary" "encoding/binary"
"io" "io"
"math" "math"
"math/rand/v2" "math/rand/v2"
"net/netip"
"slices" "slices"
"sync" "sync"
"time" "time"
@@ -37,7 +39,9 @@ const (
tcpHeld = 1024 tcpHeld = 1024
// tcpSmallest is the smallest mss that the system is believed, the one of Linux // tcpSmallest is the smallest mss that the system is believed, the one of Linux
tcpSmallest = 48 tcpSmallest = 48
// tcpOpening is how many connections can be waiting for the answer to their SYN-ACK // 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 = 65535 tcpOpening = 65535
// tcpStart is how many segments are sent before any of them is acknowledged // tcpStart is how many segments are sent before any of them is acknowledged
tcpStart = 10 tcpStart = 10
@@ -48,12 +52,18 @@ const (
// tcpTick is how often a connection that waits for something looks at what it waits for // tcpTick is how often a connection that waits for something looks at what it waits for
tcpTick = 100 * time.Millisecond tcpTick = 100 * time.Millisecond
// ticks without anything acknowledged before it is sent again, twice as many every next time, a minute at most // 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
tcpRetransmitTicks = 3 tcpRetransmitTicks = 3
tcpRetransmitTicksOpen = 20
tcpRetransmitTicksMost = 600 tcpRetransmitTicksMost = 600
// how many times it is sent again before the connection is given up: that is after four minutes, as what is tcpMinuteTicks = int(time.Minute / tcpTick)
// behind a wireless link may be away for a while, and after twenty seconds for one that never answered // an open connection is given up after four minutes of ticks without an acknowledgment since the timer first sent
tcpRetransmits = 10 // 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
tcpSynRetransmits = 5 tcpSynRetransmits = 5
// ticks between the probes of a window that stays closed // ticks between the probes of a window that stays closed
tcpProbeTicks = 10 tcpProbeTicks = 10
@@ -64,8 +74,13 @@ const (
var ( var (
errTCPReset = errors.New("connection reset") errTCPReset = errors.New("connection reset")
errTCPTimeout = errors.New("connection timed out") errTCPTimeout = errors.New("connection timed out")
errTCPRefused = errors.New("connection refused")
errTCPUnreachable = errors.New("destination unreachable")
) )
// tcpStarted is where the times that segments carry count from
var tcpStarted = time.Now()
// segment is a TCP segment that came from the tun // segment is a TCP segment that came from the tun
type segment struct { type segment struct {
seq, ack uint32 seq, ack uint32
@@ -92,7 +107,7 @@ type held struct {
data *buf.Buffer data *buf.Buffer
} }
// tcpConn is a TCP connection that the system opened through the tun. // tcpConn is a TCP connection that the system opened through the tun, or one that was opened to it by DialTCP.
// Mostly the two are on the same machine, with nothing but the queue of the tun in between, which drops what // 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 // 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, // is in between. So a connection has windows that are small enough, keeps what came after a lost segment,
@@ -114,12 +129,19 @@ type tcpConn struct {
sndShift, rcvShift uint8 sndShift, rcvShift uint8
// sack is set if the system takes selective acknowledgments // sack is set if the system takes selective acknowledgments
sack bool 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 mu sync.Mutex
readable sync.Cond readable sync.Cond
writable sync.Cond writable sync.Cond
established bool 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 is set when Close is called
closed bool closed bool
// err is why the connection is gone // err is why the connection is gone
@@ -140,6 +162,8 @@ type tcpConn struct {
rcvHeld []held rcvHeld []held
// rcvLast is the seq of the last one that was put into rcvHeld // rcvLast is the seq of the last one that was put into rcvHeld
rcvLast uint32 rcvLast uint32
// stamp is the time of the other side that is sent back to it
stamp uint32
sndUna, sndNxt uint32 sndUna, sndNxt uint32
sndWnd uint32 sndWnd uint32
@@ -165,6 +189,9 @@ type tcpConn struct {
recover uint32 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 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 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. // 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 // The first one is the SYN-ACK
timed uint32 timed uint32
@@ -176,17 +203,17 @@ type tcpConn struct {
armed bool armed bool
// stalled counts ticks without an acknowledgment of what waits for one // stalled counts ticks without an acknowledgment of what waits for one
stalled, retransmitTicks, retransmits int 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, syn segment) *tcpConn { func newTCPConn(s *stack, f flow) *tcpConn {
c := &tcpConn{ c := &tcpConn{
stack: s, stack: s,
flow: f, flow: f,
src: net.TCPDestination(net.IPAddress(f.src.Addr().AsSlice()), net.Port(f.src.Port())), 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())), 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, window: s.window,
ssthresh: uint32(s.window), ssthresh: uint32(s.window),
timedAt: time.Now(), timedAt: time.Now(),
@@ -196,6 +223,17 @@ func newTCPConn(s *stack, f flow, syn segment) *tcpConn {
c.writable.L = &c.mu c.writable.L = &c.mu
c.ipSize = putIPHeader(c.ip[:], protoTCP, f.dst.Addr(), f.src.Addr()) c.ipSize = putIPHeader(c.ip[:], protoTCP, f.dst.Addr(), f.src.Addr())
c.mss = s.mtu - c.ipSize - tcpHeaderSize 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) { for kind, value, o := nextOption(syn.options); kind != 0; kind, value, o = nextOption(o) {
switch { switch {
case kind == 2 && len(value) == 2: case kind == 2 && len(value) == 2:
@@ -205,20 +243,77 @@ func newTCPConn(s *stack, f flow, syn segment) *tcpConn {
c.rcvShift = tcpWindowShift c.rcvShift = tcpWindowShift
case kind == 4: case kind == 4:
c.sack = true 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.mss = max(c.mss, tcpSmallest)
c.cwnd = tcpStart * uint32(c.mss) c.cwnd = tcpStart * uint32(c.mss)
c.sndUna = rand.Uint32() }
c.sndNxt = c.sndUna + 1
return c // 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
} }
// header writes the headers of a segment with size bytes of payload into b and returns their size. // 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 // 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 { func (c *tcpConn) header(b []byte, seq uint32, flags byte, options []byte, size int) int {
n := copy(b, c.ip[:c.ipSize]) n := copy(b, c.ip[:c.ipSize])
t := b[n : n+tcpHeaderSize+len(options)] stamps := 0
if c.stamps {
stamps = tcpStamps
}
t := b[n : n+tcpHeaderSize+stamps+len(options)]
binary.BigEndian.PutUint16(t, c.flow.dst.Port()) binary.BigEndian.PutUint16(t, c.flow.dst.Port())
binary.BigEndian.PutUint16(t[2:], c.flow.src.Port()) binary.BigEndian.PutUint16(t[2:], c.flow.src.Port())
binary.BigEndian.PutUint32(t[4:], seq) binary.BigEndian.PutUint32(t[4:], seq)
@@ -233,7 +328,18 @@ 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))) binary.BigEndian.PutUint16(t[14:], uint16(min(window>>c.rcvShift, math.MaxUint16)))
} }
t[16], t[17], t[18], t[19] = 0, 0, 0, 0 t[16], t[17], t[18], t[19] = 0, 0, 0, 0
copy(t[tcpHeaderSize:], options) 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)
setIPLength(b[:n], n+len(t)+size) setIPLength(b[:n], n+len(t)+size)
c.rcvAcked = c.rcvNxt c.rcvAcked = c.rcvNxt
c.rcvAdvertised = window c.rcvAdvertised = window
@@ -254,7 +360,8 @@ func (c *tcpConn) send(seq uint32, flags byte, options, payload []byte) {
c.write(b[:c.header(b[:], seq, flags, options, len(payload))], payload) c.write(b[:c.header(b[:], seq, flags, options, len(payload))], payload)
} }
func (c *tcpConn) sendSynAck() { // 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() {
options := [12]byte{2, 4} options := [12]byte{2, 4}
binary.BigEndian.PutUint16(options[2:], uint16(c.stack.mtu-c.ipSize-tcpHeaderSize)) binary.BigEndian.PutUint16(options[2:], uint16(c.stack.mtu-c.ipSize-tcpHeaderSize))
n := 4 n := 4
@@ -264,7 +371,11 @@ func (c *tcpConn) sendSynAck() {
if c.rcvShift != 0 { if c.rcvShift != 0 {
n += copy(options[n:], []byte{1, 3, 3, c.rcvShift}) n += copy(options[n:], []byte{1, 3, 3, c.rcvShift})
} }
c.send(c.sndUna, tcpSyn|tcpAck, options[:n], nil) flags := byte(tcpSyn | tcpAck)
if c.opened != nil {
flags = tcpSyn
}
c.send(c.sndUna, flags, options[:n], nil)
} }
// sendAck acknowledges what was received, and tells what of rcvHeld waits for a lost segment // sendAck acknowledges what was received, and tells what of rcvHeld waits for a lost segment
@@ -278,8 +389,12 @@ func (c *tcpConn) sendAck() {
c.send(c.sndNxt, tcpAck, nil, nil) c.send(c.sndNxt, tcpAck, nil, nil)
return return
} }
// the block with the segment that came last goes first // the block with the segment that came last goes first. Next to the times there is room for one less
var blocks [4][2]uint32 var room [4][2]uint32
blocks := room[:]
if c.stamps {
blocks = room[:3]
}
n, last := 1, false n, last := 1, false
for i := 0; i < len(c.rcvHeld); { for i := 0; i < len(c.rcvHeld); {
start, end, recent := c.rcvHeld[i].seq, c.rcvHeld[i].seq, false start, end, recent := c.rcvHeld[i].seq, c.rcvHeld[i].seq, false
@@ -306,6 +421,22 @@ func (c *tcpConn) sendAck() {
c.send(c.sndNxt, tcpAck, options[:4+8*len(list)], nil) 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 // input takes a segment of the connection. It returns false if the connection is gone
// and the segment is the start of another one // and the segment is the start of another one
func (c *tcpConn) input(s segment) bool { func (c *tcpConn) input(s segment) bool {
@@ -314,6 +445,32 @@ func (c *tcpConn) input(s segment) bool {
if c.err != nil { if c.err != nil {
return s.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn 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 { 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 // 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 { if s.seq-c.rcvNxt <= uint32(c.window) || c.rcvFin && s.seq+1 == c.rcvNxt {
@@ -324,9 +481,12 @@ func (c *tcpConn) input(s segment) bool {
if s.flags&tcpSyn != 0 { if s.flags&tcpSyn != 0 {
switch { switch {
case !c.established && s.seq+1 == c.rcvNxt: case !c.established && s.seq+1 == c.rcvNxt:
c.sendSynAck() c.sendSyn()
c.timedAt = time.Time{} c.timedAt = time.Time{}
case c.closed && s.flags&tcpAck == 0: 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
c.abort(errTCPReset) c.abort(errTCPReset)
return false return false
default: default:
@@ -337,6 +497,9 @@ func (c *tcpConn) input(s segment) bool {
if s.flags&tcpAck == 0 { if s.flags&tcpAck == 0 {
return true return true
} }
if c.stamps {
c.stamped(s)
}
if !c.established { if !c.established {
if s.ack != c.sndNxt { if s.ack != c.sndNxt {
return true return true
@@ -385,6 +548,7 @@ func (c *tcpConn) acknowledge(s segment) {
if !c.timedAt.IsZero() && int32(c.sndUna-c.timed) >= 0 { if !c.timedAt.IsZero() && int32(c.sndUna-c.timed) >= 0 {
c.clocked() c.clocked()
} }
recovering := c.recovering
switch { switch {
case c.recovering && int32(c.sndUna-c.recover) >= 0: case c.recovering && int32(c.sndUna-c.recover) >= 0:
c.recovering = false c.recovering = false
@@ -393,11 +557,18 @@ func (c *tcpConn) acknowledge(s segment) {
if end := c.sndUna + min(uint32(c.mss), c.recover-c.sndUna); int32(end-c.lost) > 0 { if end := c.sndUna + min(uint32(c.mss), c.recover-c.sndUna); int32(end-c.lost) > 0 {
c.lost = end 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 // 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 sent >= c.cwnd/2 {
if c.cwnd < c.ssthresh { if c.cwnd < c.ssthresh {
c.cwnd = min(c.cwnd+acked, 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)
} else if !c.recovering { } else if !c.recovering {
// a segment more for every cwnd that arrives // 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)) c.cwnd = min(c.cwnd+max(uint32(uint64(c.mss)*uint64(acked)/uint64(c.cwnd)), 1), uint32(c.window))
@@ -417,12 +588,22 @@ func (c *tcpConn) acknowledge(s segment) {
} }
} }
} }
if !c.recovering && c.sndUna != c.sndNxt && (len(c.sacked) > 0 || c.dupAcks >= 3) { 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: the segment at sndUna is lost // 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
c.congested() c.congested()
c.lost = c.sndUna + min(uint32(c.mss), c.sndNxt-c.sndUna) c.lost = c.sndUna + min(uint32(c.mss), c.sndNxt-c.sndUna)
} }
if c.recovering { 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.resend()
} }
c.sndWnd = window c.sndWnd = window
@@ -451,7 +632,12 @@ func (c *tcpConn) clocked() {
c.rttvar += ((c.srtt - d).Abs() - c.rttvar) / 4 c.rttvar += ((c.srtt - d).Abs() - c.rttvar) / 4
c.srtt += (d - c.srtt) / 8 c.srtt += (d - c.srtt) / 8
} }
c.retransmitTicks = max(tcpRetransmitTicks, int((c.srtt+4*c.rttvar)/tcpTick)+2) 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)
} }
// sackAdd puts what the system says it has into sacked. c.mu is held // sackAdd puts what the system says it has into sacked. c.mu is held
@@ -481,7 +667,7 @@ func (c *tcpConn) congested() {
c.ssthresh = max(min(c.cwnd, c.sndNxt-c.sndUna)/2, 2*uint32(c.mss)) c.ssthresh = max(min(c.cwnd, c.sndNxt-c.sndUna)/2, 2*uint32(c.mss))
c.cwnd = c.ssthresh c.cwnd = c.ssthresh
c.recovering, c.recover, c.resent = true, c.sndNxt, c.sndUna c.recovering, c.recover, c.resent = true, c.sndNxt, c.sndUna
c.timedAt = time.Time{} c.timedAt, c.headAt = time.Time{}, time.Time{}
} }
// hole returns the first part at or after seq of what is lost: what the system does not have // hole returns the first part at or after seq of what is lost: what the system does not have
@@ -543,6 +729,10 @@ func (c *tcpConn) resend() {
room = c.cwnd - flight room = c.cwnd - flight
} }
end = start + min(end-start, room) 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.retransmit(start, end)
c.resent = end c.resent = end
} }
@@ -642,9 +832,9 @@ func (c *tcpConn) release() {
// hold keeps a segment that is not the next one. c.mu is held // hold keeps a segment that is not the next one. c.mu is held
func (c *tcpConn) hold(seq uint32, payload []byte) { func (c *tcpConn) hold(seq uint32, payload []byte) {
i := 0 i := len(c.rcvHeld)
for i < len(c.rcvHeld) && int32(c.rcvHeld[i].seq-seq) < 0 { for i > 0 && int32(c.rcvHeld[i-1].seq-seq) >= 0 {
i++ i--
} }
// without what the one before and the one after have // without what the one before and the one after have
if i > 0 { if i > 0 {
@@ -666,8 +856,9 @@ func (c *tcpConn) hold(seq uint32, payload []byte) {
if len(payload) == 0 || len(c.rcvHeld) == tcpHeld { if len(payload) == 0 || len(c.rcvHeld) == tcpHeld {
return return
} }
b := buf.NewWithSize(int32(len(payload))) // as much memory as it carries: the room that is left counts bytes, and a thousand tiny segments in pooled
b.Write(payload) // buffers would take two megabytes
b := buf.FromBytes(slices.Clone(payload))
c.rcvHeld = slices.Insert(c.rcvHeld, i, held{seq, b}) c.rcvHeld = slices.Insert(c.rcvHeld, i, held{seq, b})
c.rcvLast = seq c.rcvLast = seq
} }
@@ -716,6 +907,38 @@ func (c *tcpConn) queued(parts [][]byte, seq uint32, size int) ([][]byte, int) {
return parts, n 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 // arm makes tick run after d, unless it is going to run already. c.mu is held
func (c *tcpConn) arm(d time.Duration) { func (c *tcpConn) arm(d time.Duration) {
if c.armed { if c.armed {
@@ -742,19 +965,34 @@ func (c *tcpConn) tick() {
} }
switch { switch {
case c.sndUna != c.sndNxt: case c.sndUna != c.sndNxt:
if c.stalled++; c.stalled >= c.retransmitTicks { c.stalled++
if c.retransmits++; c.retransmits > tcpRetransmits || !c.established && c.retransmits > tcpSynRetransmits { 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 {
c.abort(errTCPTimeout) c.abort(errTCPTimeout)
return return
} }
c.stalled = 0 c.stalled = 0
c.retransmitTicks = min(c.retransmitTicks*2, tcpRetransmitTicksMost) c.retransmitTicks = min(c.retransmitTicks*2, tcpRetransmitTicksMost)
if !c.established { if !c.established {
c.sendSynAck() c.sendSyn()
c.timedAt = time.Time{} c.timedAt = time.Time{}
} else { } else {
// what the system does not say it has is lost, and the way to it is tried with a segment again // 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
c.congested() c.congested()
if recovering {
c.ssthresh = ssthresh
}
c.cwnd, c.lost = uint32(c.mss), c.sndNxt c.cwnd, c.lost = uint32(c.mss), c.sndNxt
c.resend() c.resend()
} }
@@ -792,6 +1030,10 @@ func (c *tcpConn) abort(err error) {
if c.timer != nil { if c.timer != nil {
c.timer.Stop() c.timer.Stop()
} }
if c.opened != nil {
close(c.opened)
c.opened = nil
}
for _, h := range c.rcvHeld { for _, h := range c.rcvHeld {
h.data.Release() h.data.Release()
} }
@@ -879,9 +1121,9 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
c.writing = len(mb) 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 // 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 var room [tcpParts][]byte
limit, parts := c.mss, room[:0:1] parts := room[:0:1]
if c.stack.segmenter != nil { if c.stack.segmenter != nil {
limit, parts = tcpSegment, room[:0] parts = room[:0]
} }
for end := c.sndNxt + size; c.sndNxt != end; { 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 // not a part of a segment when all of it can go later, with what is acknowledged by then
@@ -897,7 +1139,11 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
if c.err != nil { if c.err != nil {
break break
} }
most := min(limit, int(c.space())) // 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()))
}
if most > c.mss { if most > c.mss {
// the same for what the tun splits // the same for what the tun splits
most -= most % c.mss most -= most % c.mss
@@ -907,7 +1153,7 @@ func (c *tcpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
if c.sndNxt+uint32(n) == end { if c.sndNxt+uint32(n) == end {
flags |= tcpPsh flags |= tcpPsh
} }
var b [ipv6HeaderSize + tcpHeaderSize]byte var b [ipv6HeaderSize + tcpHeaderSize + tcpStamps]byte
h := b[:c.header(b[:], c.sndNxt, flags, nil, n)] h := b[:c.header(b[:], c.sndNxt, flags, nil, n)]
if c.sndUna == c.sndNxt { if c.sndUna == c.sndNxt {
// the ticks without an acknowledgment are counted from the first segment that waits for one // the ticks without an acknowledgment are counted from the first segment that waits for one
+74 -14
View File
@@ -39,9 +39,7 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
u.RLock() u.RLock()
conn, found := u.udpConns[src] conn, found := u.udpConns[src]
if found { if found {
select { if !conn.queue.push(data) {
case conn.egress <- data:
default:
errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full") errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full")
data.Release() data.Release()
} }
@@ -55,17 +53,14 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
conn, found = u.udpConns[src] conn, found = u.udpConns[src]
if !found { if !found {
egress := make(chan *buf.Buffer, 1024) conn = &udpConn{handler: u, queue: newPacketQueue(udpQueueLimit), src: src, dst: dst}
conn = &udpConn{handler: u, egress: egress, src: src, dst: dst}
u.udpConns[src] = conn u.udpConns[src] = conn
go u.handleConnection(conn, dst) go u.handleConnection(conn, dst)
} }
// send packet data to the egress channel, if it has buffer, or discard // queue the packet for the reader of the connection, or discard it
select { if !conn.queue.push(data) {
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") errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full 2")
data.Release() data.Release()
} }
@@ -76,7 +71,7 @@ func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
// Close runs twice per flow; a newer conn may already own this src. // Close runs twice per flow; a newer conn may already own this src.
if u.udpConns[conn.src] == conn { if u.udpConns[conn.src] == conn {
delete(u.udpConns, conn.src) delete(u.udpConns, conn.src)
close(conn.egress) conn.queue.close()
} }
u.Unlock() u.Unlock()
} }
@@ -85,13 +80,13 @@ func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
type udpConn struct { type udpConn struct {
handler *udpConnectionHandler handler *udpConnectionHandler
egress chan *buf.Buffer queue *packetQueue
src net.Destination src net.Destination
dst net.Destination dst net.Destination
} }
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
b, ok := <-c.egress b, ok := c.queue.pop()
if !ok { if !ok {
return nil, io.EOF return nil, io.EOF
} }
@@ -100,7 +95,7 @@ func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
// Read packets from the connection // Read packets from the connection
func (c *udpConn) Read(p []byte) (int, error) { func (c *udpConn) Read(p []byte) (int, error) {
b, ok := <-c.egress b, ok := c.queue.pop()
if !ok { if !ok {
return 0, io.EOF return 0, io.EOF
} }
@@ -137,7 +132,7 @@ func (c *udpConn) Write(p []byte) (int, error) {
// sending packets back mean sending payload with source/destination reversed // sending packets back mean sending payload with source/destination reversed
err := c.handler.writePacket(p, c.dst, c.src) err := c.handler.writePacket(p, c.dst, c.src)
if err != nil { if err != nil {
return 0, nil return 0, err
} }
return len(p), nil return len(p), nil
@@ -168,3 +163,68 @@ func (c *udpConn) SetReadDeadline(t time.Time) error {
func (c *udpConn) SetWriteDeadline(t time.Time) error { func (c *udpConn) SetWriteDeadline(t time.Time) error {
return nil 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)
}
}
@@ -1,48 +1,48 @@
package wireguard package tun
import ( import (
"testing" "testing"
"time" "time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
) )
// BenchmarkUDPManagerNewSession measures what one new UDP flow costs the // BenchmarkUDPNewSession measures what one new UDP flow costs the
// inbound while it stays open: QUIC and DNS open many short flows, and each // inbound while it stays open: QUIC and DNS open many short flows, and each
// one lives until the connection idle timeout. // one lives until the connection idle timeout.
func BenchmarkUDPManagerNewSession(b *testing.B) { func BenchmarkUDPNewSession(b *testing.B) {
m := &udpManager{ u := newUdpConnectionHandler(func(conn net.Conn, dest net.Destination) {}, nil)
handler: func(conn net.Conn, dest net.Destination) {},
m: make(map[string]*udpConn),
}
dst := net.UDPDestination(net.ParseAddress("1.1.1.1"), 443) dst := net.UDPDestination(net.ParseAddress("1.1.1.1"), 443)
payload := make([]byte, 1200)
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { 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)) src := net.UDPDestination(net.IPAddress([]byte{10, byte(i >> 16), byte(i >> 8), byte(i)}), net.Port(1024+i%60000))
m.feed(src, dst, payload) u.HandlePacket(src, dst, &buf.Buffer{})
} }
} }
func TestPacketQueueOrderAndClose(t *testing.T) { func TestPacketQueueOrderAndClose(t *testing.T) {
q := newPacketQueue(udpQueueLimit) q := newPacketQueue(udpQueueLimit)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
if !q.push(&packet{p: []byte{byte(i)}}) { b := buf.New()
b.WriteByte(byte(i))
if !q.push(b) {
t.Fatalf("push %d rejected", i) t.Fatalf("push %d rejected", i)
} }
} }
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
p, ok := q.pop() b, ok := q.pop()
if !ok || p.p[0] != byte(i) { if !ok || b.Byte(0) != byte(i) {
t.Fatalf("pop %d: got %v, %v", i, p, ok) t.Fatalf("pop %d: got %v, %v", i, b, ok)
} }
b.Release()
} }
q.close() q.close()
if _, ok := q.pop(); ok { if _, ok := q.pop(); ok {
t.Fatal("pop after close returned a packet") t.Fatal("pop after close returned a packet")
} }
if q.push(&packet{}) { if q.push(&buf.Buffer{}) {
t.Fatal("push after close accepted") t.Fatal("push after close accepted")
} }
} }
@@ -50,11 +50,11 @@ func TestPacketQueueOrderAndClose(t *testing.T) {
func TestPacketQueueLimit(t *testing.T) { func TestPacketQueueLimit(t *testing.T) {
q := newPacketQueue(udpQueueLimit) q := newPacketQueue(udpQueueLimit)
for i := 0; i < udpQueueLimit; i++ { for i := 0; i < udpQueueLimit; i++ {
if !q.push(&packet{}) { if !q.push(&buf.Buffer{}) {
t.Fatalf("push %d rejected below the limit", i) t.Fatalf("push %d rejected below the limit", i)
} }
} }
if q.push(&packet{}) { if q.push(&buf.Buffer{}) {
t.Fatal("push above the limit accepted") t.Fatal("push above the limit accepted")
} }
} }
@@ -80,7 +80,7 @@ func TestPacketQueueCloseUnblocksReader(t *testing.T) {
func TestPacketQueueDropsDrainedStorage(t *testing.T) { func TestPacketQueueDropsDrainedStorage(t *testing.T) {
q := newPacketQueue(udpQueueLimit) q := newPacketQueue(udpQueueLimit)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
q.push(&packet{}) q.push(&buf.Buffer{})
} }
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
q.pop() q.pop()
@@ -1,16 +1,18 @@
package wireguard package tun
import ( import (
"runtime" "runtime"
"testing" "testing"
"github.com/xtls/xray-core/common/buf"
) )
const benchBatch = 64 const benchBatch = 64
// Raw cost of queueing and draining a small burst, as one flow's reader does. // Raw cost of queueing and draining a small burst, as one flow's reader does.
func BenchmarkQueueBurstChan(b *testing.B) { func BenchmarkQueueBurstChan(b *testing.B) {
ch := make(chan *packet, udpQueueLimit) ch := make(chan *buf.Buffer, udpQueueLimit)
p := &packet{} p := &buf.Buffer{}
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for j := 0; j < benchBatch; j++ { for j := 0; j < benchBatch; j++ {
@@ -24,7 +26,7 @@ func BenchmarkQueueBurstChan(b *testing.B) {
func BenchmarkQueueBurstPacketQueue(b *testing.B) { func BenchmarkQueueBurstPacketQueue(b *testing.B) {
q := newPacketQueue(udpQueueLimit) q := newPacketQueue(udpQueueLimit)
p := &packet{} p := &buf.Buffer{}
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for j := 0; j < benchBatch; j++ { for j := 0; j < benchBatch; j++ {
@@ -39,8 +41,8 @@ func BenchmarkQueueBurstPacketQueue(b *testing.B) {
// Producer and consumer on different goroutines; the producer yields when the // Producer and consumer on different goroutines; the producer yields when the
// queue is full instead of spinning, like a blocking channel send would. // queue is full instead of spinning, like a blocking channel send would.
func BenchmarkQueueStreamChan(b *testing.B) { func BenchmarkQueueStreamChan(b *testing.B) {
ch := make(chan *packet, udpQueueLimit) ch := make(chan *buf.Buffer, udpQueueLimit)
p := &packet{} p := &buf.Buffer{}
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
for range ch { for range ch {
@@ -58,7 +60,7 @@ func BenchmarkQueueStreamChan(b *testing.B) {
func BenchmarkQueueStreamPacketQueue(b *testing.B) { func BenchmarkQueueStreamPacketQueue(b *testing.B) {
q := newPacketQueue(udpQueueLimit) q := newPacketQueue(udpQueueLimit)
p := &packet{} p := &buf.Buffer{}
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
for { for {
+6 -15
View File
@@ -6,11 +6,9 @@ import (
gotls "crypto/tls" gotls "crypto/tls"
"encoding/base64" "encoding/base64"
"io" "io"
"reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unsafe"
"github.com/xtls/xray-core/app/dispatcher" "github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/app/reverse" "github.com/xtls/xray-core/app/reverse"
@@ -548,7 +546,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
} }
var input *bytes.Reader var input *bytes.Reader
var rawInput *bytes.Buffer var rawInput **bytes.Buffer
switch requestAddons.Flow { switch requestAddons.Flow {
case vless.XRV: case vless.XRV:
if account.Flow == requestAddons.Flow { if account.Flow == requestAddons.Flow {
@@ -560,30 +558,23 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s
inbound.CanSpliceCopy = 3 inbound.CanSpliceCopy = 3
fallthrough // we will break Mux connections that contain TCP requests fallthrough // we will break Mux connections that contain TCP requests
case protocol.RequestCommandTCP: case protocol.RequestCommandTCP:
var t reflect.Type var target net.Conn
var p uintptr
if commonConn, ok := connection.(*encryption.CommonConn); ok { if commonConn, ok := connection.(*encryption.CommonConn); ok {
if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransportWithoutSecurity(iConn) { 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 inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport / another securityConn should not be penetrated
} }
t = reflect.TypeOf(commonConn).Elem() target = commonConn
p = uintptr(unsafe.Pointer(commonConn))
} else if tlsConn, ok := iConn.(*tls.Conn); ok { } else if tlsConn, ok := iConn.(*tls.Conn); ok {
if tlsConn.ConnectionState().Version != gotls.VersionTLS13 { if tlsConn.ConnectionState().Version != gotls.VersionTLS13 {
return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, tlsConn.ConnectionState().Version) return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, tlsConn.ConnectionState().Version)
} }
t = reflect.TypeOf(tlsConn.Conn).Elem() target = tlsConn.Conn
p = uintptr(unsafe.Pointer(tlsConn.Conn))
} else if realityConn, ok := iConn.(*reality.Conn); ok { } else if realityConn, ok := iConn.(*reality.Conn); ok {
t = reflect.TypeOf(realityConn.Conn).Elem() target = realityConn.Conn
p = uintptr(unsafe.Pointer(realityConn.Conn))
} else { } else {
return errors.New("XTLS only supports TLS and REALITY directly for now.") return errors.New("XTLS only supports TLS and REALITY directly for now.")
} }
i, _ := t.FieldByName("input") input, rawInput = vless.AccessTLSBuffers(target)
r, _ := t.FieldByName("rawInput")
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))
} }
} else { } else {
return errors.New("account " + account.ID.String() + " is not able to use the flow " + requestAddons.Flow) return errors.New("account " + account.ID.String() + " is not able to use the flow " + requestAddons.Flow)
+7 -17
View File
@@ -5,11 +5,9 @@ import (
"context" "context"
gotls "crypto/tls" gotls "crypto/tls"
"encoding/base64" "encoding/base64"
"reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
"unsafe"
utls "github.com/refraction-networking/utls" utls "github.com/refraction-networking/utls"
proxymanConfig "github.com/xtls/xray-core/app/proxyman" proxymanConfig "github.com/xtls/xray-core/app/proxyman"
@@ -244,7 +242,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
} }
var input *bytes.Reader var input *bytes.Reader
var rawInput *bytes.Buffer var rawInput **bytes.Buffer
allowUDP443 := false allowUDP443 := false
switch requestAddons.Flow { switch requestAddons.Flow {
case vless.XRV + "-udp443": case vless.XRV + "-udp443":
@@ -261,30 +259,22 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
case protocol.RequestCommandMux: case protocol.RequestCommandMux:
fallthrough // let server break Mux connections that contain TCP requests fallthrough // let server break Mux connections that contain TCP requests
case protocol.RequestCommandTCP, protocol.RequestCommandRvs: case protocol.RequestCommandTCP, protocol.RequestCommandRvs:
var t reflect.Type var target net.Conn
var p uintptr
if commonConn, ok := conn.(*encryption.CommonConn); ok { if commonConn, ok := conn.(*encryption.CommonConn); ok {
if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransportWithoutSecurity(iConn) { 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 ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport / another securityConn should not be penetrated
} }
t = reflect.TypeOf(commonConn).Elem() target = commonConn
p = uintptr(unsafe.Pointer(commonConn))
} else if tlsConn, ok := iConn.(*tls.Conn); ok { } else if tlsConn, ok := iConn.(*tls.Conn); ok {
t = reflect.TypeOf(tlsConn.Conn).Elem() target = tlsConn.Conn
p = uintptr(unsafe.Pointer(tlsConn.Conn))
} else if utlsConn, ok := iConn.(*tls.UConn); ok { } else if utlsConn, ok := iConn.(*tls.UConn); ok {
t = reflect.TypeOf(utlsConn.Conn).Elem() target = utlsConn.Conn
p = uintptr(unsafe.Pointer(utlsConn.Conn))
} else if realityConn, ok := iConn.(*reality.UConn); ok { } else if realityConn, ok := iConn.(*reality.UConn); ok {
t = reflect.TypeOf(realityConn.Conn).Elem() target = realityConn.Conn
p = uintptr(unsafe.Pointer(realityConn.Conn))
} else { } else {
return errors.New("XTLS only supports TLS and REALITY directly for now.") return errors.New("XTLS only supports TLS and REALITY directly for now.")
} }
i, _ := t.FieldByName("input") input, rawInput = vless.AccessTLSBuffers(target)
r, _ := t.FieldByName("rawInput")
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))
default: default:
panic("unknown VLESS request command") panic("unknown VLESS request command")
} }
+21
View File
@@ -5,7 +5,28 @@
// clients with 'socks' for proxying. // clients with 'socks' for proxying.
package vless package vless
import (
"bytes"
"net"
"reflect"
"github.com/xtls/xray-core/common/utils"
)
const ( const (
None = "none" None = "none"
XRV = "xtls-rprx-vision" 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
}
+11 -14
View File
@@ -16,8 +16,9 @@ import (
) )
type bind struct { type bind struct {
ctx context.Context
resolveFunc func(host string) (net.IP, error) resolveFunc func(host string) (net.IP, error)
listenFunc func() (net.PacketConn, error) listenFunc func(ctx context.Context) (net.PacketConn, error)
downFunc func() error downFunc func() error
reserved []byte reserved []byte
@@ -26,6 +27,12 @@ type bind struct {
mu sync.Mutex 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) { func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
@@ -34,7 +41,7 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
return nil, 0, conn.ErrBindAlreadyOpen return nil, 0, conn.ErrBindAlreadyOpen
} }
c, err := b.listenFunc() c, err := b.listenFunc(b.ctx)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -52,12 +59,9 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
case <-ch: case <-ch:
default: default:
errors.LogErrorInner(context.Background(), err, "unexpected closed") errors.LogErrorInner(context.Background(), err, "unexpected closed")
b.mu.Lock() if b.downFunc != nil {
downFunc := b.downFunc
b.mu.Unlock()
if downFunc != nil {
go func() { go func() {
common.Must(downFunc()) common.Must(b.downFunc())
}() }()
} }
} }
@@ -79,13 +83,6 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
}, uint16(c.LocalAddr().(*net.UDPAddr).Port), nil }, 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 { func (b *bind) Close() error {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
+132 -116
View File
@@ -7,6 +7,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
@@ -31,17 +32,13 @@ import (
) )
type Handler struct { type Handler struct {
conf *DeviceConfig p policy.Manager
policyManager policy.Manager
dns dns.Client
streamSettings *internet.MemoryStreamConfig canup *atomic.Bool
uplinkCounter stats.Counter
downlinkCounter stats.Counter
tun tun.Device
tnet *Net tnet *Net
bind *bind
dev *device.Device dev *device.Device
ipc string
mu sync.Mutex mu sync.Mutex
} }
@@ -117,24 +114,93 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
errors.LogWarning(context.Background(), "Using kernel TUN") errors.LogWarning(context.Background(), "Using kernel TUN")
tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu)) tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu))
} else { } else {
errors.LogWarning(context.Background(), "Using gVisor TUN") errors.LogWarning(context.Background(), "Using built-in TCP/IP stack")
tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true) tun, tnet, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), nil)
} }
if err != nil { if err != nil {
return nil, err 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{ return &Handler{
conf: conf, p: p,
policyManager: p,
dns: d,
streamSettings: streamSettings, canup: blockup.canup,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
tun: tun,
tnet: tnet, tnet: tnet,
bind: bind,
dev: dev,
ipc: cfg.String(),
}, nil }, nil
} }
@@ -149,7 +215,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.CanSpliceCopy = 3 ob.CanSpliceCopy = 3
dialer.SetOutboundGateway(ctx, ob) dialer.SetOutboundGateway(ctx, ob)
if err := h.init(ctx); err != nil { if err := h.up(ctx); err != nil {
return err return err
} }
@@ -159,7 +225,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
newCtx, newCancel = context.WithCancel(context.Background()) newCtx, newCancel = context.WithCancel(context.Background())
} }
sessionPolicy := h.policyManager.ForLevel(0) sessionPolicy := h.p.ForLevel(0)
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, func() { timer := signal.CancelAfterInactivity(ctx, func() {
cancel() cancel()
@@ -234,119 +300,40 @@ func (h *Handler) Close() (err error) {
if h.dev != nil { if h.dev != nil {
h.dev.Close() h.dev.Close()
h.dev = nil h.dev = nil
h.tun = nil
} else if h.tun != nil {
h.tun.Close()
h.tun = nil
} }
return nil return nil
} }
func (h *Handler) init(ctx context.Context) error { func (h *Handler) up(ctx context.Context) error {
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() defer h.mu.Unlock()
if h.tun == nil { if h.dev == nil {
return errors.New("closed") return errors.New("closed")
} }
if h.dev != nil { h.bind.SetCtx(ctx)
if !h.canup.Load() {
h.canup.Store(true)
}
if len(h.ipc) > 0 {
if err := h.dev.IpcSet(h.ipc); err != nil {
return err
}
h.ipc = ""
}
return h.dev.Up() return h.dev.Up()
} }
resolveFunc := h.resolveLocal
listenFunc := func() (net.PacketConn, error) {
dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint)
if err != nil {
return nil, err
}
var pktConn net.PacketConn
if h.streamSettings.FinalMask != nil {
conn, err := h.streamSettings.FinalMask.DialUDP(ctx, dest)
if err != nil {
return nil, errors.New("failed to dial to dest").Base(err)
}
pktConn = conn.(*net.PacketConnWrapper).PacketConn
} else {
conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings)
if err != nil {
return nil, errors.New("failed to dial to dest").Base(err)
}
switch c := conn.(type) {
case *net.PacketConnWrapper:
pktConn = c.PacketConn
case *cnc.Connection:
pktConn = &internet.FakePacketConn{Conn: c}
default:
panic(reflect.TypeOf(c))
}
}
if h.uplinkCounter != nil || h.downlinkCounter != nil {
pktConn = &PacketCounterConnection{
PacketConn: pktConn,
ReadCounter: h.downlinkCounter,
WriteCounter: h.uplinkCounter,
}
}
return pktConn, nil
}
// device.NewDevice may use the bind right away (Up -> BindUpdate -> Open),
// so everything it reads must be set before creating the device.
bind := &bind{
resolveFunc: resolveFunc,
listenFunc: listenFunc,
reserved: h.conf.Reserved,
}
logger := &device.Logger{
Verbosef: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Debug,
Content: fmt.Sprintf(format, args...),
})
},
Errorf: func(format string, args ...any) {
log.Record(&log.GeneralMessage{
Severity: log.Severity_Error,
Content: fmt.Sprintf(format, args...),
})
},
}
dev := device.NewDevice(h.tun, bind, logger)
bind.setDownFunc(dev.Down)
var cfg strings.Builder
cfg.WriteString("private_key=" + h.conf.SecretKey + "\n")
for _, peer := range h.conf.Peers {
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
if peer.PreSharedKey != "" {
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
}
cfg.WriteString("endpoint=" + peer.Endpoint + "\n")
for _, ip := range peer.AllowedIps {
cfg.WriteString("allowed_ip=" + ip + "\n")
}
if peer.KeepAlive != "" {
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
}
}
err := dev.IpcSet(cfg.String())
if err != nil {
return err
}
err = dev.Up()
if err != nil {
return err
}
h.dev = dev
return nil
}
func (h *Handler) resolveLocal(host string) (net.IP, error) { 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 { if ip := net.ParseIP(host); ip != nil {
return ip, nil return ip, nil
} }
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) ips, _, err := d.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
if err != nil { if err != nil {
return nil, err return nil, err
} }
got := ips got := ips
if h.streamSettings.SocketSettings != nil { if streamSettings.SocketSettings != nil {
var got4, got6 []net.IP var got4, got6 []net.IP
for _, ip := range ips { for _, ip := range ips {
if ip.To4() != nil { if ip.To4() != nil {
@@ -355,7 +342,7 @@ func (h *Handler) resolveLocal(host string) (net.IP, error) {
got6 = append(got6, ip) got6 = append(got6, ip)
} }
} }
switch h.streamSettings.SocketSettings.DomainStrategy { switch streamSettings.SocketSettings.DomainStrategy {
case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP: case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP:
got = ips got = ips
case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4: case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4:
@@ -379,6 +366,7 @@ func (h *Handler) resolveLocal(host string) (net.IP, error) {
} }
return got[dice.Roll(len(got))], nil return got[dice.Roll(len(got))], nil
} }
}
type UDPConnClient struct { type UDPConnClient struct {
net.PacketConn net.PacketConn
@@ -510,3 +498,31 @@ func (c *cache) Cache(host string, saddr []string, ttl uint32) {
deadline: time.Now().Add(time.Second * time.Duration(ttl)), 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
@@ -1,91 +0,0 @@
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
@@ -1,176 +0,0 @@
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)
}
}
+78 -142
View File
@@ -10,7 +10,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"io" "io"
"net" "net"
"net/netip" "net/netip"
@@ -18,101 +17,63 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"syscall" "sync"
"time" "time"
"github.com/xtls/xray-core/common/buf"
xnet "github.com/xtls/xray-core/common/net" xnet "github.com/xtls/xray-core/common/net"
xtun "github.com/xtls/xray-core/proxy/tun"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
"golang.org/x/net/dns/dnsmessage" "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 { type netTun struct {
ep *channel.Endpoint stack xtun.Stack
stack *stack.Stack
events chan tun.Event events chan tun.Event
notifyHandle *channel.NotificationHandle // packets are what the stack has sent and Read has not taken yet
incomingPacket chan *buffer.View packets chan *buf.Buffer
closed chan struct{} closed chan struct{}
closeOnce sync.Once
mtu int mtu int
dnsServers []netip.Addr
hasV4, hasV6 bool
} }
func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handleLocal bool) (tun.Device, *Net, *stack.Stack, error) { // CreateNetTUN returns a device with a TCP/IP stack on it, and what opens connections through that from
opts := stack.Options{ // localAddresses. With a handler the stack takes every connection that comes from the device, for any address.
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, // Without one it takes none
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handler xtun.ConnectionHandler) (tun.Device, *Net, error) {
HandleLocal: handleLocal,
}
dev := &netTun{ dev := &netTun{
ep: channel.New(1024, uint32(mtu), ""),
stack: stack.New(opts),
events: make(chan tun.Event, 10), events: make(chan tun.Event, 10),
incomingPacket: make(chan *buffer.View), packets: make(chan *buf.Buffer, netTunQueue),
closed: make(chan struct{}), closed: make(chan struct{}),
dnsServers: dnsServers,
mtu: mtu, mtu: mtu,
} }
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default stack, err := xtun.NewStack(context.Background(), xtun.StackOptions{Tun: dev, MTU: uint32(mtu), Addresses: localAddresses, Remote: true}, handler)
tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) if err != nil {
if tcpipErr != nil { return nil, nil, err
return nil, nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
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() {
protoNumber = ipv4.ProtocolNumber
} else if ip.Is6() {
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})
} }
dev.stack = stack
tnet := &Net{ tnet := &Net{
DialContextTCPAddrPort: dev.DialContextTCPAddrPort, DialContextTCPAddrPort: stack.DialTCP,
DialUDPAddrPort: dev.DialUDPAddrPort, DialUDPAddrPort: dev.DialUDPAddrPort,
dnsServers: dev.dnsServers, dnsServers: dnsServers,
hasV4: dev.hasV4, }
hasV6: dev.hasV6, for _, ip := range localAddresses {
if ip.Is4() {
tnet.hasV4 = true
} else if ip.Is6() {
tnet.hasV6 = true
}
} }
dev.events <- tun.EventUp dev.events <- tun.EventUp
return dev, tnet, dev.stack, nil return dev, tnet, nil
} }
func (tun *netTun) Name() (string, error) { func (tun *netTun) Name() (string, error) {
@@ -128,73 +89,62 @@ func (tun *netTun) Events() <-chan tun.Event {
} }
func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
var view *buffer.View
select { select {
case view = <-tun.incomingPacket: case packet := <-tun.packets:
sizes[0] = copy(buf[0][offset:], packet.Bytes())
packet.Release()
return 1, nil
case <-tun.closed: case <-tun.closed:
return 0, os.ErrClosed 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) { func (tun *netTun) Write(buf [][]byte, offset int) (int, error) {
for _, buf := range buf { for _, buf := range buf {
packet := buf[offset:] tun.stack.Input(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 return len(buf), nil
} }
func (tun *netTun) WriteNotify() { // WritePacket implements xtun.Tun. The packet is in the hands of the stack only until this returns, so it is copied
pkt := tun.ep.Read() // for Read. The stack does not wait for the one who reads the device, unless that one is far behind: it answers
if pkt == nil { // what it is written inside Write, and whoever writes would wait with it and lose what comes meanwhile
return func (tun *netTun) WritePacket(header, payload []byte) error {
} packet := buf.NewWithSize(int32(len(header) + len(payload)))
packet.Write(header)
view := pkt.ToView() packet.Write(payload)
pkt.DecRef()
select { select {
case tun.incomingPacket <- view: case tun.packets <- packet:
return nil
case <-tun.closed: case <-tun.closed:
view.Release() packet.Release()
return os.ErrClosed
} }
} }
// 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 { func (tun *netTun) Close() error {
tun.stack.RemoveNIC(1) tun.closeOnce.Do(func() {
tun.stack.Close() // before the stack ends its connections: nobody may be reading anymore what it writes for that
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) close(tun.closed)
} tun.stack.Close()
close(tun.events)
})
return nil return nil
} }
@@ -206,17 +156,8 @@ func (tun *netTun) BatchSize() int {
return 1 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) { func (tun *netTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) {
var pn tcpip.NetworkProtocolNumber = ipv6.ProtocolNumber conn, err := tun.stack.DialUDP()
if raddr.IsValid() || raddr.Port() > 0 {
_, pn = convertToFullAddr(raddr)
}
conn, err := gonet.DialUDP(tun.stack, nil, nil, pn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -234,20 +175,6 @@ type Net struct {
cache cache 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 ( var (
errNoSuchHost = errors.New("no such host") errNoSuchHost = errors.New("no such host")
errLameReferral = errors.New("lame referral") errLameReferral = errors.New("lame referral")
@@ -451,6 +378,8 @@ func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Q
return dnsmessage.Parser{}, dnsmessage.Header{}, err 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 p dnsmessage.Parser
var h dnsmessage.Header var h dnsmessage.Header
if useUDP { if useUDP {
@@ -458,8 +387,15 @@ func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Q
} else { } else {
p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq) p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq)
} }
stop()
c.Close() c.Close()
if err != nil { 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 { if err == context.Canceled {
err = errCanceled err = errCanceled
} else if err == context.DeadlineExceeded { } else if err == context.DeadlineExceeded {
+310
View File
@@ -0,0 +1,310 @@
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")
}
}
+13 -15
View File
@@ -21,13 +21,13 @@ import (
"github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats" "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"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
"golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
) )
type Server struct { type Server struct {
@@ -44,7 +44,6 @@ type Server struct {
downlinkCounter stats.Counter downlinkCounter stats.Counter
tun tun.Device tun tun.Device
stack *stack.Stack
dev *device.Device dev *device.Device
mu sync.Mutex mu sync.Mutex
@@ -95,11 +94,6 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
return nil, err 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)) pri := common.Must2(ParseKey(conf.SecretKey))
var pub [32]byte var pub [32]byte
curve25519.ScalarBaseMult(&pub, pri) curve25519.ScalarBaseMult(&pub, pri)
@@ -126,15 +120,15 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
tun: tun,
stack: stack,
pub: pub, pub: pub,
users: users, users: users,
} }
// Install the stack's protocol handlers before the device can deliver packets to it (Start -> dev.Up). // The stack hands its connections to s from the first packet on, which the device delivers after Start -> dev.Up.
CreateForwarder(stack, s.HandleConnection) tun, _, err := CreateNetTUN(localAddresses, nil, int(conf.Mtu), s)
CreateICMPEchoResponder(stack) if err != nil {
return nil, err
}
s.tun = tun
return s, nil return s, nil
} }
@@ -261,7 +255,7 @@ func (s *Server) Start() error {
if s.src.Address.Family().IsDomain() { if s.src.Address.Family().IsDomain() {
return errors.New("address is domain") return errors.New("address is domain")
} }
listenFunc := func() (net.PacketConn, error) { listenFunc := func(context.Context) (net.PacketConn, error) {
var pktConn net.PacketConn var pktConn net.PacketConn
var err error var err error
if s.streamSettings.FinalMask != nil { if s.streamSettings.FinalMask != nil {
@@ -379,9 +373,13 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
}) })
errors.LogInfo(ctx, "processing from ", source, " to ", dest) 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{ link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: buf.NewWriter(conn), Writer: writer,
} }
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil { if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err)) errors.LogError(ctx, errors.New("connection closed").Base(err))
+71
View File
@@ -0,0 +1,71 @@
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,27 +1,12 @@
package wireguard package wireguard
import ( import (
"context"
"fmt" "fmt"
"io"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "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) { func CalculateInterfaceName(name string) (tunName string) {
@@ -48,334 +33,3 @@ func CalculateInterfaceName(name string) (tunName string) {
tunName = fmt.Sprintf("%s%d", tunName, tunIndex) tunName = fmt.Sprintf("%s%d", tunName, tunIndex)
return 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)
}
}
+21 -48
View File
@@ -25,10 +25,6 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/hpack" "golang.org/x/net/http2/hpack"
"golang.org/x/sync/errgroup" "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/log"
"github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/app/proxyman"
@@ -64,54 +60,31 @@ const (
masqueAuthorization = "Basic dUBleGFtcGxlLmNvbTpw" 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) { func startMasqueServer(t *testing.T, h2 bool) (net.Port, [32]byte) {
dev, _, gstack, err := wireguard.CreateNetTUN([]netip.Addr{masqueServerV4, masqueServerV6}, nil, transmasque.MinPacketSize, false) dev, _, err := wireguard.CreateNetTUN([]netip.Addr{masqueServerV4, masqueServerV6}, nil, transmasque.MinPacketSize, masqueEcho{})
common.Must(err) common.Must(err)
t.Cleanup(func() { dev.Close() }) 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] var current atomic.Pointer[connectip.Conn]
go func() { go func() {
bufs := [][]byte{make([]byte, transmasque.MinPacketSize)} bufs := [][]byte{make([]byte, transmasque.MinPacketSize)}
+84 -41
View File
@@ -1,14 +1,17 @@
package scenarios package scenarios
import ( import (
"encoding/hex"
"testing" "testing"
//"time" "time"
"github.com/xtls/xray-core/app/log" "github.com/xtls/xray-core/app/log"
"github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
clog "github.com/xtls/xray-core/common/log" clog "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core" core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/infra/conf"
@@ -17,9 +20,18 @@ import (
"github.com/xtls/xray-core/proxy/wireguard" "github.com/xtls/xray-core/proxy/wireguard"
"github.com/xtls/xray-core/testing/servers/tcp" "github.com/xtls/xray-core/testing/servers/tcp"
"github.com/xtls/xray-core/testing/servers/udp" "github.com/xtls/xray-core/testing/servers/udp"
//"golang.org/x/sync/errgroup" "golang.org/x/crypto/curve25519"
"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) { func TestWireguard(t *testing.T) {
tcpServer := tcp.Server{ tcpServer := tcp.Server{
MsgProcessor: xor, MsgProcessor: xor,
@@ -28,18 +40,44 @@ func TestWireguard(t *testing.T) {
common.Must(err) common.Must(err)
defer tcpServer.Close() defer tcpServer.Close()
serverPrivate, _ := conf.ParseWireGuardKey("EGs4lTSJPmgELx6YiJAmPR2meWi6bY+e9rTdCipSj10=") udpServer := udp.Server{
serverPublic, _ := conf.ParseWireGuardKey("osAMIyil18HeZXGGBDC9KpZoM+L2iGyXWVSYivuM9B0=") MsgProcessor: xor,
clientPrivate, _ := conf.ParseWireGuardKey("CPQSpgxgdQRZa5SUbT3HLv+mmDVHLW5YR/rQlzum/2I=") }
clientPublic, _ := conf.ParseWireGuardKey("MmLJ5iHFVVBp7VsB0hxfpQ0wEzAbT2KQnpQpj0+RtBw=") udpDest, err := udpServer.Start()
common.Must(err)
defer udpServer.Close()
serverPrivate, _ := conf.ParseWireGuardKey("EGs4lTSJPmgELx6YiJAmPR2meWi6bY+e9rTdCipSj10=")
clientPrivate, _ := conf.ParseWireGuardKey("CPQSpgxgdQRZa5SUbT3HLv+mmDVHLW5YR/rQlzum/2I=")
// 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() serverPort := udp.PickPort()
serverConfig := &core.Config{ serverConfig := &core.Config{
App: []*serial.TypedMessage{ App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{ serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Debug, ErrorLogLevel: clog.Severity_Warning,
ErrorLogType: log.LogType_Console, 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{ Inbound: []*core.InboundHandlerConfig{
{ {
@@ -49,59 +87,64 @@ func TestWireguard(t *testing.T) {
}), }),
ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{ ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{
IsClient: false, IsClient: false,
NoKernelTun: false, Endpoint: []string{"10.0.0.1", "fd00::1"},
Endpoint: []string{"10.0.0.1"},
Mtu: 1420, Mtu: 1420,
SecretKey: serverPrivate, SecretKey: serverPrivate,
Peers: []*wireguard.PeerConfig{{ Users: []*protocol.User{{
PublicKey: serverPublic, Email: "love@xray.com",
AllowedIps: []string{"0.0.0.0/0", "::0/0"}, Account: serial.ToTypedMessage(&wireguard.PeerConfig{
PublicKey: wireguardPublicKey(clientPrivate),
AllowedIps: []string{"10.0.0.2/32", "fd00::2/128"},
}),
}}, }},
}), }),
}, },
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ redirect("tcp", dest),
ProxySettings: serial.ToTypedMessage(&freedom.Config{ redirect("udp", udpDest),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{}),
},
}, },
} }
clientPort := tcp.PickPort() clientPort := tcp.PickPort()
clientConfig := &core.Config{ client6Port := tcp.PickPort()
App: []*serial.TypedMessage{ clientUDPPort := udp.PickPort()
serial.ToTypedMessage(&log.Config{ dokodemoInbound := func(port net.Port, dest net.Destination) *core.InboundHandlerConfig {
ErrorLogLevel: clog.Severity_Debug, return &core.InboundHandlerConfig{
ErrorLogType: log.LogType_Console,
}),
},
Inbound: []*core.InboundHandlerConfig{
{
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(port)}},
Listen: net.NewIPOrDomain(net.LocalHostIP), Listen: net.NewIPOrDomain(net.LocalHostIP),
}), }),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(dest.Address), RewriteAddress: net.NewIPOrDomain(dest.Address),
RewritePort: uint32(dest.Port), RewritePort: uint32(dest.Port),
AllowedNetworks: []net.Network{net.Network_TCP}, AllowedNetworks: []net.Network{dest.Network},
}),
}
}
clientConfig := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&log.Config{
ErrorLogLevel: clog.Severity_Warning,
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)),
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{ ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{
IsClient: true, IsClient: true,
NoKernelTun: false, NoKernelTun: true,
Endpoint: []string{"10.0.0.2"}, Endpoint: []string{"10.0.0.2", "fd00::2"},
Mtu: 1420, Mtu: 1420,
SecretKey: clientPrivate, SecretKey: clientPrivate,
Peers: []*wireguard.PeerConfig{{ Peers: []*wireguard.PeerConfig{{
Endpoint: "127.0.0.1:" + serverPort.String(), Endpoint: "127.0.0.1:" + serverPort.String(),
PublicKey: clientPublic, PublicKey: wireguardPublicKey(serverPrivate),
AllowedIps: []string{"0.0.0.0/0", "::0/0"}, AllowedIps: []string{"0.0.0.0/0", "::0/0"},
}}, }},
}), }),
@@ -114,13 +157,13 @@ func TestWireguard(t *testing.T) {
common.Must(err) common.Must(err)
defer CloseAllServers(servers) defer CloseAllServers(servers)
// FIXME: for some reason wg server does not receive var errg errgroup.Group
for range 3 {
// var errg errgroup.Group errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*20))
// for i := 0; i < 1; i++ { }
// errg.Go(testTCPConn(clientPort, 1024, time.Second*2)) errg.Go(testTCPConn(client6Port, 1024*1024, time.Second*20))
// } errg.Go(testUDPConn(clientUDPPort, 1024, time.Second*5))
// if err := errg.Wait(); err != nil { if err := errg.Wait(); err != nil {
// t.Error(err) t.Error(err)
// } }
} }
@@ -255,6 +255,7 @@ type Config struct {
ResetMin int64 `protobuf:"varint,1,opt,name=reset_min,json=resetMin,proto3" json:"reset_min,omitempty"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -310,6 +311,13 @@ func (x *Config) GetItems() []*Item {
return nil return nil
} }
func (x *Config) GetScript() string {
if x != nil {
return x.Script
}
return ""
}
var File_transport_internet_finalmask_noise_config_proto protoreflect.FileDescriptor var File_transport_internet_finalmask_noise_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" + const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" +
@@ -337,11 +345,12 @@ const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" +
"\x06packet\x18\x05 \x01(\fR\x06packet\x12\x1b\n" + "\x06packet\x18\x05 \x01(\fR\x06packet\x12\x1b\n" +
"\tdelay_min\x18\x06 \x01(\x03R\bdelayMin\x12\x1b\n" + "\tdelay_min\x18\x06 \x01(\x03R\bdelayMin\x12\x1b\n" +
"\tdelay_max\x18\a \x01(\x03R\bdelayMax\x12L\n" + "\tdelay_max\x18\a \x01(\x03R\bdelayMax\x12L\n" +
"\bsegments\x18\b \x03(\v20.xray.transport.internet.finalmask.noise.SegmentR\bsegments\"\x87\x01\n" + "\bsegments\x18\b \x03(\v20.xray.transport.internet.finalmask.noise.SegmentR\bsegments\"\x9f\x01\n" +
"\x06Config\x12\x1b\n" + "\x06Config\x12\x1b\n" +
"\treset_min\x18\x01 \x01(\x03R\bresetMin\x12\x1b\n" + "\treset_min\x18\x01 \x01(\x03R\bresetMin\x12\x1b\n" +
"\treset_max\x18\x02 \x01(\x03R\bresetMax\x12C\n" + "\treset_max\x18\x02 \x01(\x03R\bresetMax\x12C\n" +
"\x05items\x18\x03 \x03(\v2-.xray.transport.internet.finalmask.noise.ItemR\x05itemsB\x97\x01\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" +
"+com.xray.transport.internet.finalmask.noiseP\x01Z<github.com/xtls/xray-core/transport/internet/finalmask/noise\xaa\x02'Xray.Transport.Internet.Finalmask.Noiseb\x06proto3" "+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 ( var (
@@ -37,4 +37,5 @@ message Config {
int64 reset_min = 1; int64 reset_min = 1;
int64 reset_max = 2; int64 reset_max = 2;
repeated Item items = 3; repeated Item items = 3;
string script = 4;
} }
+35 -6
View File
@@ -1,6 +1,7 @@
package noise package noise
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"net" "net"
@@ -10,6 +11,7 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
) )
const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -17,17 +19,27 @@ const asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
type noiseConn struct { type noiseConn struct {
net.PacketConn net.PacketConn
config *Config config *Config
script *scriptEngine
noise []scriptPacket
m map[string]time.Time m map[string]time.Time
mu sync.Mutex mu sync.Mutex
counter atomic.Uint32 counter atomic.Uint32
} }
func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
return &noiseConn{ conn := &noiseConn{
PacketConn: raw, PacketConn: raw,
config: c, config: c,
m: make(map[string]time.Time), m: make(map[string]time.Time),
}, nil }
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
} }
func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
@@ -97,13 +109,30 @@ func (c *noiseConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
t := c.m[addr.String()] t := c.m[addr.String()]
if t.IsZero() || (c.config.ResetMax > 0 && time.Now().After(t)) { if t.IsZero() || (c.config.ResetMax > 0 && time.Now().After(t)) {
for _, item := range c.config.Items { c.sendNoise(addr)
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) 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) 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
@@ -0,0 +1,148 @@
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
}
@@ -0,0 +1,70 @@
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")
}
}
}
})
}
@@ -0,0 +1,107 @@
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
}
@@ -0,0 +1,725 @@
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,17 +6,21 @@ import (
"encoding/binary" "encoding/binary"
"io" "io"
gonet "net" gonet "net"
"os"
"path/filepath"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/transport/internet/finalmask" "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/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "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/header"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" "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/salamander"
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku" "github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
) )
@@ -1043,3 +1047,70 @@ 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])
}
}
+45 -10
View File
@@ -8,6 +8,8 @@ package xdns
import ( import (
_ "github.com/xtls/xray-core/common/serial" _ "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" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
@@ -102,6 +104,9 @@ type ResolverProto struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -150,6 +155,27 @@ func (x *ResolverProto) GetAddr() string {
return "" 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 { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Domains []*DomainProto `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"` Domains []*DomainProto `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"`
@@ -214,17 +240,22 @@ var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescrip
const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" + const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" +
"\n" + "\n" +
".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\"\x8b\x01\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" +
"\vDomainProto\x12\x12\n" + "\vDomainProto\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" +
"\tlen_limit\x18\x02 \x01(\x05R\blenLimit\x12\x1f\n" + "\tlen_limit\x18\x02 \x01(\x05R\blenLimit\x12\x1f\n" +
"\vlabel_limit\x18\x03 \x01(\x05R\n" + "\vlabel_limit\x18\x03 \x01(\x05R\n" +
"labelLimit\x12\x14\n" + "labelLimit\x12\x14\n" +
"\x05types\x18\x04 \x03(\x05R\x05types\x12\x14\n" + "\x05types\x18\x04 \x03(\x05R\x05types\x12\x14\n" +
"\x05edns0\x18\x05 \x01(\x05R\x05edns0\"7\n" + "\x05edns0\x18\x05 \x01(\x05R\x05edns0\"\xd9\x01\n" +
"\rResolverProto\x12\x12\n" + "\rResolverProto\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\"\xcb\x01\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" +
"\x06Config\x12M\n" + "\x06Config\x12M\n" +
"\adomains\x18\x01 \x03(\v23.xray.transport.internet.finalmask.xdns.DomainProtoR\adomains\x12S\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" + "\tresolvers\x18\x02 \x03(\v25.xray.transport.internet.finalmask.xdns.ResolverProtoR\tresolvers\x12\x1d\n" +
@@ -249,15 +280,19 @@ var file_transport_internet_finalmask_xdns_config_proto_goTypes = []any{
(*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto (*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto
(*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto (*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto
(*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config (*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
} }
var file_transport_internet_finalmask_xdns_config_proto_depIdxs = []int32{ var file_transport_internet_finalmask_xdns_config_proto_depIdxs = []int32{
0, // 0: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto 3, // 0: xray.transport.internet.finalmask.xdns.ResolverProto.tls_config:type_name -> xray.transport.internet.tls.Config
1, // 1: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto 4, // 1: xray.transport.internet.finalmask.xdns.ResolverProto.quic_config:type_name -> xray.transport.internet.QuicParams
2, // [2:2] is the sub-list for method output_type 0, // 2: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto
2, // [2:2] is the sub-list for method input_type 1, // 3: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto
2, // [2:2] is the sub-list for extension type_name 4, // [4:4] is the sub-list for method output_type
2, // [2:2] is the sub-list for extension extendee 4, // [4:4] is the sub-list for method input_type
0, // [0:2] is the sub-list for field type_name 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
} }
func init() { file_transport_internet_finalmask_xdns_config_proto_init() } func init() { file_transport_internet_finalmask_xdns_config_proto_init() }
@@ -7,6 +7,8 @@ option java_package = "com.xray.transport.internet.finalmask.xdns";
option java_multiple_files = true; option java_multiple_files = true;
import "common/serial/typed_message.proto"; import "common/serial/typed_message.proto";
import "transport/internet/tls/config.proto";
import "transport/internet/config.proto";
message DomainProto { message DomainProto {
string name = 1; string name = 1;
@@ -19,6 +21,9 @@ message DomainProto {
message ResolverProto { message ResolverProto {
string type = 1; string type = 1;
string addr = 2; string addr = 2;
int32 worker = 3;
xray.transport.internet.tls.Config tls_config = 4;
xray.transport.internet.QuicParams quic_config = 5;
} }
message Config { message Config {
@@ -20,6 +20,8 @@ func NewResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, err
return NewTCPResolver(config, dialer) return NewTCPResolver(config, dialer)
case "udp": case "udp":
return NewUDPResolver(config, dialer) return NewUDPResolver(config, dialer)
case "https":
return NewDOHResolver(config, dialer)
default: default:
return nil, errors.New("unknown type") return nil, errors.New("unknown type")
} }
@@ -0,0 +1,301 @@
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)
}
+19 -1
View File
@@ -22,6 +22,12 @@ type interConn struct {
client bool client bool
user *protocol.MemoryUser user *protocol.MemoryUser
lock chan struct{}
mu sync.Mutex
}
func (c *interConn) unlock() {
<-c.lock
} }
func (c *interConn) User() *protocol.MemoryUser { func (c *interConn) User() *protocol.MemoryUser {
@@ -33,6 +39,9 @@ func (c *interConn) Read(b []byte) (int, error) {
} }
func (c *interConn) Write(b []byte) (int, error) { func (c *interConn) Write(b []byte) (int, error) {
c.lock <- struct{}{}
defer c.unlock()
if c.client { if c.client {
c.client = false c.client = false
if _, err := c.stream.Write(append(quicvarint.Append(nil, FrameTypeTCPRequest), b...)); err != nil { if _, err := c.stream.Write(append(quicvarint.Append(nil, FrameTypeTCPRequest), b...)); err != nil {
@@ -40,11 +49,20 @@ func (c *interConn) Write(b []byte) (int, error) {
} }
return len(b), nil return len(b), nil
} }
return c.stream.Write(b) return c.stream.Write(b)
} }
func (c *interConn) Close() error { 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) c.stream.CancelRead(0)
return c.stream.Close() return c.stream.Close()
} }
+1
View File
@@ -257,6 +257,7 @@ func (c *client) tcp(ctx context.Context) (stat.Connection, error) {
remote: c.conn.RemoteAddr(), remote: c.conn.RemoteAddr(),
client: true, client: true,
lock: make(chan struct{}, 1),
}, nil }, nil
} }
+1
View File
@@ -134,6 +134,7 @@ func (h *httpHandler) StreamDispatcher(ft http3.FrameType, stream *quic.Stream,
remote: h.conn.RemoteAddr(), remote: h.conn.RemoteAddr(),
user: h.user, user: h.user,
lock: make(chan struct{}, 1),
}) })
return true, nil return true, nil
default: default:
+9 -4
View File
@@ -2,12 +2,13 @@ package reality
import ( import (
"context" "context"
"crypto/ecdh"
"crypto/mldsa"
"io" "io"
"net" "net"
"os" "os"
"time" "time"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/reality" "github.com/xtls/reality"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
@@ -23,7 +24,6 @@ func (c *Config) GetREALITYConfig() *reality.Config {
Dest: c.Dest, Dest: c.Dest,
Xver: byte(c.Xver), Xver: byte(c.Xver),
PrivateKey: c.PrivateKey,
MinClientVer: c.MinClientVer, MinClientVer: c.MinClientVer,
MaxClientVer: c.MaxClientVer, MaxClientVer: c.MaxClientVer,
MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond, MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond,
@@ -33,9 +33,14 @@ func (c *Config) GetREALITYConfig() *reality.Config {
KeyLogWriter: KeyLogWriterFromConfig(c), KeyLogWriter: KeyLogWriterFromConfig(c),
} }
var err error
if config.PrivateKey, err = ecdh.X25519().NewPrivateKey(c.PrivateKey); err != nil {
panic(err)
}
if c.Mldsa65Seed != nil { if c.Mldsa65Seed != nil {
_, key := mldsa65.NewKeyFromSeed((*[32]byte)(c.Mldsa65Seed)) if config.Mldsa65Key, err = mldsa.NewPrivateKey(mldsa.MLDSA65(), c.Mldsa65Seed); err != nil {
config.Mldsa65Key = key.Bytes() panic(err)
}
} }
if c.LimitFallbackUpload != nil { if c.LimitFallbackUpload != nil {
config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes
+9 -10
View File
@@ -5,7 +5,9 @@ import (
"context" "context"
"crypto/ecdh" "crypto/ecdh"
"crypto/ed25519" "crypto/ed25519"
"crypto/hkdf"
"crypto/hmac" "crypto/hmac"
"crypto/mldsa"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
gotls "crypto/tls" gotls "crypto/tls"
@@ -22,7 +24,6 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
utls "github.com/refraction-networking/utls" utls "github.com/refraction-networking/utls"
"github.com/xtls/reality" "github.com/xtls/reality"
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
@@ -31,7 +32,6 @@ import (
"github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/crypto/hkdf"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@@ -114,8 +114,8 @@ func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x50
if len(certs[0].Extensions) > 0 { if len(certs[0].Extensions) > 0 {
h.Write(c.HandshakeState.Hello.Raw) h.Write(c.HandshakeState.Hello.Raw)
h.Write(c.HandshakeState.ServerHello.Raw) h.Write(c.HandshakeState.ServerHello.Raw)
verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify) verify, _ := mldsa.NewPublicKey(mldsa.MLDSA65(), c.Config.Mldsa65Verify)
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) { if mldsa.Verify(verify, h.Sum(nil), certs[0].Extensions[0].Value, nil) == nil {
c.Verified = true c.Verified = true
return nil 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) publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey)
if err != nil { if err != nil {
return nil, errors.New("REALITY: publicKey == nil") return nil, errors.New("REALITY: nil publicKey").Base(err)
} }
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil { if ecdhe == nil {
@@ -185,12 +185,11 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
if ecdhe == nil { 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.") return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.")
} }
uConn.AuthKey, _ = ecdhe.ECDH(publicKey) if uConn.AuthKey, err = ecdhe.ECDH(publicKey); err != nil {
if uConn.AuthKey == nil { return nil, errors.New("REALITY: nil sharedKey").Base(err)
return nil, errors.New("REALITY: SharedKey == nil")
} }
if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil { if uConn.AuthKey, err = hkdf.Key(sha256.New, uConn.AuthKey, hello.Random[:20], "REALITY", 32); err != nil {
return nil, err return nil, errors.New("REALITY: nil authKey").Base(err)
} }
aead := crypto.NewAesGcm(uConn.AuthKey) aead := crypto.NewAesGcm(uConn.AuthKey)
if config.Show { if config.Show {
+19
View File
@@ -28,3 +28,22 @@ func (v *SocketConfig) ParseTFOValue() int {
} }
return tfo 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 != "" { if config.Interface != "" {
iface, err := net.InterfaceByName(config.Interface) iface, err := net.InterfaceByName(config.Interface)
if err != nil { if err != nil {
return errors.New("failed to get interface ", config.Interface).Base(err) return NewBindError(errors.New("failed to get interface ", config.Interface).Base(err))
} }
if network == "tcp6" || network == "udp6" { if network == "tcp6" || network == "udp6" {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil { if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil {
return errors.New("failed to set IPV6_BOUND_IF").Base(err) return NewBindError(errors.New("failed to set IPV6_BOUND_IF").Base(err))
} }
} else { } else {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil { if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil {
return errors.New("failed to set IP_BOUND_IF").Base(err) return NewBindError(errors.New("failed to set IP_BOUND_IF").Base(err))
} }
} }
} }
+1 -1
View File
@@ -22,7 +22,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
if config.Interface != "" { if config.Interface != "" {
if err := syscall.BindToDevice(int(fd), config.Interface); err != nil { if err := syscall.BindToDevice(int(fd), config.Interface); err != nil {
return errors.New("failed to set Interface").Base(err) return NewBindError(errors.New("failed to set Interface").Base(err))
} }
} }
+5 -5
View File
@@ -35,7 +35,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
if config.Interface != "" { if config.Interface != "" {
inf, err := net.InterfaceByName(config.Interface) inf, err := net.InterfaceByName(config.Interface)
if err != nil { if err != nil {
return errors.New("failed to find the interface").Base(err) return NewBindError(errors.New("failed to find the interface").Base(err))
} }
// easy way to check if the address is ipv4 // easy way to check if the address is ipv4
isV4 := strings.Contains(address, ".") isV4 := strings.Contains(address, ".")
@@ -47,20 +47,20 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
binary.BigEndian.PutUint32(bytes[:], uint32(inf.Index)) binary.BigEndian.PutUint32(bytes[:], uint32(inf.Index))
idx := *(*uint32)(unsafe.Pointer(&bytes[0])) idx := *(*uint32)(unsafe.Pointer(&bytes[0]))
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)); err != nil { if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)); err != nil {
return errors.New("failed to set IP_UNICAST_IF").Base(err) return NewBindError(errors.New("failed to set IP_UNICAST_IF").Base(err))
} }
if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) { if 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 { if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, int(idx)); err != nil {
return errors.New("failed to set IP_MULTICAST_IF").Base(err) return NewBindError(errors.New("failed to set IP_MULTICAST_IF").Base(err))
} }
} }
} else { } else {
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, inf.Index); err != nil { if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, inf.Index); err != nil {
return errors.New("failed to set IPV6_UNICAST_IF").Base(err) return NewBindError(errors.New("failed to set IPV6_UNICAST_IF").Base(err))
} }
if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) { if 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 { if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_IF, inf.Index); err != nil {
return errors.New("failed to set IPV6_MULTICAST_IF").Base(err) return NewBindError(errors.New("failed to set IPV6_MULTICAST_IF").Base(err))
} }
} }
} }
+24 -10
View File
@@ -68,19 +68,27 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
} }
} }
var lc net.ListenConfig var lc net.ListenConfig
if sockopt != nil || len(Controllers) > 0 {
lc.Control = func(network, address string, c syscall.RawConn) error { lc.Control = func(network, address string, c syscall.RawConn) error {
for _, ctl := range Controllers { for _, ctl := range Controllers {
if err := ctl(network, address, c); err != nil { if err := ctl(network, address, c); err != nil {
errors.LogInfoInner(ctx, err, "failed to apply external controller") return errors.New("failed to apply external controller").Base(err)
} }
} }
return c.Control(func(fd uintptr) {
if sockopt != nil { if sockopt != nil {
if err := applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); err != nil { var sockErr, controlErr error
errors.LogInfo(ctx, err, "failed to apply socket options") controlErr = c.Control(func(fd uintptr) {
} if sockErr = applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); sockErr != nil {
errors.LogInfoInner(ctx, sockErr, "failed to apply socket options")
} }
}) })
if _, ok := sockErr.(*BindError); ok {
return sockErr
}
return controlErr
}
return nil
}
} }
packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String()) packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String())
if err != nil { if err != nil {
@@ -128,16 +136,22 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
dialer.Control = func(network, address string, c syscall.RawConn) error { dialer.Control = func(network, address string, c syscall.RawConn) error {
for _, ctl := range Controllers { for _, ctl := range Controllers {
if err := ctl(network, address, c); err != nil { if err := ctl(network, address, c); err != nil {
errors.LogInfoInner(ctx, err, "failed to apply external controller") return errors.New("failed to apply external controller").Base(err)
} }
} }
return c.Control(func(fd uintptr) {
if sockopt != nil { if sockopt != nil {
if err := applyOutboundSocketOptions(network, address, fd, sockopt); err != nil { var sockErr, controlErr error
errors.LogInfoInner(ctx, err, "failed to apply socket options") controlErr = c.Control(func(fd uintptr) {
} if sockErr = applyOutboundSocketOptions(network, address, fd, sockopt); sockErr != nil {
errors.LogInfoInner(ctx, sockErr, "failed to apply socket options")
} }
}) })
if _, ok := sockErr.(*BindError); ok {
return sockErr
}
return controlErr
}
return nil
} }
} }

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