Compare commits

...
19 Commits
Author SHA1 Message Date
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
c77a4d0e49 MASQUE client: Support XMUX ("maxConnections": 1 by default); Use Chrome's User-Agent by default (#7121)
https://github.com/XTLS/Xray-core/pull/6810#issuecomment-5842441136
https://github.com/XTLS/Xray-core/pull/6878#issuecomment-6095112915
https://github.com/XTLS/Xray-core/pull/7121#issuecomment-6117636043

---------

Co-authored-by: SVLAVR <skugorov.s@gmail.com>
2026-10-12 19:40:04 +00:00
MeowandGitHub 5a64db009b Lua: Support inline scripts; Add ./xray base64 encode/decode [-gzip] command (#7132)
https://github.com/XTLS/Xray-core/pull/7124#issuecomment-6110557806
https://github.com/XTLS/Xray-core/pull/7132#issuecomment-6118169645
https://github.com/XTLS/Xray-core/pull/7132#issuecomment-6126428359
2026-10-12 16:58:20 +00:00
SVLAVRandGitHub 2bea2df6a1 Hysteria client: Release client map lock before cleanup (#7122)
https://github.com/XTLS/Xray-core/pull/7122#issuecomment-6119280343
2026-10-12 06:56:24 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb2dcedbb7 Bump golang.org/x/net from 0.59.0 to 0.60.0 (#7133)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.59.0 to 0.60.0.
- [Commits](https://github.com/golang/net/compare/v0.59.0...v0.60.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.60.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-12 05:16:45 +00:00
7c2e05dc47 Hysteria client: Honor cancellation during dialing (#7119)
Fixes https://github.com/XTLS/Xray-core/issues/7120

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-10-12 05:01:23 +00:00
brookwkoandRPRX 1dd2a0bd3c TUN inbound: Support autoSystemDnsToGateway on FreeBSD as well (#7123)
https://github.com/XTLS/Xray-core/issues/6454#issuecomment-5859521287
https://github.com/XTLS/Xray-core/pull/6691#issuecomment-5445818753
https://github.com/XTLS/Xray-core/pull/6773 & https://github.com/XTLS/Xray-core/pull/6853
2026-10-12 02:48:59 +00:00
Artem LytkinandRPRX 70ef97d564 TUN inbound: Use Xray-core's built-in lightweight TCP/IP stack instead of gVisor (#7130)
https://github.com/XTLS/Xray-core/pull/6771#issuecomment-5752154561
https://github.com/XTLS/Xray-core/pull/6795#issuecomment-6095740152
https://github.com/XTLS/Xray-core/pull/7130#issuecomment-6116258188
2026-10-12 02:18:56 +00:00
151 changed files with 11839 additions and 2891 deletions
+3 -4
View File
@@ -266,10 +266,9 @@ type Config struct {
DisableFallback bool `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"`
DisableFallbackIfMatch bool `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,omitempty"`
EnableParallelQuery bool `protobuf:"varint,14,opt,name=enableParallelQuery,proto3" json:"enableParallelQuery,omitempty"`
// Absolute path to the Lua DNS query script.
Script string `protobuf:"bytes,15,opt,name=script,proto3" json:"script,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Script string `protobuf:"bytes,15,opt,name=script,proto3" json:"script,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
-1
View File
@@ -75,6 +75,5 @@ message Config {
bool enableParallelQuery = 14;
// Absolute path to the Lua DNS query script.
string script = 15;
}
+10 -6
View File
@@ -32,7 +32,6 @@ type DNS struct {
matcherInfos []*DomainMatcherInfo
checkSystem bool
script *scriptEngine
scriptPath string
}
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher.
@@ -171,6 +170,13 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, NewLocalDNSClient(ipOption))
}
var script *scriptEngine
if config.Script != "" {
if script, err = newScriptEngine(config.Script); err != nil {
return nil, errors.New("failed to prepare DNS script").Base(err)
}
}
return &DNS{
hosts: hosts,
ipOption: &ipOption,
@@ -182,7 +188,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
disableFallbackIfMatch: config.DisableFallbackIfMatch,
enableParallelQuery: config.EnableParallelQuery,
checkSystem: checkSystem,
scriptPath: config.Script,
script: script,
}, nil
}
@@ -193,12 +199,10 @@ func (*DNS) Type() interface{} {
// Start implements common.Runnable.
func (s *DNS) Start() error {
if s.scriptPath != "" {
engine, err := newScriptEngine(s.scriptPath, s)
if err != nil {
if s.script != nil {
if err := s.script.start(s); err != nil {
return errors.New("failed to initialize DNS script").Base(err)
}
s.script = engine
}
return nil
}
+1 -1
View File
@@ -52,7 +52,7 @@ func luaServers(s *DNS) []luaDNSServer {
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
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)
for i, client := range servers {
+4 -1
View File
@@ -72,10 +72,13 @@ end
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
b.Fatal(err)
}
engine, err := newScriptEngine(path, server)
engine, err := newScriptEngine(path)
if err != nil {
b.Fatal(err)
}
if err := engine.start(server); err != nil {
b.Fatal(err)
}
b.Cleanup(engine.close)
for _, bench := range []struct {
name string
+16 -8
View File
@@ -15,17 +15,22 @@ import (
const scriptExecutionTimeout = 6 * time.Second
type scriptEngine struct {
pool *xlua.Pool
program *xlua.Program
pool *xlua.Pool
}
func newScriptEngine(path string, server *DNS) (*scriptEngine, error) {
program, err := xlua.CompileFile(path)
func newScriptEngine(script string) (*scriptEngine, error) {
program, err := xlua.Compile(script)
if err != nil {
return nil, err
}
return &scriptEngine{program: program}, nil
}
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, program.NewStateFactory(
func (e *scriptEngine) start(server *DNS) error {
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
scriptExecutionTimeout*20,
256,
func(L *lua.LState) {
geodata.RegisterLua(L)
log.RegisterLua(L)
@@ -38,15 +43,18 @@ func newScriptEngine(path string, server *DNS) (*scriptEngine, error) {
return nil
}))
if err != nil {
return nil, err
return err
}
errors.LogInfo(server.ctx, "DNS script initialized from ", path)
return &scriptEngine{pool: pool}, nil
e.pool = pool
errors.LogInfo(server.ctx, "DNS script initialized")
return nil
}
func (e *scriptEngine) close() {
e.pool.Close()
if e.pool != nil {
e.pool.Close()
}
}
func (e *scriptEngine) query(domain string, option dns.IPOption) (ips []net.IP, ttl uint32, queryErr error) {
+67 -18
View File
@@ -85,10 +85,13 @@ end
ctx: context.Background(),
clients: []*Client{{server: upstream, ipOption: &option, timeoutMs: time.Second}},
}
engine, err := newScriptEngine(path, server)
engine, err := newScriptEngine(path)
if err != nil {
t.Fatal(err)
}
if err := engine.start(server); err != nil {
t.Fatal(err)
}
defer engine.close()
ips, ttl, err := engine.query("good.example", option)
@@ -164,11 +167,15 @@ end
if err != nil {
t.Fatal(err)
}
engine, err := newScriptEngine(scriptPath)
if err != nil {
t.Fatal(err)
}
server := &DNS{
ctx: context.Background(),
hosts: hosts,
ipOption: &option,
scriptPath: scriptPath,
ctx: context.Background(),
hosts: hosts,
ipOption: &option,
script: engine,
clients: []*Client{
{id: "primary", server: primary, ipOption: &option, timeoutMs: 2 * time.Second},
{id: "fallback", server: fallback, ipOption: &option, timeoutMs: 2 * time.Second},
@@ -202,29 +209,67 @@ end
func TestDNSScriptRejectsInvalidStartup(t *testing.T) {
for _, tc := range []struct {
name string
script string
name, script string
compileError bool
}{
{"syntax", "function HandleDNSQuery("},
{"missing hook", "value = 1"},
{"top-level error", `error("setup failed")`},
{"syntax", "function HandleDNSQuery(", true},
{"missing hook", "value = 1", false},
{"invalid hook", "HandleDNSQuery = true", false},
{"top-level error", `error("setup failed")`, false},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "script.lua")
if err := os.WriteFile(path, []byte(tc.script), 0o600); err != nil {
t.Fatal(err)
}
server := &DNS{ctx: context.Background(), scriptPath: path}
server, err := New(context.Background(), &Config{Script: path})
if tc.compileError {
if server != nil || err == nil || !strings.Contains(err.Error(), "failed to prepare DNS script") {
t.Fatalf("New = %v, %v; want nil server and script preparation error", server, err)
}
return
}
if err != nil {
t.Fatal(err)
}
defer server.Close()
if server.script.program == nil || server.script.pool != nil {
t.Fatal("New did not compile the script without starting its pool")
}
if err := server.Close(); err != nil {
t.Fatal(err)
}
if err := server.Start(); err == nil {
t.Fatal("Start accepted an invalid DNS script")
}
if server.script != nil {
t.Fatal("Start retained a script engine after failure")
if server.script.pool != nil {
t.Fatal("Start retained a script pool after failure")
}
})
}
}
func TestDNSScriptProgramSnapshot(t *testing.T) {
path := filepath.Join(t.TempDir(), "script.lua")
if err := os.WriteFile(path, []byte(`function HandleDNSQuery() return nil, nil, "compiled" end`), 0o600); err != nil {
t.Fatal(err)
}
engine, err := newScriptEngine(path)
if err != nil {
t.Fatal(err)
}
defer engine.close()
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := engine.start(&DNS{ctx: context.Background()}); err != nil {
t.Fatal(err)
}
if _, _, err := engine.query("example.com", featureDNS.IPOption{}); err == nil || !strings.Contains(err.Error(), "compiled") {
t.Fatalf("query error = %v, want compiled", err)
}
}
func TestDNSScriptFakeDNSOption(t *testing.T) {
path := filepath.Join(t.TempDir(), "script.lua")
script := `
@@ -251,12 +296,16 @@ end
answers: map[string]net.IP{"good.example": net.ParseIP("198.18.0.1")},
ttl: 30,
}
engine, err := newScriptEngine(path)
if err != nil {
t.Fatal(err)
}
server := &DNS{
ctx: context.Background(),
hosts: hosts,
ipOption: &option,
scriptPath: path,
clients: []*Client{{id: "fake", server: upstream, ipOption: &option, timeoutMs: time.Second}},
ctx: context.Background(),
hosts: hosts,
ipOption: &option,
script: engine,
clients: []*Client{{id: "fake", server: upstream, ipOption: &option, timeoutMs: time.Second}},
}
if err := server.Start(); err != nil {
t.Fatal(err)
+7
View File
@@ -23,6 +23,9 @@ import (
const idleTimeout = 30 * time.Second
// DO NOT EDIT. Ignored by geodata, rejected by Lua.
const luaGuardHeader = "\x12\x01\x00"
type stage struct {
target 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 {
temp.Close()
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
return stats
}
stats.Average = time.Duration(int(sum) / cnt)
stats.Average = sum / time.Duration(cnt)
var std float64
if cnt < 2 {
// no enough data for standard deviation, we assume it's half of the average rtt
+1 -1
View File
@@ -174,7 +174,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound
Mark: streamSettings.SocketSettings.Mark,
})
}
if streamSettings != nil && streamSettings.ProtocolName == "splithttp" {
if streamSettings != nil && (streamSettings.ProtocolName == "splithttp" || streamSettings.ProtocolName == "masque") {
ctx = session.ContextWithAllowedNetwork(ctx, net.Network_UDP)
}
+3 -4
View File
@@ -587,10 +587,9 @@ type Config struct {
DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"`
Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`
BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"`
// Absolute path to the Lua routing script.
Script string `protobuf:"bytes,4,opt,name=script,proto3" json:"script,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Script string `protobuf:"bytes,4,opt,name=script,proto3" json:"script,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
-1
View File
@@ -110,6 +110,5 @@ message Config {
DomainStrategy domain_strategy = 1;
repeated RoutingRule rule = 2;
repeated BalancingRule balancing_rule = 3;
// Absolute path to the Lua routing script.
string script = 4;
}
+3 -3
View File
@@ -30,9 +30,9 @@ func (r *Router) RegisterLua(L *lua.LState) {
module.RawSetString("LocalOS", lua.LString(runtime.GOOS))
module.RawSetString("PickOutbound", L.NewFunction(func(L *lua.LState) int {
tag, ok := L.Get(2).(lua.LString)
tag, ok := L.Get(1).(lua.LString)
if !ok {
L.ArgError(2, "balancer tag must be a string")
L.ArgError(1, "balancer tag must be a string")
return 0
}
balancer, found := (*r.balancers.Load())[string(tag)]
@@ -62,7 +62,7 @@ func (r *Router) RegisterLua(L *lua.LState) {
}
func registerLuaContext(L *lua.LState) {
pushIPs := xlua.NewSlicePusher[net.IP](L)
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
attributes := L.NewTypeMetatable(luaAttributesType)
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
values := L.CheckUserData(1).Value.(map[string]string)
+10 -6
View File
@@ -20,7 +20,6 @@ import (
type Router struct {
domainStrategy Config_DomainStrategy
rules atomic.Pointer[[]*Rule]
scriptPath string
script *scriptEngine
balancers atomic.Pointer[map[string]*Balancer]
dns dns.Client
@@ -42,12 +41,19 @@ type Route struct {
// Init initializes the Router.
func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) error {
r.domainStrategy = config.DomainStrategy
r.scriptPath = config.Script
r.dns = d
r.ctx = ctx
r.ohm = ohm
r.dispatcher = dispatcher
if config.Script != "" {
script, err := newScriptEngine(config.Script)
if err != nil {
return errors.New("failed to prepare routing script").Base(err)
}
r.script = script
}
r.rules.Store(new([]*Rule))
r.balancers.Store(&map[string]*Balancer{})
return r.ReloadRules(config, false)
@@ -228,12 +234,10 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
// Start implements common.Runnable.
func (r *Router) Start() error {
if r.scriptPath != "" {
engine, err := newScriptEngine(r.scriptPath, r)
if err != nil {
if r.script != nil {
if err := r.script.start(r); err != nil {
return errors.New("failed to initialize routing script").Base(err)
}
r.script = engine
}
return nil
}
+16 -8
View File
@@ -16,17 +16,22 @@ import (
const scriptExecutionTimeout = 6 * time.Second
type scriptEngine struct {
pool *xlua.Pool
program *xlua.Program
pool *xlua.Pool
}
func newScriptEngine(path string, router *Router) (*scriptEngine, error) {
program, err := xlua.CompileFile(path)
func newScriptEngine(script string) (*scriptEngine, error) {
program, err := xlua.Compile(script)
if err != nil {
return nil, err
}
return &scriptEngine{program: program}, nil
}
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, program.NewStateFactory(
func (e *scriptEngine) start(router *Router) error {
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
scriptExecutionTimeout*20,
256,
func(L *lua.LState) {
geodata.RegisterLua(L)
log.RegisterLua(L)
@@ -40,15 +45,18 @@ func newScriptEngine(path string, router *Router) (*scriptEngine, error) {
return nil
}))
if err != nil {
return nil, err
return err
}
errors.LogInfo(router.ctx, "routing script initialized from ", path)
return &scriptEngine{pool: pool}, nil
e.pool = pool
errors.LogInfo(router.ctx, "routing script initialized")
return nil
}
func (e *scriptEngine) close() {
e.pool.Close()
if e.pool != nil {
e.pool.Close()
}
}
func (e *scriptEngine) pickRoute(ctx routing.Context) (routing.Route, error) {
+48 -9
View File
@@ -72,24 +72,63 @@ func startLuaRouter(t *testing.T, script string, d featureDNS.Client, config *Co
}
func TestRouterScriptStartup(t *testing.T) {
for _, tc := range []struct{ name, script string }{
{"syntax error", "function HandleRoute("},
{"missing hook", "value = 1"},
{"initialization error", `error("setup failed")`},
for _, tc := range []struct {
name, script string
compileError bool
}{
{"syntax error", "function HandleRoute(", true},
{"missing hook", "value = 1", false},
{"invalid hook", "HandleRoute = true", false},
{"initialization error", `error("setup failed")`, false},
} {
t.Run(tc.name, func(t *testing.T) {
r := new(Router)
if err := r.Init(context.Background(), &Config{Script: writeRouteScript(t, tc.script)}, nil, nil, nil); err != nil {
err := r.Init(context.Background(), &Config{Script: writeRouteScript(t, tc.script)}, nil, nil, nil)
if tc.compileError {
if r.script != nil || err == nil || !strings.Contains(err.Error(), "failed to prepare routing script") {
t.Fatalf("Init = %v; want no engine and script preparation error", err)
}
return
}
if err != nil {
t.Fatal(err)
}
defer r.Close()
if r.script.program == nil || r.script.pool != nil {
t.Fatal("Init did not compile the script without starting its pool")
}
if err := r.Close(); err != nil {
t.Fatal(err)
}
if err := r.Start(); err == nil {
t.Fatal("Start accepted an invalid routing script")
}
if r.script.pool != nil {
t.Fatal("Start retained a script pool after failure")
}
})
}
}
func TestRouterScriptProgramSnapshot(t *testing.T) {
path := writeRouteScript(t, `function HandleRoute() return "compiled", "compiled-rule" end`)
engine, err := newScriptEngine(path)
if err != nil {
t.Fatal(err)
}
defer engine.close()
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := engine.start(&Router{ctx: context.Background()}); err != nil {
t.Fatal(err)
}
route, err := engine.pickRoute(newLuaRouteTestContext())
if err != nil || route == nil || route.GetOutboundTag() != "compiled" || route.GetRuleTag() != "compiled-rule" {
t.Fatalf("route = %v, %v; want compiled, compiled-rule", route, err)
}
}
func TestRouterScriptRouting(t *testing.T) {
for _, tc := range []struct {
name, body string
@@ -101,7 +140,7 @@ func TestRouterScriptRouting(t *testing.T) {
{name: "route", body: `return "lua-out", "lua-rule"`, wantTag: "lua-out", wantRule: "lua-rule", wantCalls: "2"},
{name: "no match", body: `return nil`, wantErr: common.ErrNoClue, wantCalls: "2"},
{name: "empty tag", body: `return ""`, wantErr: common.ErrNoClue, wantCalls: "2"},
{name: "balancer error", body: `local tag, err = router:PickOutbound("missing"); return tag, nil, err`, wantMessage: "not found", wantCalls: "2"},
{name: "balancer error", body: `local tag, err = router.PickOutbound("missing"); return tag, nil, err`, wantMessage: "not found", wantCalls: "2"},
{name: "string error", body: `return nil, nil, "blocked"`, wantMessage: "blocked", wantCalls: "2"},
{name: "invalid tag", body: `return false`, wantMessage: "outboundTag", wantCalls: "2"},
{name: "invalid rule", body: `return "lua-out", false`, wantMessage: "ruleTag", wantCalls: "2"},
@@ -202,7 +241,7 @@ func TestRouterScriptBalancerReload(t *testing.T) {
r := startLuaRouter(t, `
local router = require("xray.router")
function HandleRoute()
local tag, err = router:PickOutbound("balance")
local tag, err = router.PickOutbound("balance")
return tag, "balanced", err
end`, nil, config("old"))
pick := func(want string) {
@@ -236,7 +275,7 @@ func TestRouterScriptConcurrentBalancerReload(t *testing.T) {
r := startLuaRouter(t, `
local router = require("xray.router")
function HandleRoute()
local tag, err = router:PickOutbound("balance")
local tag, err = router.PickOutbound("balance")
return tag, nil, err
end`, nil, config("a"))
@@ -367,10 +406,10 @@ end`)
if err != nil {
t.Fatal(err)
}
defer instance.Close()
if err := instance.Start(); err != nil {
t.Fatal(err)
}
defer instance.Close()
r := instance.GetFeature(routing.RouterType()).(*Router)
route, err := r.PickRoute(newLuaRouteTestContext())
if err != nil || route.GetOutboundTag() != "direct" || route.GetRuleTag() != "outer-route" {
+6
View File
@@ -0,0 +1,6 @@
package base64
const (
Prefix = "base64:"
GzipPrefix = "base64+gzip:"
)
+38
View File
@@ -0,0 +1,38 @@
package base64
import (
"bytes"
"compress/gzip"
"encoding/base64"
"io"
"strings"
"github.com/xtls/xray-core/common/errors"
)
// Decode decodes Base64 text with an optional [Prefix] or [GzipPrefix].
func Decode(value string) ([]byte, error) {
value, zip := strings.CutPrefix(value, GzipPrefix)
if !zip {
value = strings.TrimPrefix(value, Prefix)
}
data, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return nil, errors.New("failed to decode base64").Base(err)
}
if !zip {
return data, nil
}
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, errors.New("failed to decompress gzip").Base(err)
}
defer reader.Close()
data, err = io.ReadAll(reader)
if err != nil {
return nil, errors.New("failed to decompress gzip").Base(err)
}
return data, nil
}
+28
View File
@@ -0,0 +1,28 @@
package base64
import (
"bytes"
"testing"
)
func TestDecodeFormats(t *testing.T) {
compressed, err := Encode([]byte("x"), true)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, value, want string
}{
{"unprefixed", "eA==", "x"},
{"base64", Prefix + "eA==", "x"},
{"gzip", compressed, "x"},
{"empty", Prefix, ""},
} {
t.Run(tc.name, func(t *testing.T) {
data, err := Decode(tc.value)
if err != nil || !bytes.Equal(data, []byte(tc.want)) {
t.Fatalf("Decode = %q, %v; want %q", data, err, tc.want)
}
})
}
}
+29
View File
@@ -0,0 +1,29 @@
package base64
import (
"bytes"
"compress/gzip"
"encoding/base64"
"github.com/xtls/xray-core/common/errors"
)
// Encode returns prefixed Base64 text, optionally compressed with gzip at the highest level.
func Encode(data []byte, zip bool) (string, error) {
if !zip {
return Prefix + base64.StdEncoding.EncodeToString(data), nil
}
var buf bytes.Buffer
writer, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return "", errors.New("failed to compress gzip").Base(err)
}
if _, err := writer.Write(data); err != nil {
return "", errors.New("failed to compress gzip").Base(err)
}
if err := writer.Close(); err != nil {
return "", errors.New("failed to compress gzip").Base(err)
}
return GzipPrefix + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
+62
View File
@@ -0,0 +1,62 @@
package base64
import (
"bytes"
"compress/gzip"
"encoding/base64"
"io"
"strings"
"testing"
)
func TestEncodeFormats(t *testing.T) {
for _, tc := range []struct {
name string
data []byte
}{
{"empty", nil},
{"bytes", []byte("你好\r\n\x00\xff")},
} {
t.Run(tc.name, func(t *testing.T) {
for _, zip := range []bool{false, true} {
prefix := Prefix
if zip {
prefix = GzipPrefix
}
t.Run(prefix, func(t *testing.T) {
value, err := Encode(tc.data, zip)
if err != nil {
t.Fatal(err)
}
payload, ok := strings.CutPrefix(value, prefix)
if !ok {
t.Fatalf("encoded value does not start with %q", prefix)
}
// Read the output independently of Decode so matching bugs cannot hide.
data, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
t.Fatal(err)
}
if zip {
// gzip's XFL byte identifies the compression level we selected.
if len(data) < 10 || data[8] != 2 {
t.Fatal("gzip output does not use the highest compression level")
}
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
t.Fatal(err)
}
defer reader.Close()
data, err = io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
}
if !bytes.Equal(data, tc.data) {
t.Fatal("encoded output does not preserve the input bytes")
}
})
}
})
}
}
+19 -14
View File
@@ -17,12 +17,12 @@ import (
)
func checkFile(file, code string) error {
r, err := filesystem.OpenAsset(file)
r, size, err := filesystem.OpenAsset(file)
if err != nil {
return errors.New("failed to open ", file).Base(err)
}
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 nil
@@ -30,12 +30,12 @@ func checkFile(file, code string) error {
func loadFile(file, code string) ([]byte, error) {
runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file)
r, size, err := filesystem.OpenAsset(file)
if err != nil {
return nil, errors.New("failed to open ", file).Base(err)
}
defer r.Close()
bs, err := find(r, []byte(code), true)
bs, err := find(r, []byte(code), true, size)
if err != nil {
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.
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file)
r, size, err := filesystem.OpenAsset(file)
if err != nil {
return errors.New("failed to open ", file).Base(err)
}
defer r.Close()
br := bufio.NewReaderSize(r, 64*1024)
n, err := seek(br, []byte(code))
n, err := seek(br, []byte(code), size)
if err != nil {
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 {
return 0, err
}
if shift == 63 && b > 1 {
break // Only one value bit fits in the tenth byte.
}
x |= (uint64(b) & 0x7F) << shift
if (b & 0x80) == 0 {
return x, nil
@@ -123,9 +126,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
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)
bodyL, err := seek(br, code)
bodyL, err := seek(br, code, size)
if err != nil || !readBody {
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.
func seek(br *bufio.Reader, code []byte) (int, error) {
func seek(br *bufio.Reader, code []byte, size int64) (int, error) {
codeL := len(code)
if codeL == 0 {
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
for {
if _, err := br.ReadByte(); err != nil {
tag, err := br.ReadByte()
if err != nil {
return 0, err
}
@@ -153,10 +157,10 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
if err != nil {
return 0, err
}
bodyL := int(x)
if bodyL <= 0 {
return 0, errors.New("invalid body length: ", bodyL)
if x == 0 || x > uint64(size) || x > uint64(^uint(0)>>1) {
return 0, errors.New("invalid body length: ", x)
}
bodyL := int(x)
// 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.
@@ -167,7 +171,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
}
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
}
if _, err := br.Discard(bodyL); err != nil {
+53
View File
@@ -1,6 +1,7 @@
package geodata
import (
"bytes"
"fmt"
"os"
"path/filepath"
@@ -12,6 +13,58 @@ import (
"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 Domain_Type
Value string
+2 -2
View File
@@ -44,8 +44,8 @@ func RegisterLua(L *lua.LState) {
"Match": luaIPMatch,
"AnyMatch": luaIPAnyMatch,
"Matches": luaIPMatches,
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[net.IP](L)),
})
"FilterIPs": newLuaIPFilterIPs(xlua.NewReadOnlySlicePusher[net.IP](L)),
}, "SetReverse", "ToggleReverse")
return 1
}))
+6
View File
@@ -78,6 +78,7 @@ local geodata = require("xray.geodata")
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
assert(matcher.Reload == nil and matcher.reload == nil)
assert(matcher:match(ip))
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
assert(not matcher:AnyMatch(nil))
@@ -97,6 +98,11 @@ assert(matcher.missing == nil)
local domain = geodata.BuildDomainMatcher("full:example.com")
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:Match("example.com") == 1)
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
import (
"net"
"reflect"
glua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
)
@@ -9,7 +12,7 @@ import (
// The returned function wraps slices without reflection or metatable lookup,
// 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) {
metatable := luar.New(L, []T{}).(*glua.LUserData).Metatable
metatable := luar.MT(L, []T(nil)).LTable
return func(L *glua.LState, values []T) {
if values == nil {
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.
// It returns the result count and whether it handled the arguments. On false,
// it must leave the stack unchanged for the original luar wrapper.
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.
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
// uses the methods table directly instead of luar's reflected __index handler.
// value must expose methods only. Bindings and their closures are installed once
// per Go type per LState, outside the method-call hot path.
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod) {
// Only directMethods and additionalMethods are exposed, including their aliases.
// Bindings and their closures are installed once per Go type per LState, outside
// 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)
metatable := userdata.Metatable.(*glua.LTable)
methods := metatable.RawGetString("methods").(*glua.LTable)
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 {
original := methods.RawGetString(name)
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)
}
L.Push(userdata)
+183
View File
@@ -2,6 +2,7 @@ package lua
import (
"net"
"reflect"
"testing"
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) {
first := glua.NewState()
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)
}
}
}
*/
+196 -13
View File
@@ -1,47 +1,80 @@
package lua
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
glua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"
)
// Program holds immutable bytecode that can be run by independent LStates.
type Program struct {
proto *glua.FunctionProto
proto *glua.FunctionProto
privileged bool
}
// LStateFactory returns a fully initialized state or nil and an error.
// Implementations must close partial states on failure; callers own successful states.
type LStateFactory func(context.Context) (*glua.LState, error)
// CompileFile reads and compiles a Lua file once.
func CompileFile(path string) (*Program, error) {
f, err := os.Open(path)
// Compile reads a Lua file or decodes an inline script and compiles it into an immutable program.
func Compile(script string) (*Program, error) {
var code []byte
var err error
name := "[inline]"
privileged := false
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)
} else if name, err = platform.ResolveLuaFile(script); err == nil {
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 {
return nil, err
}
defer f.Close()
chunk, err := parse.Parse(bufio.NewReader(f), path)
chunk, err := parse.Parse(bytes.NewReader(code), name)
if err != nil {
return nil, err
}
proto, err := glua.Compile(chunk, path)
proto, err := glua.Compile(chunk, name)
if err != nil {
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
// runs validate. It removes the initialization context before returning a state
// owned by the caller.
func (p *Program) NewState(ctx context.Context, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
L := glua.NewState()
// 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) 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
defer func() {
if !valid {
@@ -53,6 +86,10 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
if register != nil {
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))
// Execute the Lua script's top level.
if err := L.PCall(0, 0, nil); err != nil {
@@ -67,11 +104,157 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
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.
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) {
initCtx, cancel := context.WithTimeout(ctx, initTimeout)
defer cancel()
return p.NewState(initCtx, register, validate)
return p.NewState(initCtx, registrySize, register, validate)
}
}
+1117 -11
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -34,12 +34,13 @@ func ReadAsset(file string) ([]byte, error) {
return ReadFile(path)
}
func OpenAsset(file string) (io.ReadCloser, error) {
path, _, err := getAssetFileLocation(file)
func OpenAsset(file string) (io.ReadCloser, int64, error) {
path, info, err := getAssetFileLocation(file)
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) {
+17 -2
View File
@@ -93,7 +93,7 @@ func GetConfDirPath() string {
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.
func ResolveLuaFile(path string) (string, error) {
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) {
+37
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/xtls/xray-core/common"
@@ -88,6 +89,10 @@ func TestResolveLuaFile(t *testing.T) {
for _, path := range paths[:3] {
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] {
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) {
case cmdarg.Arg:
r, err := confloader.LoadConfig(v[0])
common.Must(err)
if err != nil {
return nil, err
}
data, err := buf.ReadAllToBytes(r)
common.Must(err)
if err != nil {
return nil, err
}
return loadProtobufConfig(data)
case io.Reader:
data, err := buf.ReadAllToBytes(v)
+11 -14
View File
@@ -4,7 +4,6 @@ go 1.27
require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
@@ -20,20 +19,20 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
github.com/yuin/gopher-lua v1.1.2
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/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.59.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11
lukechampine.com/blake3 v1.4.1
@@ -41,13 +40,12 @@ require (
)
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/andybalholm/brotli v1.2.3 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
@@ -57,10 +55,9 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
golang.org/x/tools v0.51.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
)
+20 -20
View File
@@ -1,12 +1,10 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
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/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
@@ -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/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/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/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
@@ -95,8 +95,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
@@ -107,12 +107,12 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/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.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,22 +121,22 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+12 -9
View File
@@ -14,7 +14,6 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
)
type NameServerConfig struct {
@@ -273,6 +272,17 @@ func (m *HostsWrapper) Build() ([]*dns.Config_HostMapping, error) {
// Build implements Buildable
func (c *DNSConfig) Build() (*dns.Config, error) {
if c.Script != "" {
if c.DisableFallback || c.DisableFallbackIfMatch || c.EnableParallelQuery {
return nil, errors.New("DNS script cannot be used with disableFallback, disableFallbackIfMatch or enableParallelQuery")
}
for i, server := range c.Servers {
if len(server.Domains) > 0 || server.SkipFallback || server.FinalQuery {
return nil, errors.New("DNS script cannot be used with domains, skipFallback or finalQuery in server ", i)
}
}
}
config := &dns.Config{
Tag: c.Tag,
DisableCache: c.DisableCache,
@@ -282,14 +292,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
DisableFallbackIfMatch: c.DisableFallbackIfMatch,
EnableParallelQuery: c.EnableParallelQuery,
QueryStrategy: resolveQueryStrategy(c.QueryStrategy),
}
if c.Script != "" {
path, err := platform.ResolveLuaFile(c.Script)
if err != nil {
return nil, errors.New("failed to resolve DNS script: ", c.Script).Base(err)
}
config.Script = path
Script: c.Script,
}
if c.ClientIP != nil {
+42 -37
View File
@@ -2,8 +2,7 @@ package conf_test
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -125,41 +124,7 @@ func TestDNSConfigParsing(t *testing.T) {
}
}
func TestDNSScriptConfig(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.confdir", dir)
path := filepath.Join(dir, "lookup.lua")
if err := os.WriteFile(path, []byte("function HandleDNSQuery(domain, ipv4, ipv6, fake) end"), 0o600); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
script string
wantError bool
}{
{"relative", "lookup.lua", false},
{"absolute", path, false},
{"missing", "missing.lua", true},
{"directory", dir, true},
} {
t.Run(tc.name, func(t *testing.T) {
built, err := (&DNSConfig{Script: tc.script}).Build()
if tc.wantError {
if err == nil {
t.Fatal("Build accepted an invalid script path")
}
return
}
if err != nil {
t.Fatal(err)
}
if built.Script != path {
t.Fatalf("script path = %q, want %q", built.Script, path)
}
})
}
func TestDNSNameServerID(t *testing.T) {
var parsed DNSConfig
if err := json.Unmarshal([]byte(`{"servers":[{"id":"primary","address":"1.1.1.1"}]}`), &parsed); err != nil {
t.Fatal(err)
@@ -172,3 +137,43 @@ func TestDNSScriptConfig(t *testing.T) {
t.Fatalf("nameserver IDs = %v, want primary", built.NameServer)
}
}
func TestDNSScriptConfig(t *testing.T) {
for _, test := range []struct {
name string
input string
wantError string
}{
{name: "script only", input: `{"script":"script.lua"}`},
{name: "compatible fields", input: `{"script":"script.lua","servers":["1.1.1.1",{"id":"primary","address":"8.8.8.8","expectedIPs":["8.8.8.8"],"queryStrategy":"UseIPv4","disableCache":true}],"hosts":{"example.com":"127.0.0.1"},"disableCache":true}`},
{name: "disabled options", input: `{"script":"script.lua","disableFallback":false,"disableFallbackIfMatch":false,"enableParallelQuery":false,"servers":[{"address":"1.1.1.1","domains":[],"skipFallback":false,"finalQuery":false}]}`},
{name: "domains", input: `{"script":"script.lua","servers":[{"address":"1.1.1.1","domains":["domain:example.com"]}]}`, wantError: "domains"},
{name: "skip fallback", input: `{"script":"script.lua","servers":[{"address":"1.1.1.1","skipFallback":true}]}`, wantError: "skipFallback"},
{name: "final query", input: `{"script":"script.lua","servers":[{"address":"1.1.1.1","finalQuery":true}]}`, wantError: "finalQuery"},
{name: "later server", input: `{"script":"script.lua","servers":["1.1.1.1",{"address":"8.8.8.8","finalQuery":true}]}`, wantError: "server 1"},
{name: "disable fallback", input: `{"script":"script.lua","disableFallback":true}`, wantError: "disableFallback"},
{name: "disable fallback if match", input: `{"script":"script.lua","disableFallbackIfMatch":true}`, wantError: "disableFallbackIfMatch"},
{name: "parallel query", input: `{"script":"script.lua","enableParallelQuery":true}`, wantError: "enableParallelQuery"},
{name: "empty script", input: `{"script":"","disableFallback":true,"disableFallbackIfMatch":true,"enableParallelQuery":true,"servers":[{"address":"1.1.1.1","domains":["example.com"],"skipFallback":true,"finalQuery":true}]}`},
} {
t.Run(test.name, func(t *testing.T) {
var config DNSConfig
if err := json.Unmarshal([]byte(test.input), &config); err != nil {
t.Fatal(err)
}
built, err := config.Build()
if test.wantError != "" {
if built != nil || err == nil || !strings.Contains(err.Error(), "DNS script cannot be used with ") || !strings.Contains(err.Error(), test.wantError) {
t.Fatalf("Build() = %v, %v; want nil config and script conflict error mentioning %s", built, err, test.wantError)
}
return
}
if err != nil {
t.Fatal(err)
}
if built.Script != config.Script || len(built.NameServer) != len(config.Servers) {
t.Fatalf("Build() did not preserve script or servers: %v", built)
}
})
}
}
+68 -11
View File
@@ -16,8 +16,61 @@ import (
. "github.com/xtls/xray-core/infra/conf"
masqueproxy "github.com/xtls/xray-core/proxy/masque"
"github.com/xtls/xray-core/transport/internet/masque"
"github.com/xtls/xray-core/transport/internet/splithttp"
)
func defaultMasqueXmux() *splithttp.XmuxConfig {
return &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{},
MaxConnections: &splithttp.RangeConfig{From: 1, To: 1},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{From: 1800, To: 3000},
}
}
func TestMasqueXmuxConfig(t *testing.T) {
creator := func() Buildable {
return new(MasqueConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{"xmux": {"maxConnections": "2-4", "hMaxReusableSecs": 60, "hKeepAlivePeriod": -1}}`,
Parser: loadJSON(creator),
Output: &masque.Config{
Path: "/.well-known/masque/ip/*/*/",
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{},
MaxConnections: &splithttp.RangeConfig{From: 2, To: 4},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{From: 60, To: 60},
HKeepAlivePeriod: -1,
},
},
},
{
Input: `{"xmux": {"maxConcurrency": 8}}`,
Parser: loadJSON(creator),
Output: &masque.Config{
Path: "/.well-known/masque/ip/*/*/",
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{From: 8, To: 8},
MaxConnections: &splithttp.RangeConfig{},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{},
},
},
},
})
if _, err := loadJSON(creator)(`{"xmux": {"maxConnections": 2, "maxConcurrency": 8}}`); err == nil {
t.Error("expected an error for maxConnections with maxConcurrency")
}
}
func TestMasqueConfig(t *testing.T) {
creator := func() Buildable {
return new(MasqueConfig)
@@ -27,7 +80,7 @@ func TestMasqueConfig(t *testing.T) {
{
Input: `{}`,
Parser: loadJSON(creator),
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/"},
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/", Xmux: defaultMasqueXmux()},
},
{
Input: `{
@@ -40,12 +93,13 @@ func TestMasqueConfig(t *testing.T) {
Host: "example.com:8443",
Path: "/.well-known/masque/ip/*/*/",
Headers: map[string]string{"Authorization": "Basic dTpw"},
Xmux: defaultMasqueXmux(),
},
},
{
Input: `{"path": "/masque/ip{?target,ipproto}"}`,
Parser: loadJSON(creator),
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*"},
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*", Xmux: defaultMasqueXmux()},
},
{
Input: `{"user": "u", "pass": "p:q", "headers": {"X-Token": "a"}}`,
@@ -53,6 +107,7 @@ func TestMasqueConfig(t *testing.T) {
Output: &masque.Config{
Path: "/.well-known/masque/ip/*/*/",
Headers: map[string]string{"Authorization": "Basic dTpwOnE=", "X-Token": "a"},
Xmux: defaultMasqueXmux(),
},
},
})
@@ -122,6 +177,7 @@ func TestMasqueWarpConfig(t *testing.T) {
Host: "cloudflareaccess.com",
Path: "/",
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: address},
Xmux: defaultMasqueXmux(),
},
},
})
@@ -134,6 +190,7 @@ func TestMasqueWarpConfig(t *testing.T) {
Host: "example.com",
Path: "/warp",
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: address},
Xmux: defaultMasqueXmux(),
},
},
})
@@ -164,6 +221,7 @@ func TestMasqueWarpConfig(t *testing.T) {
Host: "cloudflareaccess.com",
Path: "/",
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: []string{"172.16.0.2/32"}},
Xmux: defaultMasqueXmux(),
},
},
})
@@ -199,18 +257,17 @@ func TestMasqueOutboundConfig(t *testing.T) {
return err
}
if err := build(`{
"protocol": "masque",
"settings": {"address": "example.com", "port": 443},
"streamSettings": {"network": "masque", "security": "tls"},
"mux": {"enabled": false, "concurrency": -1}
}`); err != nil {
t.Error(err)
for _, input := range []string{
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": false, "concurrency": -1}}`,
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1, "xudpConcurrency": 8}}`,
} {
if err := build(input); err != nil {
t.Errorf("%s: %v", input, err)
}
}
for _, input := range []string{
`{"protocol": "masque", "settings": {"address": "example.com"}, "streamSettings": {"network": "masque", "security": "tls"}}`,
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1}}`,
`{"protocol": "freedom", "streamSettings": {"network": "masque", "security": "tls"}}`,
} {
if err := build(input); err == nil {
+3 -7
View File
@@ -7,7 +7,6 @@ import (
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/serial"
"google.golang.org/protobuf/proto"
@@ -95,13 +94,10 @@ func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
func (c *RouterConfig) Build() (*router.Config, error) {
config := new(router.Config)
if c.Script != "" {
path, err := platform.ResolveLuaFile(c.Script)
if err != nil {
return nil, errors.New("failed to resolve routing script").Base(err)
}
config.Script = path
if c.Script != "" && (c.DomainStrategy != nil || len(c.RuleList) > 0) {
return nil, errors.New("routing script cannot be used with domainStrategy or rules")
}
config.Script = c.Script
config.DomainStrategy = c.getDomainStrategy()
+22 -22
View File
@@ -2,8 +2,7 @@ package conf_test
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
_ "unsafe"
@@ -240,36 +239,37 @@ func TestRouterConfig(t *testing.T) {
}
func TestRouterScriptConfig(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.confdir", dir)
path := filepath.Join(dir, "route.lua")
if err := os.WriteFile(path, []byte("function HandleRoute() end"), 0o600); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
for _, test := range []struct {
name string
script string
input string
wantError bool
}{
{"relative", "route.lua", false},
{"absolute", path, false},
{"missing", "missing.lua", true},
{"directory", dir, true},
{name: "script only", input: `{"script":"missing.lua"}`},
{name: "empty rules", input: `{"script":"missing.lua","rules":[]}`},
{name: "balancers", input: `{"script":"missing.lua","balancers":[{"tag":"b1","selector":["direct"]}]}`},
{name: "default domain strategy", input: `{"script":"missing.lua","domainStrategy":"AsIs"}`, wantError: true},
{name: "IP domain strategy", input: `{"script":"missing.lua","domainStrategy":"IPIfNonMatch"}`, wantError: true},
{name: "empty domain strategy", input: `{"script":"missing.lua","domainStrategy":""}`, wantError: true},
{name: "rules", input: `{"script":"missing.lua","rules":[{"outboundTag":"direct","domain":["example.com"]}]}`, wantError: true},
{name: "empty script", input: `{"script":"","domainStrategy":"IPOnDemand","rules":[{"outboundTag":"direct","domain":["example.com"]}]}`},
} {
t.Run(tc.name, func(t *testing.T) {
built, err := (&RouterConfig{Script: tc.script}).Build()
if tc.wantError {
if err == nil {
t.Fatal("Build accepted invalid script path")
t.Run(test.name, func(t *testing.T) {
var config RouterConfig
if err := json.Unmarshal([]byte(test.input), &config); err != nil {
t.Fatal(err)
}
built, err := config.Build()
if test.wantError {
if built != nil || err == nil || !strings.Contains(err.Error(), "routing script cannot be used with domainStrategy or rules") {
t.Fatalf("Build() = %v, %v; want nil config and script conflict error", built, err)
}
return
}
if err != nil {
t.Fatal(err)
}
if built.Script != path {
t.Fatalf("script path = %q, want %q", built.Script, path)
if built.Script != config.Script || len(built.Rule) != len(config.RuleList) || len(built.BalancingRule) != len(config.Balancers) {
t.Fatalf("Build() did not preserve script, rules or balancers: %v", built)
}
})
}
+140 -10
View File
@@ -9,12 +9,15 @@ import (
"fmt"
"net/netip"
"net/url"
"os"
"regexp"
"strings"
googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors"
xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -28,6 +31,7 @@ import (
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
"github.com/xtls/xray-core/transport/internet/tls"
"google.golang.org/protobuf/proto"
)
@@ -304,11 +308,26 @@ type NoiseItem struct {
}
type NoiseMask struct {
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
Reset Int32Range `json:"reset"`
Noise []NoiseItem `json:"noise"`
Script string `json:"script"`
}
func (c *NoiseMask) Build() (proto.Message, error) {
if c.Script != "" {
if len(c.Noise) > 0 {
return nil, errors.New(`"script" and "noise" can't be used together`)
}
if _, err := xlua.Compile(c.Script); err != nil {
return nil, errors.New("failed to compile the noise script").Base(err)
}
return &noise.Config{
ResetMin: int64(c.Reset.From),
ResetMax: int64(c.Reset.To),
Script: c.Script,
}, nil
}
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
for _, item := range c.Noise {
if len(item.Packet) > 0 && item.Rand.To > 0 {
@@ -799,7 +818,10 @@ type XDNSDomain 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 {
@@ -834,6 +856,26 @@ func (c *XDNS) Build() (proto.Message, error) {
}
}
for i := range c.Resolvers {
worker := c.Resolvers[i].Worker
if worker == 0 {
worker = 32
}
var tlsConfig *tls.Config
var quicConfig *internet.QuicParams
if c.Resolvers[i].TlsConfig != nil {
proto, err := c.Resolvers[i].TlsConfig.Build()
if err != nil {
return nil, err
}
tlsConfig = proto.(*tls.Config)
}
if c.Resolvers[i].QuicConfig != nil {
proto, err := c.Resolvers[i].QuicConfig.Build()
if err != nil {
return nil, err
}
quicConfig = proto.(*internet.QuicParams)
}
for j := range c.Resolvers[i].Addrs {
var u *url.URL
var e error
@@ -845,18 +887,21 @@ func (c *XDNS) Build() (proto.Message, error) {
if e != nil {
return nil, e
}
var addr string
switch u.Scheme {
case "tcp", "udp":
host := u.Hostname()
port := u.Port()
if port == "" {
port = "53"
}
addr = net.JoinHostPort(host, port)
case "https":
addr = u.String()
default:
return nil, errors.New("invalid protocol")
}
var host, port string
host = u.Hostname()
port = u.Port()
if port == "" {
port = "53"
}
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: net.JoinHostPort(host, port)})
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: addr, Worker: worker, TlsConfig: tlsConfig, QuicConfig: quicConfig})
}
}
if c.ExtraPoll < 0 || c.ExtraPoll > 3 {
@@ -1149,6 +1194,91 @@ type QuicParamsConfig struct {
DisableStatelessReset bool `json:"disableStatelessReset"`
}
func (config *QuicParamsConfig) Build() (proto.Message, error) {
profile := strings.ToLower(config.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := config.BrutalUp.Bps()
if err != nil {
return nil, err
}
down, err := config.BrutalDown.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
config.Congestion = strings.ToLower(config.Congestion)
switch config.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", config.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if config.InitStreamReceiveWindow > 0 && config.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if config.MaxStreamReceiveWindow > 0 && config.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if config.InitConnectionReceiveWindow > 0 && config.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if config.MaxConnectionReceiveWindow > 0 && config.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if config.MaxIdleTimeout != 0 && (config.MaxIdleTimeout < 4 || config.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if config.KeepAlivePeriod != 0 && (config.KeepAlivePeriod < 2 || config.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if config.MaxIncomingStreams != 0 && config.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if config.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
return &internet.QuicParams{
Congestion: config.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: config.BrutalDisableLossCompensation,
InitStreamReceiveWindow: config.InitStreamReceiveWindow,
MaxStreamReceiveWindow: config.MaxStreamReceiveWindow,
InitConnReceiveWindow: config.InitConnectionReceiveWindow,
MaxConnReceiveWindow: config.MaxConnectionReceiveWindow,
MaxIdleTimeout: config.MaxIdleTimeout,
KeepAlivePeriod: config.KeepAlivePeriod,
DisablePathMtuDiscovery: config.DisablePathMTUDiscovery,
DisableChromeParrot: config.DisableChromeParrot,
DisableGSO: config.DisableGSO,
MaxIncomingStreams: config.MaxIncomingStreams,
DisableStatelessReset: config.DisableStatelessReset,
}, nil
}
type FinalMask struct {
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
@@ -2,6 +2,9 @@ package conf
import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"testing"
"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])
}
}
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")
}
})
}
}
+6 -85
View File
@@ -1,13 +1,11 @@
package conf
import (
"os"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
)
type TransportProtocol string
@@ -228,102 +226,25 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
if c.FinalMask != nil {
for _, mask := range c.FinalMask.Tcp {
u, err := mask.Build(true)
proto, err := mask.Build(true)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u))
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(proto))
}
for _, mask := range c.FinalMask.Udp {
u, err := mask.Build(false)
proto, err := mask.Build(false)
if err != nil {
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
}
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u))
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(proto))
}
if c.FinalMask.QuicParams != nil {
profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile)
switch profile {
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
if profile == "" {
profile = string(bbr.ProfileStandard)
}
default:
return nil, errors.New("unknown bbr profile")
}
up, err := c.FinalMask.QuicParams.BrutalUp.Bps()
proto, err := c.FinalMask.QuicParams.Build()
if err != nil {
return nil, err
}
down, err := c.FinalMask.QuicParams.BrutalDown.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
}
if down > 0 && down < 65536 {
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
}
c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion)
switch c.FinalMask.QuicParams.Congestion {
case "", "brutal", "reno", "bbr":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
}
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 {
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 {
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 {
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
}
if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) {
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
}
if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if c.FinalMask.QuicParams.Debug {
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile,
BrutalUp: up,
BrutalDown: down,
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
}
config.QuicParams = proto.(*internet.QuicParams)
}
}
+18
View File
@@ -808,6 +808,7 @@ type MasqueConfig struct {
Pass string `json:"pass"`
Headers map[string]string `json:"headers"`
Warp *MasqueWarpConfig `json:"warp"`
Xmux XmuxConfig `json:"xmux"`
}
func (c *MasqueConfig) Build() (proto.Message, error) {
@@ -878,11 +879,28 @@ func (c *MasqueConfig) Build() (proto.Message, error) {
}
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.User+":"+c.Pass))
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 1
c.Xmux.MaxConnections.To = 1
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
}
return &masque.Config{
Host: host,
Path: path,
Headers: headers,
Warp: warp,
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
},
}, nil
}
+1 -1
View File
@@ -59,7 +59,7 @@ func (v *TunConfig) Build() (proto.Message, error) {
if slices.Contains(config.AutoSystemWfpBlockLeak, "dns") && len(v.DNS) == 0 {
return nil, errors.New(`autoSystemWfpBlockLeak "dns" needs dns to be set`)
}
case "linux":
case "linux", "freebsd":
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
return nil, errors.New("autoSystemDnsToGateway needs gateway to be set")
}
+7 -6
View File
@@ -3,6 +3,7 @@ package conf_test
import (
"encoding/json"
"runtime"
"slices"
"testing"
. "github.com/xtls/xray-core/infra/conf"
@@ -43,18 +44,18 @@ func TestTunConfigAutoSystem(t *testing.T) {
func TestTunConfigAutoSystemNeeds(t *testing.T) {
for _, c := range []struct {
input string
goos string // where it is rejected
goos []string // where it is rejected
}{
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, "windows"},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, ""},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, "windows"},
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, "linux"},
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, nil},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
} {
config := new(TunConfig)
if err := json.Unmarshal([]byte(c.input), config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); (err != nil) != (runtime.GOOS == c.goos) {
if _, err := config.Build(); (err != nil) != slices.Contains(c.goos, runtime.GOOS) {
t.Errorf("%s on %s: error = %v", c.input, runtime.GOOS, err)
}
}
+1 -5
View File
@@ -344,11 +344,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
return nil, err
}
if _, ok := ts.(*masque.ClientConfig); ok {
if ms := senderSettings.MultiplexSettings; ms != nil && ms.Enabled {
return nil, errors.New(`masque outbound does not support "mux"`)
}
} else if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
if _, ok := ts.(*masque.ClientConfig); !ok && senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
return nil, errors.New("the masque transport can only be used by the masque outbound")
}
+14
View File
@@ -0,0 +1,14 @@
package base64
import "github.com/xtls/xray-core/main/commands/base"
// CmdBase64 holds all Base64 subcommands.
var CmdBase64 = &base.Command{
UsageLine: "{{.Exec}} base64",
Short: "Encode and decode Base64",
Long: "{{.Exec}} {{.LongName}} provides tools to encode and decode arbitrary bytes.",
Commands: []*base.Command{
cmdEncode,
cmdDecode,
},
}
+66
View File
@@ -0,0 +1,66 @@
package base64
import (
"fmt"
"io"
"os"
"strings"
xbase64 "github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdDecode = &base.Command{
UsageLine: "{{.Exec}} base64 decode [-gzip] <value | - | stdin:> [output-file]",
Short: "Decode Base64",
Long: fmt.Sprintf(`
Decode a Base64 value, or pass "-" or "stdin:" to read stdin.
Write to output-file, or stdout if omitted.
Accepts %s, %s, or no prefix; gzip is enabled by %s or -gzip.
Examples:
{{.Exec}} base64 decode eA== out.bin
{{.Exec}} base64 decode -gzip H4sIAAAAAAAC/6sAAIMW3IwBAAAA out.bin
`, xbase64.Prefix, xbase64.GzipPrefix, xbase64.GzipPrefix),
}
var decodeGzipEnabled = cmdDecode.Flag.Bool("gzip", false, "Decompress the decoded bytes with gzip.")
func init() {
cmdDecode.Run = executeDecode
}
func executeDecode(cmd *base.Command, args []string) {
if len(args) == 0 || len(args) > 2 {
cmd.Usage()
}
value := args[0]
if value == "-" || value == "stdin:" {
input, err := io.ReadAll(os.Stdin)
if err != nil {
base.Fatalf("failed to read input: %s", err)
}
value = string(input)
}
value = strings.TrimSpace(value)
if *decodeGzipEnabled && !strings.HasPrefix(value, xbase64.GzipPrefix) {
value = xbase64.GzipPrefix + strings.TrimPrefix(value, xbase64.Prefix)
}
data, err := xbase64.Decode(value)
if err != nil {
base.Fatalf("failed to decode input: %s", err)
}
if len(args) == 2 && args[1] != "-" {
err = os.WriteFile(args[1], data, 0o666)
} else {
_, err = os.Stdout.Write(data)
}
if err != nil {
base.Fatalf("failed to write decoded output: %s", err)
}
}
+57
View File
@@ -0,0 +1,57 @@
package base64
import (
"fmt"
"io"
"os"
xbase64 "github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdEncode = &base.Command{
UsageLine: "{{.Exec}} base64 encode [-gzip] <input-file | - | stdin:>",
Short: "Encode as Base64",
Long: fmt.Sprintf(`
Encode input-file as Base64 with the %s prefix.
Pass "-" or "stdin:" to read stdin.
Use -gzip to compress and output %s.
Examples:
{{.Exec}} base64 encode input.bin
{{.Exec}} base64 encode -gzip input.bin
`, xbase64.Prefix, xbase64.GzipPrefix),
}
var gzipEnabled = cmdEncode.Flag.Bool("gzip", false, "Compress the input with gzip.")
func init() {
cmdEncode.Run = executeEncode
}
func executeEncode(cmd *base.Command, args []string) {
if len(args) != 1 {
cmd.Usage()
}
var data []byte
var err error
if args[0] == "-" || args[0] == "stdin:" {
data, err = io.ReadAll(os.Stdin)
} else {
data, err = os.ReadFile(args[0])
}
if err != nil {
base.Fatalf("failed to read input: %s", err)
}
value, err := xbase64.Encode(data, *gzipEnabled)
if err != nil {
base.Fatalf("failed to encode input: %s", err)
}
if _, err := fmt.Fprintln(os.Stdout, value); err != nil {
base.Fatalf("failed to write encoded output: %s", err)
}
}
+6
View File
@@ -2,7 +2,10 @@ package all
import (
"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/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/base"
)
@@ -11,10 +14,13 @@ func init() {
base.RootCommand.Commands = append(
base.RootCommand.Commands,
api.CmdAPI,
base64.CmdBase64,
convert.CmdConvert,
// lua.CmdLua,
tls.CmdTLS,
cmdUUID,
cmdX25519,
// cmdEd25519,
cmdWG,
cmdMLDSA65,
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
import (
"crypto/mldsa"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/xray-core/main/commands/base"
)
@@ -39,8 +39,8 @@ func executeMLDSA65(cmd *base.Command, args []string) {
} else {
rand.Read(seed[:])
}
pub, _ := mldsa65.NewKeyFromSeed(&seed)
key, _ := mldsa.NewPrivateKey(mldsa.MLDSA65(), seed[:])
fmt.Printf("Seed: %v\nVerify: %v\n",
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 {
// Executable name of current binary
Exec string
// Full name of the command being executed
CommandName string
// commands column width of current command
CommandsWidth int
}
+1
View File
@@ -61,6 +61,7 @@ BigCmdLoop:
}
buildCommandText(cmd)
CommandEnv.CommandName = cmdName
cmd.Run(cmd, args)
Exit()
return
+35 -1
View File
@@ -8,12 +8,14 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/main/commands/base"
"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)
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 {
@@ -41,6 +48,33 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
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
// 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.
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,
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() {
cmdRun.Run = executeRun // break init loop
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.")
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
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
* main func in this file is run.
@@ -214,6 +215,11 @@ func getConfigFormat() string {
}
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)
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)
}
})
}
}
+104 -35
View File
@@ -7,7 +7,6 @@ import (
"net/netip"
"slices"
"sync"
"sync/atomic"
"time"
"golang.zx2c4.com/wireguard/tun"
@@ -26,6 +25,7 @@ import (
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/masque"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
)
@@ -43,11 +43,15 @@ type Client struct {
ctx context.Context
cancel context.CancelFunc
tunnel atomic.Pointer[tunnel]
mu sync.Mutex
xmux *splithttp.XmuxManager
dialCtx context.Context
dialer internet.Dialer
lastErr error
lastErrAt time.Time
tunnelsMu sync.Mutex
tunnels map[*tunnel]struct{}
}
func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
@@ -55,7 +59,8 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
if _, ok := streamSettings.ProtocolSettings.(*masque.Config); !ok {
transportConfig, ok := streamSettings.ProtocolSettings.(*masque.Config)
if !ok {
return nil, errors.New("not masque transport")
}
if tls.ConfigFromStreamSettings(streamSettings) == nil {
@@ -86,7 +91,13 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
server: server,
policyManager: p,
remoteDNS: remoteDNS,
tunnels: make(map[*tunnel]struct{}),
}
xmuxConfig := transportConfig.Xmux
if xmuxConfig == nil {
xmuxConfig = &splithttp.XmuxConfig{}
}
c.xmux = splithttp.NewXmuxManager(*xmuxConfig, c.newXmuxConn)
c.ctx, c.cancel = context.WithCancel(context.Background())
return c, nil
}
@@ -100,10 +111,11 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
ob.Name = "masque"
ob.CanSpliceCopy = 3
t, err := c.getTunnel(ctx, dialer)
xmuxClient, t, err := c.getTunnel(ctx, dialer)
if err != nil {
return errors.New("failed to establish CONNECT-IP tunnel").Base(err)
}
defer xmuxClient.DoneRunning()
var newCtx context.Context
var newCancel context.CancelFunc
@@ -180,40 +192,63 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
return nil
}
func (c *Client) getTunnel(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
func (c *Client) getTunnel(ctx context.Context, dialer internet.Dialer) (*splithttp.XmuxClient, *tunnel, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.ctx.Err() != nil {
return nil, errors.New("closed")
}
if t := c.tunnel.Load(); t != nil {
select {
case <-t.done:
default:
return t, nil
}
return nil, nil, errors.New("closed")
}
if err := ctx.Err(); err != nil {
return nil, err
return nil, nil, err
}
var xmuxClient *splithttp.XmuxClient
if c.lastErr != nil && time.Since(c.lastErrAt) < retryInterval {
return nil, c.lastErr
if xmuxClient = c.xmux.GetExistingXmuxClient(ctx); xmuxClient == nil {
return nil, nil, c.lastErr
}
} else {
c.dialCtx, c.dialer = ctx, dialer
xmuxClient = c.xmux.GetXmuxClient(ctx)
c.dialCtx, c.dialer = nil, nil
if err := xmuxClient.XmuxConn.(*tunnel).err; err != nil {
if xmuxClient = c.xmux.GetExistingXmuxClient(ctx); xmuxClient == nil {
return nil, nil, err
}
}
}
t := xmuxClient.XmuxConn.(*tunnel)
if c.ctx.Err() != nil {
return nil, nil, errors.New("closed")
}
xmuxClient.AddRunning()
return xmuxClient, t, nil
}
t, err := c.establish(ctx, dialer)
func (c *Client) newXmuxConn() splithttp.XmuxConn {
t, err := c.establish(c.dialCtx, c.dialer)
if err != nil {
c.lastErr, c.lastErrAt = err, time.Now()
return nil, err
return failedTunnel(err)
}
c.lastErr = nil
c.tunnel.Store(t)
if c.ctx.Err() != nil {
if c.tunnel.CompareAndSwap(t, nil) {
t.close()
}
return nil, errors.New("closed")
c.tunnelsMu.Lock()
if c.tunnels == nil {
c.tunnelsMu.Unlock()
t.close()
return failedTunnel(errors.New("closed"))
}
return t, nil
c.tunnels[t] = struct{}{}
c.tunnelsMu.Unlock()
if t.IsClosed() {
c.removeTunnel(t)
}
return t
}
func (c *Client) removeTunnel(t *tunnel) {
c.tunnelsMu.Lock()
delete(c.tunnels, t)
c.tunnelsMu.Unlock()
}
func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
@@ -229,7 +264,7 @@ func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel
conn.Close()
return nil, errors.New("not a CONNECT-IP connection")
}
t, err := newTunnel(conn, mconn.LocalAddrs(), c.remoteDNS)
t, err := newTunnel(conn, mconn.LocalAddrs(), c.remoteDNS, c.removeTunnel)
if err != nil {
conn.Close()
return nil, err
@@ -240,7 +275,11 @@ func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel
func (c *Client) Close() error {
c.cancel()
if t := c.tunnel.Swap(nil); t != nil {
c.tunnelsMu.Lock()
tunnels := c.tunnels
c.tunnels = nil
c.tunnelsMu.Unlock()
for t := range tunnels {
t.close()
}
return nil
@@ -252,9 +291,31 @@ type tunnel struct {
tnet *wireguard.Net
done chan struct{}
closeOnce sync.Once
onClose func(*tunnel)
err error
}
func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr) (*tunnel, error) {
func failedTunnel(err error) *tunnel {
t := &tunnel{err: err, done: make(chan struct{})}
t.close()
return t
}
func (t *tunnel) IsClosed() bool {
select {
case <-t.done:
return true
default:
return false
}
}
func (t *tunnel) Close() error {
t.close()
return nil
}
func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr, onClose func(*tunnel)) (*tunnel, error) {
var dns []netip.Addr
for _, addr := range remoteDNS {
if slices.ContainsFunc(local, func(l netip.Addr) bool { return l.Is4() == addr.Is4() }) {
@@ -266,15 +327,16 @@ func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr)
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 {
return nil, err
}
t := &tunnel{
conn: conn,
dev: dev,
tnet: tnet,
done: make(chan struct{}),
conn: conn,
dev: dev,
tnet: tnet,
done: make(chan struct{}),
onClose: onClose,
}
go t.readFromTunnel()
go t.writeToTunnel()
@@ -316,8 +378,15 @@ func (t *tunnel) writeToTunnel() {
func (t *tunnel) close() {
t.closeOnce.Do(func() {
close(t.done)
t.conn.Close()
t.dev.Close()
if t.conn != nil {
t.conn.Close()
}
if t.dev != nil {
t.dev.Close()
}
if t.onClose != nil {
t.onClose(t)
}
})
}
+54
View File
@@ -0,0 +1,54 @@
package masque
import (
"context"
"errors"
"testing"
"time"
"github.com/xtls/xray-core/transport/internet/splithttp"
)
func newTestClient(newConn func(c *Client) splithttp.XmuxConn) *Client {
c := &Client{ctx: context.Background()}
c.xmux = splithttp.NewXmuxManager(splithttp.XmuxConfig{
MaxConnections: &splithttp.RangeConfig{From: 3, To: 3},
}, func() splithttp.XmuxConn {
return newConn(c)
})
return c
}
func TestGetTunnelFallsBack(t *testing.T) {
refused := errors.New("refused")
healthy := &tunnel{done: make(chan struct{})}
dials := 0
c := newTestClient(func(c *Client) splithttp.XmuxConn {
if dials++; dials == 1 {
return healthy
}
c.lastErr, c.lastErrAt = refused, time.Now()
return failedTunnel(refused)
})
for i := 0; i < 5; i++ {
if _, got, err := c.getTunnel(context.Background(), nil); err != nil || got != healthy {
t.Fatalf("flow %d: got %p, %v instead of the healthy tunnel", i, got, err)
}
}
if dials != 2 {
t.Errorf("dialed %d times, a failed tunnel should not be retried within %v", dials, retryInterval)
}
}
func TestGetTunnelFailsWithoutTunnels(t *testing.T) {
refused := errors.New("refused")
c := newTestClient(func(c *Client) splithttp.XmuxConn {
c.lastErr, c.lastErrAt = refused, time.Now()
return failedTunnel(refused)
})
for i := 0; i < 2; i++ {
if _, _, err := c.getTunnel(context.Background(), nil); err != refused {
t.Fatalf("flow %d: got %v instead of %v", i, err, refused)
}
}
}
+13 -9
View File
@@ -22,6 +22,7 @@ import (
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
xtun "github.com/xtls/xray-core/proxy/tun"
"github.com/xtls/xray-core/proxy/wireguard"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
@@ -166,17 +167,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if mtu == 0 {
mtu = masque.MinPacketSize
}
dev, _, gstack, err := wireguard.CreateNetTUN(local, nil, mtu, false)
if err != nil {
return nil, err
}
s := &Server{
validator: users,
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
ctx: core.ToBackgroundDetachedContext(ctx),
mtu: mtu,
dev: dev,
pools: pools,
local: local,
tunnels: make(map[netip.Addr]*serverTunnel),
@@ -187,7 +182,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
if content := session.ContentFromContext(ctx); content != nil {
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
}
@@ -500,7 +499,8 @@ func packetDestination(packet []byte) (netip.Addr, bool) {
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()
source := net.DestinationFromAddr(conn.RemoteAddr())
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)
writer := buf.NewWriter(conn)
if dest.Network == net.Network_TCP {
writer = &xtun.TCPWriter{Writer: writer}
}
link := &transport.Link{
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
Writer: buf.NewWriter(conn),
Writer: writer,
}
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
errors.LogError(ctx, errors.New("connection closed").Base(err))
+57
View File
@@ -7,13 +7,17 @@ import (
"net/netip"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
"golang.zx2c4.com/wireguard/tun"
)
@@ -326,3 +330,56 @@ func TestPacketDestination(t *testing.T) {
require.False(t, ok)
}
}
// stackConn is a TCP connection as the stack hands it over: it is its own buf.Writer.
type stackConn struct {
net.Conn
closed atomic.Int32
}
func (c *stackConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c *stackConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
buf.ReleaseMulti(mb)
return nil
}
func (c *stackConn) Close() error {
c.closed.Add(1)
return nil
}
func (c *stackConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IP{10, 13, 0, 2}, Port: 1234}
}
// closingDispatcher does what an outbound does when the other side has no more to send.
type closingDispatcher struct {
routing.Dispatcher
conn *stackConn
closed int32
}
func (d *closingDispatcher) DispatchLink(_ context.Context, _ net.Destination, link *transport.Link) error {
common.Close(link.Writer)
d.closed = d.conn.closed.Load()
return nil
}
func TestServerKeepsConnectionOpenWhenWriterCloses(t *testing.T) {
conn := &stackConn{}
dispatcher := &closingDispatcher{conn: conn}
s := &Server{
ctx: context.Background(),
dispatcher: dispatcher,
tunnels: map[netip.Addr]*serverTunnel{netip.MustParseAddr("10.13.0.2"): newServerTunnel(nil, &protocol.MemoryUser{})},
}
s.HandleConnection(conn, net.TCPDestination(net.ParseAddress("192.0.2.1"), 443))
if dispatcher.closed != 0 {
t.Fatal("the connection was closed with its writer, while the client may still be sending")
}
if got := conn.closed.Load(); got != 1 {
t.Fatalf("connection closed %d times, want 1", got)
}
}
+9 -6
View File
@@ -180,14 +180,14 @@ type VisionReader struct {
isUplink bool
conn net.Conn
input *bytes.Reader
rawInput *bytes.Buffer
rawInput **bytes.Buffer
ob *session.Outbound
// internal
directReadCounter stats.Counter
}
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, ob *session.Outbound) *VisionReader {
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput **bytes.Buffer, ob *session.Outbound) *VisionReader {
return &VisionReader{
Reader: reader,
trafficState: trafficState,
@@ -261,12 +261,15 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, inputBuffer)
}
if rawInputBuffer, err := buf.ReadFrom(w.rawInput); err == nil && !rawInputBuffer.IsEmpty() {
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
}
*w.input = bytes.Reader{} // release memory
w.input = nil
*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
if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil {
+11 -5
View File
@@ -169,11 +169,17 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
}
sessionItem.Unlock()
}
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
})
if err != nil {
continue
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
})
if err != nil {
continue
}
}
payloadBuf := buf.New()
+11 -5
View File
@@ -336,11 +336,17 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
}
sessionItem.Unlock()
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
})
if err != nil {
continue
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
})
if err != nil {
continue
}
}
pBuf := buf.New()
+10 -4
View File
@@ -253,10 +253,16 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
}
sessionItem.Unlock()
}
link, err := sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil {
b.Release()
continue
link := sessionItem.link.Load()
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil {
b.Release()
continue
}
}
_ = link.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))
}
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
return errors.New("connection ends").Base(err)
}
@@ -212,8 +212,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return nil
}
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
return errors.New("connection ends").Base(err)
}
+1 -1
View File
@@ -153,7 +153,7 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
return DecodedUDPPacket{}, ErrBadHeaderType
}
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
if diff > 30 {
return DecodedUDPPacket{}, ErrBadTimestamp
}
+2 -2
View File
@@ -49,12 +49,12 @@ func (f *SlidingWindow) Add(counter uint64) {
if counter > f.last {
lastBlockIndex := f.last >> swBlockBitLog
diff := int(blockIndex - lastBlockIndex)
diff := blockIndex - lastBlockIndex
if diff > swRingBlocks {
diff = swRingBlocks
}
for i := 0; i < diff; i++ {
for i := 0; i < int(diff); i++ {
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
f.ring[lastBlockIndex] = 0
}
+8 -21
View File
@@ -18,12 +18,8 @@ import (
)
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
if s.currentConn.Load() == nil {
s.currentConn.Store(conn)
}
if s.timer != nil {
s.timer.Update()
}
s.currentConn.Store(conn)
s.timer.Update()
}
func (s *ServerUDPSession) WriteToClient(b []byte) error {
@@ -57,19 +53,16 @@ func (s *ServerUDPSession) EnsureLink(
policyManager policy.Manager,
responseEncoder func(dest net.Destination, payload []byte) ([]byte, error),
) (*transport.Link, error) {
s.UpdateConn(conn)
if link := s.link.Load(); link != nil {
return link, nil
}
s.Lock()
defer s.Unlock()
if link := s.link.Load(); link != nil {
s.UpdateConn(conn)
return link, nil
}
s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx)
inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil {
@@ -94,7 +87,6 @@ func (s *ServerUDPSession) EnsureLink(
return nil, err
}
s.link.Store(link)
sessionPolicy := policyManager.ForLevel(level)
s.timer = signal.CancelAfterInactivity(sessCtx, func() {
if s.manager != nil {
@@ -103,6 +95,7 @@ func (s *ServerUDPSession) EnsureLink(
s.Close()
cancel()
}, sessionPolicy.Timeouts.ConnectionIdle)
s.link.Store(link)
go handleUDPResponse(s, link, dest, responseEncoder)
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)) {
defer func() {
if s.timer != nil {
s.timer.SetTimeout(0)
}
}()
defer s.timer.SetTimeout(0)
for {
resMb, err := link.Reader.ReadMultiBuffer()
if err != nil {
return
}
if s.timer != nil {
s.timer.Update()
}
s.timer.Update()
for i, rb := range resMb {
b := rb.Bytes()
if encode != nil {
+12 -4
View File
@@ -11,6 +11,7 @@ import (
"sync"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/antireplay"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -253,7 +254,7 @@ func ReadClientRequestHeaderWithFixed(reader *StreamReader, fixedChunk []byte) (
}
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
if diff > 30 {
return nil, ErrBadTimestamp
}
@@ -430,7 +431,7 @@ func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt [
}
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
diff := math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))
if diff > 30 {
return nil, ErrBadTimestamp
}
@@ -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 {
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
defer timer.SetTimeout(0)
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
requestDone := func() error {
@@ -644,6 +646,12 @@ func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
}
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil {
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
return errors.New("connection ends").Base(err)
}
return nil
}
+31 -9
View File
@@ -15,17 +15,17 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
## DETAILS
By default, enabling the feature will only bring the tun interface up. \
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS and FreeBSD use the first IPv4 prefix from `gateway` for the point-to-point address. \
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` for the point-to-point address and FreeBSD for the interface address, the one after the gateway. \
Without `gateway`, the systems differ: Xray assigns no address on Linux, Windows gives the interface link-local addresses itself (an IPv6 one at once, an IPv4 one from `169.254.0.0/16` after a few seconds), and macOS and FreeBSD use `169.254.10.1/30`. \
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
macOS does not configure system DNS from the `dns` field, and neither does Linux by default; system DNS remains managed by the OS or distribution-specific network services. \
Windows, Linux, macOS and FreeBSD can also apply system routes from `autoSystemRoutingTable`.
macOS does not configure system DNS from the `dns` field, and neither do Linux and FreeBSD by default; system DNS remains managed by the OS or distribution-specific network services. \
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
### SYSTEM DNS ON LINUX (`autoSystemDnsToGateway`)
### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`)
On Linux, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it is Linux-only.
On Linux and FreeBSD, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it exists only on those two systems. This section describes Linux; the FreeBSD differences follow in [SYSTEM DNS ON FREEBSD](#system-dns-on-freebsd).
It uses `resolvectl`, which means it only works when all of these hold. Where Xray can tell that one does not, it does not start:
@@ -66,6 +66,24 @@ Where it cannot apply, Xray does not start, rather than run with the leak descri
On `Close()` the setting is reverted. It is **not** reverted if the process is killed with `SIGKILL`, since a process cannot handle that signal; run `resolvectl revert <iface>` to clean up by hand. An application that brings its own DNS endpoint is unaffected either way — this only covers the system resolver.
### SYSTEM DNS ON FREEBSD
On FreeBSD the same option goes through `resolvconf(8)`, the openresolv that ships in the base system, instead of `resolvectl`. The checks on Xray's own `dns` and routing configuration above apply unchanged, as does the loop through a `dns` upstream that bootstraps over the system resolver.
The address handed over is the gateway itself, not the next address: on FreeBSD the interface gets the address after the first IPv4 `gateway` (as on macOS), and the gateway, which nothing owns, is routed into the TUN, so a query to it is what Xray answers (e.g. `169.254.10.1/30` -> resolver `169.254.10.1`, local address `169.254.10.2`). Without `gateway` the config is rejected, as on Linux.
The entry is added with `resolvconf -a <iface> -x`, which makes the TUN's nameserver the only one in use, the equivalent of `domain ~.` plus `default-route true` above; without `-x` the nameservers DHCP supplied stay listed alongside it, and the system falls back to them. `resolvconf` exits successfully, and still records the entry, when `resolvconf=NO` is set in `resolvconf.conf`, but then runs none of the subscribers that write the resolver files, so Xray confirms the takeover in those files and does not start when the nameserver reached none of them: `/etc/resolv.conf` first, where it also has to be the only nameserver besides loopback ones (`inclusive_interfaces` in `resolvconf.conf` makes `resolvconf` ignore `-x` and keep the DHCP nameservers listed, which would be the leak again), and then, when `resolvconf.conf` names an `unbound_conf` file, that file. The latter is the `local_unbound` layout, where `local-unbound-setup` turns the `libc` subscriber off and has `resolvconf` write unbound's forwarders instead, with `resolv.conf` left pointed at loopback; the system then resolves through unbound, which forwards into the TUN. That only works with unbound's DNSSEC validation turned off, which `local_unbound` has on by default: the extra queries validation makes get no answer through the `dns` outbound, and resolution times out. A drop-in such as `/var/unbound/conf.d/xray.conf` with `server:` and `module-config: "iterator"`, followed by `service local_unbound restart`, turns it off (verified on FreeBSD 15.1).
| Environment | Behaviour |
|---|---|
| default FreeBSD, `resolv.conf` managed by `resolvconf` (DHCP or static) | applies |
| `local_unbound` enabled | applies, through unbound's forwarders; needs validation off (see above) |
| `resolvconf=NO` in `resolvconf.conf` | no resolver file changes, does not start |
| `inclusive_interfaces` covering the TUN in `resolvconf.conf` | DHCP nameservers stay listed, does not start |
| another resolver in front of `resolv.conf` that `resolvconf` is not configured to feed | nameserver reaches no resolver file, does not start |
On `Close()` the entry is removed with `resolvconf -d <iface>`. It is **not** removed if the process is killed with `SIGKILL`; run `resolvconf -d <iface>` to clean up by hand. The next start with the same interface name replaces the stale entry on its own.
Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \
Here is simple Xray config snippet to enable the inbound:
```
@@ -245,10 +263,10 @@ So everything applicable for ipv4 above also works for ipv6, you only need to gi
FreeBSD support of the same functionality is implemented through tun(4).
Interface name in the configuration must comply to the scheme "tunN", where N is some number. \
It's necessary to set an IP address to the interface, ex.:
```
ifconfig tun0 inet 169.254.10.1/30
```
FreeBSD requires the tun interface to have an IPv4 address before IPv4 routes can use it. \
By default Xray uses `169.254.10.1/30` as the gateway and assigns the next address in the prefix to the tun interface; the gateway itself belongs to nothing and is routed into the TUN. \
You can override this by setting `gateway`; FreeBSD uses the first IPv4 prefix in the list, and IPv6 `gateway` entries are not used for addressing.
To attach routing to the interface, route command like following can be executed:
```
route add -net 1.1.1.0/24 -iface tun10
@@ -257,6 +275,10 @@ route add -net 1.1.1.0/24 -iface tun10
route add -inet6 -host 2606:4700:4700::1111 -iface tun10
route add -inet6 -host 2606:4700:4700::1001 -iface tun10
```
Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running. \
With `autoSystemRoutingTable`, Xray's own connections are kept out of the TUN through `autoOutboundsInterface` (the default), which on FreeBSD switches them to a second routing table (FIB 1) that mirrors the physical default route; this needs the boot tunable `net.fibs` set to at least 2, and Xray does not start otherwise. \
`autoSystemDnsToGateway` points the system resolver at the TUN through `resolvconf(8)`, see [SYSTEM DNS ON FREEBSD](#system-dns-on-freebsd).
Important to remember that everything written above about Linux routing concept, also apply to FreeBSD. If you simply route default route through tun interface, that will result network loop and immediate network failure.
## MAC OS X SUPPORT
+34 -24
View File
@@ -56,6 +56,25 @@ func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
return nil
}
// TCPWriter is the writing side of a TCP connection of the stack for the dispatcher. It has no Close: the outbound
// closes its writer when the other side has no more to send, and the system may still be sending
type TCPWriter struct {
Writer buf.Writer
// Counter counts what is written, if there is one
Counter stats.Counter
}
func (w *TCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
size := mb.Len()
if err := w.Writer.WriteMultiBuffer(mb); err != nil {
return err
}
if w.Counter != nil {
w.Counter.Add(int64(size))
}
return nil
}
// ConnectionHandler interface with the only method that stack is going to push new connections to
type ConnectionHandler interface {
HandleConnection(conn net.Conn, destination net.Destination)
@@ -125,7 +144,8 @@ func (t *Handler) Start() error {
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
return errors.New("iface not found")
}
return c.Control(func(fd uintptr) {
var controlErr, sockErr error
controlErr = c.Control(func(fd uintptr) {
addrPort, _ := netip.ParseAddrPort(address)
// skip loopback
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
@@ -133,9 +153,13 @@ func (t *Handler) Start() error {
}
err := setinterface(network, address, fd, iface)
if err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
sockErr = errors.New("[tun] falied to set interface").Base(err)
}
})
if sockErr != nil {
return sockErr
}
return controlErr
})
}
@@ -143,6 +167,7 @@ func (t *Handler) Start() error {
tunStackOptions := StackOptions{
Tun: tunInterface,
MTU: t.config.MTU,
IdleTimeout: t.policyManager.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle,
}
tunStack, err := NewStack(t.ctx, tunStackOptions, t)
@@ -194,22 +219,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
defer cancel()
ctx = c.ContextWithID(ctx, session.NewID())
// if the connection is already closed, conn.RemoteAddr() will be nil
// due to gvisor weird behavior
remote := conn.RemoteAddr()
if remote == nil {
errors.LogInfo(t.ctx, "dropped quickly closed connection")
return
}
source := net.DestinationFromAddr(remote)
isUDP := destination.Network == net.Network_UDP
if !isUDP && (t.uplinkCounter != nil || t.downlinkCounter != nil) {
conn = &stat.CounterConnection{
Connection: conn,
ReadCounter: t.uplinkCounter,
WriteCounter: t.downlinkCounter,
}
}
source := net.DestinationFromAddr(conn.RemoteAddr())
inbound := session.Inbound{
Name: "tun",
@@ -235,13 +245,13 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
})
errors.LogInfo(ctx, "processing from ", source, " to ", destination)
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}
// both kinds of connection are buf.Reader and buf.Writer, what they carry is counted on the way
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn), Counter: t.uplinkCounter}
writer := buf.NewWriter(conn)
if isUDP {
reader.Counter = t.uplinkCounter
if t.downlinkCounter != nil {
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
}
if destination.Network != net.Network_UDP {
writer = &TCPWriter{Writer: writer, Counter: t.downlinkCounter}
} else if t.downlinkCounter != nil {
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
}
link := &transport.Link{
+48
View File
@@ -8,6 +8,7 @@ import (
"testing"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
@@ -103,6 +104,53 @@ func (d *testDispatcher) DispatchLink(ctx context.Context, dest xnet.Destination
return link.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(d.writePayload)})
}
// closableConn is a connection that is its own writer, as the ones of the stack are
type closableConn struct {
*testConn
closed int32
}
func (c *closableConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
buf.ReleaseMulti(mb)
return nil
}
func (c *closableConn) Close() error {
atomic.AddInt32(&c.closed, 1)
return nil
}
// closingDispatcher does what an outbound does when the other side has no more to send
type closingDispatcher struct {
testDispatcher
conn *closableConn
closed int32
}
func (d *closingDispatcher) DispatchLink(_ context.Context, _ xnet.Destination, link *transport.Link) error {
common.Close(link.Writer)
d.closed = atomic.LoadInt32(&d.conn.closed)
return nil
}
func TestHandlerClosesTunConnectionWhenBothSidesAreDone(t *testing.T) {
conn := &closableConn{testConn: newTestConn(nil)}
dispatcher := &closingDispatcher{conn: conn}
handler := &Handler{
ctx: context.Background(),
config: &Config{},
dispatcher: dispatcher,
}
handler.HandleConnection(conn, xnet.TCPDestination(xnet.LocalHostIP, 443))
if dispatcher.closed != 0 {
t.Fatal("the connection was closed with its writer, while the system may still be sending")
}
if got := atomic.LoadInt32(&conn.closed); got != 1 {
t.Fatalf("connection closed %d times, want 1", got)
}
}
func TestHandlerCountsTunConnectionTraffic(t *testing.T) {
uplinkCounter := new(testCounter)
downlinkCounter := new(testCounter)
-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)
}
+390
View File
@@ -0,0 +1,390 @@
package tun
import (
"encoding/binary"
"math"
"math/bits"
"net/netip"
"slices"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
)
const (
ipv4HeaderSize = 20
ipv6HeaderSize = 40
tcpHeaderSize = 20
udpHeaderSize = 8
protoICMPv4 = 1
protoTCP = 6
protoUDP = 17
protoICMPv6 = 58
// the smallest that a link is believed to take whatever is said about it: what every IPv4 host takes,
// and the smallest link that IPv6 runs on
ipv4SmallestMTU = 576
ipv6SmallestMTU = 1280
// the header that an IPv6 packet has in front of each of its fragments, and its number
fragmentHeaderSize = 8
protoFragment = 44
// fragmentsMost is how many packets can be waiting for the rest of their fragments,
// fragmentsTime how long they are waited for at most
fragmentsMost = 64
fragmentsTime = 30 * time.Second
)
// packet is an IP packet with its header read
type packet struct {
proto byte
src, dst netip.Addr
// payload is what follows the IP header
payload []byte
// fragment is set if payload is a part of what follows the IP header: the one at offset of the packet
// with the number id, which has more after it or not
fragment bool
more bool
offset int
id uint32
}
// parsePacket reads the IP header of b
func parsePacket(b []byte) (p packet, ok bool) {
if len(b) < ipv4HeaderSize {
return p, false
}
switch b[0] >> 4 {
case 4:
size := int(b[0]&0xf) * 4
total := int(binary.BigEndian.Uint16(b[2:]))
if size < ipv4HeaderSize || total < size || total > len(b) {
return p, false
}
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
p.fragment, p.more, p.offset, p.id = true, f&0x2000 != 0, int(f&0x1fff)*8, uint32(binary.BigEndian.Uint16(b[4:]))
}
p.proto = b[9]
p.src = netip.AddrFrom4([4]byte(b[12:16]))
p.dst = netip.AddrFrom4([4]byte(b[16:20]))
p.payload = b[size:total]
case 6:
if len(b) < ipv6HeaderSize {
return p, false
}
total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:]))
if total > len(b) {
return p, false
}
p.proto = b[6]
p.src = netip.AddrFrom16([16]byte(b[8:24]))
p.dst = netip.AddrFrom16([16]byte(b[24:40]))
p.payload = b[ipv6HeaderSize:total]
// hop-by-hop options, routing, destination options
for p.proto == 0 || p.proto == 43 || p.proto == 60 {
if len(p.payload) < 8 {
return p, false
}
size := int(p.payload[1])*8 + 8
if size > len(p.payload) {
return p, false
}
p.proto, p.payload = p.payload[0], p.payload[size:]
}
if p.proto == protoFragment {
f := p.payload
if len(f) < fragmentHeaderSize {
return p, false
}
p.fragment, p.more, p.offset, p.id = true, f[3]&1 != 0, int(binary.BigEndian.Uint16(f[2:])&^7), binary.BigEndian.Uint32(f[4:])
p.proto, p.payload = f[0], f[fragmentHeaderSize:]
}
default:
return p, false
}
return p, true
}
// verified reports whether the checksum of what the packet carries is right
func (p packet) verified() bool {
data := p.payload
switch p.proto {
case protoICMPv4:
return checksum(sum(data, 0)) == 0
case protoUDP:
if len(data) < udpHeaderSize {
return false
}
// the packet may be longer than what it carries
size := int(binary.BigEndian.Uint16(data[4:]))
if size < udpHeaderSize || size > len(data) {
return false
}
if p.src.Is4() && data[6]|data[7] == 0 {
// zero stands for no checksum
return true
}
data = data[:size]
case protoTCP, protoICMPv6:
default:
return true
}
pseudo := uint64(p.proto) + uint64(len(data))
if p.src.Is4() {
src, dst := p.src.As4(), p.dst.As4()
pseudo = sum(dst[:], sum(src[:], pseudo))
} else {
src, dst := p.src.As16(), p.dst.As16()
pseudo = sum(dst[:], sum(src[:], pseudo))
}
return checksum(sum(data, pseudo)) == 0
}
// putIPHeader writes the header of a packet from src to dst into b, without its length, and returns the size of the header
func putIPHeader(b []byte, proto byte, src, dst netip.Addr) int {
if src.Is4() {
s, d := src.As4(), dst.As4()
clear(b[:ipv4HeaderSize])
b[0] = 0x45
if proto == protoTCP {
b[6] = 0x40 // don't fragment
}
b[8] = 64
b[9] = proto
copy(b[12:], s[:])
copy(b[16:], d[:])
return ipv4HeaderSize
}
s, d := src.As16(), dst.As16()
clear(b[:ipv6HeaderSize])
b[0] = 0x60
b[6] = proto
b[7] = 64
copy(b[8:], s[:])
copy(b[24:], d[:])
return ipv6HeaderSize
}
// setIPLength completes the header ip of a packet that is size bytes long with it
func setIPLength(ip []byte, size int) {
if len(ip) == ipv4HeaderSize {
binary.BigEndian.PutUint16(ip[2:], uint16(size))
ip[10], ip[11] = 0, 0
binary.BigEndian.PutUint16(ip[10:], checksum(sum(ip, 0)))
return
}
binary.BigEndian.PutUint16(ip[4:], uint16(size-ipv6HeaderSize))
}
// transportChecksum is the checksum of a TCP or UDP segment, header and payload, of a packet with the header ip.
// The checksum field of header is zero
func transportChecksum(ip []byte, proto byte, header, payload []byte) uint16 {
return checksum(sum(payload, sum(header, pseudoSum(ip, proto, len(header)+len(payload)))))
}
// pseudoSum is what the header ip of a packet adds to the checksum of its TCP or UDP segment of size bytes
func pseudoSum(ip []byte, proto byte, size int) uint64 {
addresses := ip[12:ipv4HeaderSize]
if len(ip) == ipv6HeaderSize {
addresses = ip[8:]
}
return sum(addresses, uint64(proto)+uint64(size))
}
// sum adds the 16-bit words of b to s, with the carry of every addition added back
func sum(b []byte, s uint64) uint64 {
var c uint64
for len(b) >= 32 {
s, c = bits.Add64(s, binary.BigEndian.Uint64(b), c)
s, c = bits.Add64(s, binary.BigEndian.Uint64(b[8:]), c)
s, c = bits.Add64(s, binary.BigEndian.Uint64(b[16:]), c)
s, c = bits.Add64(s, binary.BigEndian.Uint64(b[24:]), c)
b = b[32:]
}
for len(b) >= 8 {
s, c = bits.Add64(s, binary.BigEndian.Uint64(b), c)
b = b[8:]
}
if len(b) >= 4 {
s, c = bits.Add64(s, uint64(binary.BigEndian.Uint32(b)), c)
b = b[4:]
}
if len(b) >= 2 {
s, c = bits.Add64(s, uint64(binary.BigEndian.Uint16(b)), c)
b = b[2:]
}
if len(b) == 1 {
s, c = bits.Add64(s, uint64(b[0])<<8, c)
}
s, c = bits.Add64(s, 0, c)
return s + c
}
// checksum folds s into the 16 bits that go into a header
func checksum(s uint64) uint16 {
s = s>>32 + s&0xffffffff
s = s>>16 + s&0xffff
s = s>>16 + s&0xffff
s = s>>16 + s&0xffff
return ^uint16(s)
}
// writeFragments writes a packet that is larger than mtu to tun in fragments that are not. header is its IP header
// with what follows that up to payload, which is no more than the header of a UDP packet. id is the number
// of the packet if it is an IPv6 one
func writeFragments(tun Tun, mtu int, id uint32, header, payload []byte) error {
var b [ipv6HeaderSize + fragmentHeaderSize + udpHeaderSize]byte
size := ipv4HeaderSize
if header[0]>>4 == 6 {
size = ipv6HeaderSize
}
n := copy(b[:], header[:size])
if size == ipv6HeaderSize {
// the number goes into a header of its own, behind the IP header. That of IPv4 has it
b[6], b[n] = protoFragment, header[6]
binary.BigEndian.PutUint32(b[n+4:], id)
n += fragmentHeaderSize
}
// first goes in front of payload, in the first fragment
first := header[size:]
if n+len(first) > len(b) {
return errors.New("no room for a header of ", len(first), " bytes in a fragment")
}
// every fragment but the last is a number of 8 bytes
room, total := (mtu-n)&^7, len(first)+len(payload)
if room < 8 {
return errors.New("no room for a fragment in an MTU of ", mtu)
}
for offset := 0; offset < total; {
h, part := b[:n], min(room, total-offset)
last := offset+part == total
if size == ipv6HeaderSize {
f := uint16(offset)
if !last {
f |= 1
}
binary.BigEndian.PutUint16(b[size+2:], f)
} else {
f := uint16(offset / 8)
if !last {
f |= 0x2000
}
binary.BigEndian.PutUint16(b[6:], f)
}
setIPLength(b[:size], n+part)
data := payload[max(offset-len(first), 0):min(offset+part-len(first), len(payload))]
if offset == 0 {
h = b[:n+copy(b[n:], first)]
}
if err := tun.WritePacket(h, data); err != nil {
return err
}
offset += part
}
return nil
}
// fragmented is a packet of which not every fragment has come yet
type fragmented struct {
// data is what follows its IP header, as far as its last fragment that has come goes
data []byte
// parts are what of data has come, in order and apart from each other
parts [][2]int
// size is that of data when all of it has come, known from the last fragment
size int
known bool
since time.Time
}
// reassembly puts the packets that come in fragments together
type reassembly struct {
mu sync.Mutex
packets map[fragments]*fragmented
}
// fragments is what the fragments of a packet have in common
type fragments struct {
src, dst netip.Addr
proto byte
id uint32
}
// add takes the fragment p of a packet. With the last one that was missing, it returns the packet.
// A packet whose fragments do not fit each other is given up
func (r *reassembly) add(p packet) (packet, bool) {
fragment, offset, end := p.payload, p.offset, p.offset+len(p.payload)
// what an IP packet can carry
most := math.MaxUint16
if p.src.Is4() {
most -= ipv4HeaderSize
}
if len(fragment) == 0 || end > most || p.more && len(fragment)%8 != 0 {
return p, false
}
key, now := fragments{p.src, p.dst, p.proto, p.id}, time.Now()
r.mu.Lock()
defer r.mu.Unlock()
f := r.packets[key]
if f != nil && now.Sub(f.since) > fragmentsTime {
// what is left of a packet whose other fragments were lost: this is one of another packet with its number
f = nil
}
if f == nil {
if _, stale := r.packets[key]; !stale && len(r.packets) >= fragmentsMost {
// the one that is waited for the longest makes room, not the one that comes now
var longest fragments
var since time.Time
for other, old := range r.packets {
if since.IsZero() || old.since.Before(since) {
longest, since = other, old.since
}
}
delete(r.packets, longest)
}
if r.packets == nil {
r.packets = make(map[fragments]*fragmented)
}
f = &fragmented{since: now}
r.packets[key] = f
}
for _, part := range f.parts {
if offset >= part[0] && end <= part[1] {
// came already
return p, false
}
if offset < part[1] && part[0] < end {
// fragments of one packet do not overlap: these are of two, or made to be taken for something else
delete(r.packets, key)
return p, false
}
}
if f.known && end > f.size || !p.more && len(f.data) > end {
// past its end, or an end before what came
delete(r.packets, key)
return p, false
}
if !p.more {
f.size, f.known = end, true
}
if len(f.data) < end {
f.data = append(f.data, make([]byte, end-len(f.data))...)
}
copy(f.data[offset:], fragment)
i := 0
for i < len(f.parts) && f.parts[i][1] < offset {
i++
}
j := i
for ; j < len(f.parts) && f.parts[j][0] <= end; j++ {
offset, end = min(offset, f.parts[j][0]), max(end, f.parts[j][1])
}
f.parts = slices.Replace(f.parts, i, j, [2]int{offset, end})
if !f.known || f.parts[0] != [2]int{0, f.size} {
return p, false
}
delete(r.packets, key)
p.payload, p.fragment = f.data[:f.size], false
return p, true
}
+527
View File
@@ -1,17 +1,544 @@
package tun
import (
"context"
"encoding/binary"
"io"
"math"
"math/rand/v2"
gonet "net"
"net/netip"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
// Stack interface implement ip protocol stack, bridging raw network packets and data streams
type Stack interface {
// Start makes the stack read the packets of its tun
Start() error
Close() error
// Input takes a packet of a tun that hands them over instead of being read. The packet is not kept,
// but it may be written to. The packets of one connection have to come one after another
Input(packet []byte)
// DialTCP opens a connection to address from the one of StackOptions.Addresses that is of its family
DialTCP(ctx context.Context, address netip.AddrPort) (net.Conn, error)
// DialUDP opens a port on StackOptions.Addresses. What anyone sends to the port is read from it
DialUDP() (net.PacketConn, error)
}
// StackOptions for the stack implementation
type StackOptions struct {
Tun Tun
MTU uint32
IdleTimeout time.Duration
// Addresses are what the stack opens connections from
Addresses []netip.Addr
// Remote is set if the tun is a tunnel to another system and not the tun of this one: what comes through it
// has come a long way, so its checksums are looked at, nothing but that way limits the windows, and the
// segments of TCP carry the times of both sides if the other side has them
Remote bool
}
// flow is what the packets of one connection have in common, src is the side of the system
type flow struct {
src, dst netip.AddrPort
}
// stack turns the packets that the system sends to the tun into connections and back.
// It has only what the two need to talk through a tun, and a local link when the system passes on
// what others send, see tcpConn. The tun may also be a tunnel to a system that is far away, and the stack
// the one that opens the connections
type stack struct {
ctx context.Context
tun Tun
mtu int
// segmenter is the tun if it takes segments that are larger than its MTU
segmenter segmenter
// window is the window of every connection
window int32
// verify is set if the checksums of what comes are looked at
verify bool
// stamps is set if connections carry the times of RFC 7323 when the other side does
stamps bool
// handle is given every new connection that the other side opens. Without it those are refused
handle func(conn net.Conn, destination net.Destination)
udp *udpConnectionHandler
// addresses are what connections are opened from
addresses []netip.Addr
mu sync.Mutex
conns map[flow]*tcpConn
// sockets are the ports that DialUDP has opened
sockets map[netip.AddrPort]*udpSocket
closed bool
// opening is how many of conns wait for the answer to their SYN or SYN-ACK
opening atomic.Int32
// fragments are the packets that are larger than a link on their way and come in parts.
// identification numbers the ones that leave like that: a counter for each of a few groups of destinations,
// each from a start of its own, so that no two of them that go one way meet, and no destination learns how much
// goes to the others
fragments reassembly
identification [64]atomic.Uint32
}
// NewStack builds new ip stack. Without a handler it takes no connections, only opens them
func NewStack(ctx context.Context, options StackOptions, handler ConnectionHandler) (Stack, error) {
s := &stack{
ctx: ctx,
tun: options.Tun,
mtu: int(options.MTU),
window: tcpWindow,
addresses: options.Addresses,
verify: options.Remote,
stamps: options.Remote,
conns: make(map[flow]*tcpConn),
sockets: make(map[netip.AddrPort]*udpSocket),
}
if t, ok := options.Tun.(segmenter); ok && t.Segments() {
s.segmenter = t
s.window = tcpLargeWindow
}
if options.Remote {
s.window = tcpLargeWindow
}
for i := range s.identification {
s.identification[i].Store(rand.Uint32())
}
if handler != nil {
s.handle = handler.HandleConnection
s.udp = newUdpConnectionHandler(s.handle, s.writeUDP)
}
return s, nil
}
// Start is called by Handler to bring stack to life
func (s *stack) Start() error {
go s.read()
return nil
}
// Close is called by Handler to shut down the stack
func (s *stack) Close() error {
s.mu.Lock()
s.closed = true
conns := make([]*tcpConn, 0, len(s.conns))
for _, c := range s.conns {
conns = append(conns, c)
}
sockets := s.sockets
s.sockets = nil
s.mu.Unlock()
for _, c := range conns {
c.mu.Lock()
if c.err == nil {
c.reset()
}
c.mu.Unlock()
}
for _, u := range sockets {
u.queue.close()
}
return nil
}
// read takes the packets of the tun until it is closed
func (s *stack) read() {
// a tun that does not split segments gives them as large as a packet can be
b := make([]byte, packetRoom+1<<16)
for {
b, err := s.tun.ReadPacket(b)
if err != nil {
s.mu.Lock()
closed := s.closed
s.mu.Unlock()
if !closed {
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
}
return
}
s.Input(b)
}
}
// Input implements Stack. Several tunnels may hand over their packets at once
func (s *stack) Input(b []byte) {
p, ok := parsePacket(b)
if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() {
// nothing comes from many at once, and an address that stands for this machine itself is not one
// that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which
// would be dialed as that: ::ffff:0.0.0.0 is this machine as well
return
}
if s.verify && b[0]>>4 == 4 && checksum(sum(b[:int(b[0]&0xf)*4], 0)) != 0 {
return
}
// what was put together may be of two packets with one number, its checksum tells
verify := s.verify || p.fragment
if p.fragment {
p, ok = s.fragments.add(p)
}
if !ok || verify && !p.verified() {
return
}
switch p.proto {
case protoTCP:
s.handleTCP(p)
case protoUDP:
s.handleUDP(p)
case protoICMPv4, protoICMPv6:
s.handleICMP(p)
}
}
// local returns the address that packets to remote are sent from
func (s *stack) local(remote netip.Addr) (netip.Addr, error) {
for _, a := range s.addresses {
if a.Is4() == remote.Is4() {
return a, nil
}
}
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) {
t := p.payload
if len(t) < tcpHeaderSize {
return
}
size := int(t[12]>>4) * 4
if size < tcpHeaderSize || size > len(t) {
return
}
f := flow{
src: netip.AddrPortFrom(p.src, binary.BigEndian.Uint16(t)),
dst: netip.AddrPortFrom(p.dst, binary.BigEndian.Uint16(t[2:])),
}
seg := segment{
seq: binary.BigEndian.Uint32(t[4:]),
ack: binary.BigEndian.Uint32(t[8:]),
flags: t[13],
window: binary.BigEndian.Uint16(t[14:]),
options: t[tcpHeaderSize:size],
payload: t[size:],
}
s.mu.Lock()
c := s.conns[f]
s.mu.Unlock()
if c != nil && c.input(seg) {
return
}
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn || s.handle == nil {
if seg.flags&tcpRst == 0 {
s.refuse(f, seg)
}
return
}
if s.opening.Load() >= tcpOpening {
return
}
c = newTCPConn(s, f)
c.synchronize(seg)
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return
}
s.conns[f] = c
s.mu.Unlock()
s.opening.Add(1)
c.mu.Lock()
c.sendSyn()
c.arm(tcpTick)
c.mu.Unlock()
}
// refuse answers a segment of a connection that is not known with a reset
func (s *stack) refuse(f flow, seg segment) {
var b [ipv6HeaderSize + tcpHeaderSize]byte
n := putIPHeader(b[:], protoTCP, f.dst.Addr(), f.src.Addr())
t := b[n : n+tcpHeaderSize]
binary.BigEndian.PutUint16(t, f.dst.Port())
binary.BigEndian.PutUint16(t[2:], f.src.Port())
t[12] = tcpHeaderSize / 4 << 4
if seg.flags&tcpAck != 0 {
binary.BigEndian.PutUint32(t[4:], seg.ack)
t[13] = tcpRst
} else {
ack := seg.seq + uint32(len(seg.payload))
if seg.flags&(tcpSyn|tcpFin) != 0 {
ack++
}
binary.BigEndian.PutUint32(t[8:], ack)
t[13] = tcpRst | tcpAck
}
setIPLength(b[:n], n+tcpHeaderSize)
binary.BigEndian.PutUint16(t[16:], transportChecksum(b[:n], protoTCP, t, nil))
_ = s.tun.WritePacket(b[:n+tcpHeaderSize], nil)
}
// remove forgets a connection. c.mu is held
func (s *stack) remove(c *tcpConn) {
if !c.established {
s.opening.Add(-1)
}
s.mu.Lock()
if s.conns[c.flow] == c {
delete(s.conns, c.flow)
}
s.mu.Unlock()
}
func (s *stack) handleUDP(p packet) {
u := p.payload
if len(u) < udpHeaderSize {
return
}
size := int(binary.BigEndian.Uint16(u[4:]))
if size < udpHeaderSize || size > len(u) {
return
}
var socket *udpSocket
if s.handle == nil {
// only for a port that was opened from here
s.mu.Lock()
socket = s.sockets[netip.AddrPortFrom(p.dst, binary.BigEndian.Uint16(u[2:]))]
s.mu.Unlock()
if socket == nil {
return
}
}
// the packet waits for its reader, longer than read keeps it
b := buf.NewWithSize(int32(size - udpHeaderSize))
b.Write(u[udpHeaderSize:size])
src := net.UDPDestination(net.IPAddress(p.src.AsSlice()), net.Port(binary.BigEndian.Uint16(u)))
if socket == nil {
s.udp.HandlePacket(src, net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:]))), b)
} else if b.UDP = &src; !socket.queue.push(b) {
b.Release()
}
}
// writeUDP writes a packet with the payload from src to dst to the tun
func (s *stack) writeUDP(payload []byte, src net.Destination, dst net.Destination) error {
srcIP, _ := netip.AddrFromSlice(src.Address.IP())
dstIP, _ := netip.AddrFromSlice(dst.Address.IP())
return s.sendUDP(payload, netip.AddrPortFrom(srcIP, uint16(src.Port)), netip.AddrPortFrom(dstIP, uint16(dst.Port)))
}
func (s *stack) sendUDP(payload []byte, src, dst netip.AddrPort) error {
srcIP, dstIP := src.Addr().Unmap(), dst.Addr().Unmap()
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() || len(payload) > math.MaxUint16-udpHeaderSize {
return errors.New("failed to write udp packet of ", len(payload), " bytes from ", src, " to ", dst)
}
var b [ipv6HeaderSize + udpHeaderSize]byte
n := putIPHeader(b[:], protoUDP, srcIP, dstIP)
u := b[n : n+udpHeaderSize]
binary.BigEndian.PutUint16(u, src.Port())
binary.BigEndian.PutUint16(u[2:], dst.Port())
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(payload)))
check := transportChecksum(b[:n], protoUDP, u, payload)
if check == 0 {
// zero stands for no checksum
check = 0xffff
}
binary.BigEndian.PutUint16(u[6:], check)
return s.write(b[:n+udpHeaderSize], payload)
}
// 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) {
m := p.payload
request, reply := byte(8), byte(0)
if p.proto == protoICMPv6 {
request, reply = 128, 129
}
if len(m) < 8 {
return
}
switch {
case p.proto == protoICMPv4 && m[0] == 3 && m[1] == 4:
s.undelivered(m[8:], max(int(binary.BigEndian.Uint16(m[6:])), ipv4SmallestMTU))
return
case p.proto == protoICMPv6 && m[0] == 2:
s.undelivered(m[8:], max(int(binary.BigEndian.Uint32(m[4:])), ipv6SmallestMTU))
return
case p.proto == protoICMPv4 && m[0] == 3, p.proto == protoICMPv6 && m[0] == 1:
s.undelivered(m[8:], 0)
return
}
if m[0] != request || m[1] != 0 || s.handle == nil && !slices.Contains(s.addresses, p.dst) {
// a stack that takes no connections is one host, not all of them
return
}
var b [ipv6HeaderSize]byte
n := putIPHeader(b[:], p.proto, p.dst, p.src)
// the reply is the request with another type
m[0], m[2], m[3] = reply, 0, 0
if p.proto == protoICMPv6 {
binary.BigEndian.PutUint16(m[2:], transportChecksum(b[:n], protoICMPv6, m, nil))
} else {
binary.BigEndian.PutUint16(m[2:], checksum(sum(m, 0)))
}
_ = s.write(b[:n], m)
}
// undelivered takes the start of a packet that was sent from here and did not get to where it goes. If it did not fit
// a link on its way, which takes packets of mtu bytes, the connection that sent it goes on with smaller segments.
// Without an mtu there is no way at all, and a connection that is being opened is not waited for
func (s *stack) undelivered(b []byte, mtu int) {
var proto byte
var local, remote netip.Addr
switch {
case len(b) >= ipv4HeaderSize && b[0]>>4 == 4:
proto, local, remote = b[9], netip.AddrFrom4([4]byte(b[12:16])), netip.AddrFrom4([4]byte(b[16:20]))
b = b[min(int(b[0]&0xf)*4, len(b)):]
case len(b) >= ipv6HeaderSize && b[0]>>4 == 6:
proto, local, remote = b[6], netip.AddrFrom16([16]byte(b[8:24])), netip.AddrFrom16([16]byte(b[24:40]))
b = b[ipv6HeaderSize:]
}
// of the segment there are the ports and the sequence number at least
if proto != protoTCP || len(b) < 8 {
return
}
s.mu.Lock()
c := s.conns[flow{
src: netip.AddrPortFrom(remote, binary.BigEndian.Uint16(b[2:])),
dst: netip.AddrPortFrom(local, binary.BigEndian.Uint16(b)),
}]
s.mu.Unlock()
if c == nil {
return
}
if seq := binary.BigEndian.Uint32(b[4:]); mtu != 0 {
c.shrink(seq, mtu)
} else {
c.unreachable(seq)
}
}
-267
View File
@@ -1,267 +0,0 @@
package tun
import (
"context"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/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"
"gvisor.dev/gvisor/pkg/waiter"
)
const (
defaultNIC tcpip.NICID = 1
tcpRXBufMinSize = tcp.MinBufferSize
tcpRXBufDefSize = tcp.DefaultSendBufferSize
tcpRXBufMaxSize = 8 << 20 // 8MiB
tcpTXBufMinSize = tcp.MinBufferSize
tcpTXBufDefSize = tcp.DefaultReceiveBufferSize
tcpTXBufMaxSize = 6 << 20 // 6MiB
)
// stackGVisor is ip stack implemented by gVisor package
type stackGVisor struct {
ctx context.Context
tun Tun
idleTimeout time.Duration
handler *Handler
stack *stack.Stack
endpoint stack.LinkEndpoint
}
// NewStack builds new ip stack (using gVisor)
func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) {
gStack := &stackGVisor{
ctx: ctx,
tun: options.Tun,
idleTimeout: options.IdleTimeout,
handler: handler,
}
return gStack, nil
}
// Start is called by Handler to bring stack to life
func (t *stackGVisor) Start() error {
linkEndpoint, err := t.tun.newEndpoint()
if err != nil {
return err
}
ipStack, err := createStack(linkEndpoint)
if err != nil {
return err
}
tcpForwarder := tcp.NewForwarder(ipStack, 0, 65535, func(r *tcp.ForwarderRequest) {
go func(r *tcp.ForwarderRequest) {
var wq waiter.Queue
id := r.ID()
// Perform a TCP three-way handshake.
ep, err := r.CreateEndpoint(&wq)
if err != nil {
errors.LogError(t.ctx, err.String())
r.Complete(true)
return
}
options := ep.SocketOptions()
options.SetKeepAlive(false)
options.SetReuseAddress(true)
options.SetReusePort(true)
t.handler.HandleConnection(
gonet.NewTCPConn(&wq, ep),
// local address on the gVisor side is connection destination
net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)),
)
// close the socket
ep.Close()
// send connection complete upstream
r.Complete(false)
}(r)
})
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
// Use custom UDP packet handler, instead of strict gVisor forwarder, for FullCone NAT support
udpForwarder := newUdpConnectionHandler(t.handler.HandleConnection, t.writeRawUDPPacket)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
data := pkt.Data().AsRange().ToSlice()
// if len(data) == 0 {
// return false
// }
// source/destination of the packet we process as incoming, on gVisor side are Remote/Local
// in other terms, src is the side behind tun, dst is the side behind gVisor
// this function handle packets passing from the tun to the gVisor, therefore the src/dst assignement
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))
udpForwarder.HandlePacket(src, dst, data)
return true
})
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, t.handleICMPv4Packet)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, t.handleICMPv6Packet)
t.stack = ipStack
t.endpoint = linkEndpoint
return nil
}
func (t *stackGVisor) 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 := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView()))
if err != nil {
return errors.New("failed to write raw udp packet back to stack", err)
}
return nil
}
// Close is called by Handler to shut down the stack
func (t *stackGVisor) Close() error {
if t.stack == nil {
return nil
}
t.endpoint.Attach(nil)
t.stack.Close()
for _, endpoint := range t.stack.CleanupEndpoints() {
endpoint.Abort()
}
return nil
}
// createStack configure gVisor ip stack
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
HandleLocal: false,
}
gStack := stack.New(opts)
err := gStack.CreateNIC(defaultNIC, ep)
if err != nil {
return nil, errors.New(err.String())
}
gStack.SetRouteTable([]tcpip.Route{
{Destination: header.IPv4EmptySubnet, NIC: defaultNIC},
{Destination: header.IPv6EmptySubnet, NIC: defaultNIC},
})
err = gStack.SetSpoofing(defaultNIC, true)
if err != nil {
return nil, errors.New(err.String())
}
err = gStack.SetPromiscuousMode(defaultNIC, true)
if err != nil {
return nil, errors.New(err.String())
}
cOpt := tcpip.CongestionControlOption("cubic")
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &cOpt)
sOpt := tcpip.TCPSACKEnabled(true)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt)
mOpt := tcpip.TCPModerateReceiveBufferOption(true)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &mOpt)
// Disable RACK/TLP loss recovery to fix connection stalls under high load
rOpt := tcpip.TCPRecovery(0)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &rOpt)
tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{
Min: tcpRXBufMinSize,
Default: tcpRXBufDefSize,
Max: tcpRXBufMaxSize,
}
err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt)
if err != nil {
return nil, errors.New(err.String())
}
tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{
Min: tcpTXBufMinSize,
Default: tcpTXBufDefSize,
Max: tcpTXBufMaxSize,
}
err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt)
if err != nil {
return nil, errors.New(err.String())
}
return gStack, nil
}
-153
View File
@@ -1,153 +0,0 @@
package tun
import (
"context"
"errors"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
var ErrQueueEmpty = errors.New("queue is empty")
type GVisorDevice interface {
WritePacket(packet *stack.PacketBuffer) tcpip.Error
ReadPacket() (byte, *stack.PacketBuffer, error)
Wait()
}
// LinkEndpoint implements GVisor stack.LinkEndpoint
var _ stack.LinkEndpoint = (*LinkEndpoint)(nil)
type LinkEndpoint struct {
deviceMTU uint32
device GVisorDevice
dispatcherCancel context.CancelFunc
}
func (e *LinkEndpoint) MTU() uint32 {
return e.deviceMTU
}
func (e *LinkEndpoint) SetMTU(_ uint32) {
// not Implemented, as it is not expected GVisor will be asking tun device to be modified
}
func (e *LinkEndpoint) MaxHeaderLength() uint16 {
return 0
}
func (e *LinkEndpoint) LinkAddress() tcpip.LinkAddress {
return ""
}
func (e *LinkEndpoint) SetLinkAddress(_ tcpip.LinkAddress) {
// not Implemented, as it is not expected GVisor will be asking tun device to be modified
}
func (e *LinkEndpoint) Capabilities() stack.LinkEndpointCapabilities {
return stack.CapabilityRXChecksumOffload
}
func (e *LinkEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
if e.dispatcherCancel != nil {
e.dispatcherCancel()
e.dispatcherCancel = nil
}
if dispatcher != nil {
ctx, cancel := context.WithCancel(context.Background())
go e.dispatchLoop(ctx, dispatcher)
e.dispatcherCancel = cancel
}
}
func (e *LinkEndpoint) IsAttached() bool {
return e.dispatcherCancel != nil
}
func (e *LinkEndpoint) Wait() {
}
func (e *LinkEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
func (e *LinkEndpoint) AddHeader(buffer *stack.PacketBuffer) {
// tun interface doesn't have link layer header, it will be added by the OS
}
func (e *LinkEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
return true
}
func (e *LinkEndpoint) Close() {
if e.dispatcherCancel != nil {
e.dispatcherCancel()
e.dispatcherCancel = nil
}
}
func (e *LinkEndpoint) SetOnCloseAction(_ func()) {
}
func (e *LinkEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) {
var n int
var err tcpip.Error
for _, packetBuffer := range packetBufferList.AsSlice() {
err = e.device.WritePacket(packetBuffer)
if err != nil {
return n, &tcpip.ErrAborted{}
}
n++
}
return n, nil
}
func (e *LinkEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) {
var networkProtocolNumber tcpip.NetworkProtocolNumber
var version byte
var packet *stack.PacketBuffer
var err error
for {
select {
case <-ctx.Done():
return
default:
version, packet, err = e.device.ReadPacket()
// on "queue empty", ask device to yield slightly and continue
if errors.Is(err, ErrQueueEmpty) {
e.device.Wait()
continue
}
// stop dispatcher loop on any other interface failure
if err != nil {
e.Attach(nil)
return
}
// extract network protocol number from the packet first byte
// (which is returned separately, since it is so incredibly hard to extract one byte from
// stack.PacketBuffer without additional memory allocation and full copying it back and forth)
switch version {
case 4:
networkProtocolNumber = header.IPv4ProtocolNumber
case 6:
networkProtocolNumber = header.IPv6ProtocolNumber
default:
// discard unknown network protocol packet
packet.DecRef()
continue
}
// dispatch the buffer to the stack
dispatcher.DeliverNetworkPacket(networkProtocolNumber, packet)
// signal the buffer that it can be released
packet.DecRef()
}
}
}
-98
View File
@@ -1,98 +0,0 @@
package tun
import (
"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"
)
func (t *stackGVisor) handleICMPv4Packet(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
return t.handleICMPEchoPacket(header.IPv4ProtocolNumber, id, pkt)
}
func (t *stackGVisor) handleICMPv6Packet(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
return t.handleICMPEchoPacket(header.IPv6ProtocolNumber, id, pkt)
}
func (t *stackGVisor) handleICMPEchoPacket(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
}
message := transportPacketBytes(pkt)
ident, sequence, ok := tunicmp.ParseEchoRequest(netProto, message)
if !ok {
return true
}
reply, err := tunicmp.BuildLocalEchoReply(netProto, message, dstIP, srcIP)
if err != nil {
errors.LogInfoInner(t.ctx, err, "[tun] failed to build local icmp echo reply")
return true
}
errors.LogDebug(t.ctx, "[tun][icmp] ", tunicmp.ProtocolLabel(netProto), " local echo reply ", dstIP, " -> ", srcIP, " id=", ident, " seq=", sequence)
if err := t.writeRawICMPPacket(netProto, reply, dstIP, srcIP); err != nil {
errors.LogInfoInner(t.ctx, err, "[tun] failed to write local icmp echo reply")
}
return true
}
func (t *stackGVisor) writeRawICMPPacket(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
ipHeaderSize := header.IPv6MinimumSize
ipProtocol := header.IPv6ProtocolNumber
transportProtocol := header.ICMPv6ProtocolNumber
if netProto == header.IPv4ProtocolNumber {
ipHeaderSize = header.IPv4MinimumSize
ipProtocol = header.IPv4ProtocolNumber
transportProtocol = header.ICMPv4ProtocolNumber
}
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: ipHeaderSize,
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(transportProtocol),
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: transportProtocol,
HopLimit: 64,
SrcAddr: srcIP,
DstAddr: dstIP,
})
}
if err := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView())); err != nil {
return errors.New("failed to write raw icmp packet back to stack", err)
}
return nil
}
func transportPacketBytes(pkt *stack.PacketBuffer) []byte {
headerBytes := pkt.TransportHeader().Slice()
payloadBytes := pkt.Data().AsRange().ToSlice()
message := make([]byte, len(headerBytes)+len(payloadBytes))
copy(message, headerBytes)
copy(message[len(headerBytes):], payloadBytes)
return message
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -1,6 +1,7 @@
package tun
import "gvisor.dev/gvisor/pkg/tcpip/stack"
// packetRoom is what a tun may need in front of a packet in the buffer that it is read into
const packetRoom = 16
// Tun interface implements tun interface interaction
type Tun interface {
@@ -8,5 +9,18 @@ type Tun interface {
Close() error
Name() (string, error)
Index() (int, error)
newEndpoint() (stack.LinkEndpoint, error)
// ReadPacket waits for a packet, reads it into b and returns it
ReadPacket(b []byte) ([]byte, error)
// WritePacket writes a packet that is in two parts, the second of which may be empty
WritePacket(header, payload []byte) error
}
// segmenter is a tun whose other side takes TCP segments that are larger than the MTU,
// splits them only if it has to send them on, and completes their checksum then
type segmenter interface {
// Segments reports whether WriteSegment can be used
Segments() bool
// WriteSegment writes a packet with a TCP segment whose payload is in parts, to be sent on with mss bytes
// of it at most in one. Its checksum is that of the pseudo-header only, not inverted
WriteSegment(header []byte, payload [][]byte, mss int) error
}
+10 -14
View File
@@ -10,12 +10,10 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
type AndroidTun struct {
tunFd int
*fdTun
options *Config
}
@@ -33,8 +31,14 @@ func NewTun(options *Config) (Tun, error) {
return nil, err
}
// the descriptor stays with the application, Close ends the reading of a duplicate
fd, err = unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return nil, err
}
return &AndroidTun{
tunFd: fd,
fdTun: newFdTun(fd, 0),
options: options,
}, nil
}
@@ -44,7 +48,7 @@ func (t *AndroidTun) Start() error {
}
func (t *AndroidTun) Close() error {
return nil
return t.closeFile()
}
func (t *AndroidTun) Name() (string, error) {
@@ -52,7 +56,7 @@ func (t *AndroidTun) Name() (string, error) {
if err != nil {
return "", err
}
if err = unix.IoctlIfreq(t.tunFd, unix.TUNGETIFF, ifr); err != nil {
if err = unix.IoctlIfreq(t.fd, unix.TUNGETIFF, ifr); err != nil {
return "", err
}
return ifr.Name(), nil
@@ -70,14 +74,6 @@ func (t *AndroidTun) Index() (int, error) {
return iface.Index, nil
}
func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
return fdbased.New(&fdbased.Options{
FDs: []int{t.tunFd},
MTU: t.options.MTU,
RXChecksumOffload: true,
})
}
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.BindToDevice(int(fd), iface.Name)
}
+36 -76
View File
@@ -20,9 +20,6 @@ import (
"github.com/xtls/xray-core/common/platform"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const (
@@ -47,8 +44,8 @@ type DarwinTun struct {
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
// Genuinely blocks Wait() until tunFd is readable, instead of the
// previous procyield-only busy-spin (dispatchLoop in
// stack_gvisor_endpoint.go calls ReadPacket() then Wait() in a tight
// previous procyield-only busy-spin (ReadPacket() reads and then calls
// Wait() in a tight
// loop with no other throttling whenever the queue is empty -- with
// only procyield(1), that pins a full CPU core for as long as the
// tunnel is up, observed causing severe device heating/thermal
@@ -66,7 +63,7 @@ type DarwinTun struct {
// waitKqueue owns a kqueue fd used by DarwinTun.Wait() to block on
// read-readiness. Closing and waiting can race from different goroutines
// (Close() from the caller that tears down the tunnel, Wait() from
// dispatchLoop's own goroutine) -- reviewer feedback on XTLS/Xray-core#6580
// the goroutine that reads packets) -- reviewer feedback on XTLS/Xray-core#6580
// found that a bare `int` fd field let Close() race Wait()'s use of the
// same fd number, and on Darwin a closed fd number can be reused by an
// unrelated concurrent open() before Wait() gets to call Kevent on it,
@@ -110,9 +107,9 @@ func newWaitKqueue(fd int) *waitKqueue {
// wait blocks until the registered fd is readable, timeout elapses, or a
// benign interrupt occurs -- all three are "this kqueue is still healthy,
// the caller should just try again" and return true; the caller
// (DarwinTun.Wait) doesn't need to distinguish them since it always calls
// ReadPacket() right after anyway, and that already handles "nothing was
// actually there" via ErrQueueEmpty. Returns false only when the kqueue
// (DarwinTun.Wait) doesn't need to distinguish them since ReadPacket()
// reads right after anyway, and that already handles "nothing was
// actually there" by waiting again. Returns false only when the kqueue
// itself is no longer usable -- already closed, or the kevent syscall
// failed for a reason other than EINTR -- see its own call site in
// DarwinTun.Wait for why a persistent failure must not be silently
@@ -145,10 +142,7 @@ func (w *waitKqueue) close() {
})
}
var (
_ Tun = (*DarwinTun)(nil)
_ GVisorDevice = (*DarwinTun)(nil)
)
var _ Tun = (*DarwinTun)(nil)
func NewTun(options *Config) (Tun, error) {
// Check if fd is provided via environment (iOS mode)
@@ -274,73 +268,43 @@ func (t *DarwinTun) Index() (int, error) {
return iface.Index, nil
}
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
defer b.Release()
// prepare Darwin specific packet header
_, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0})
// copy the bytes of slices that compose the packet into the allocated buffer
for _, packetElement := range packet.AsSlices() {
_, _ = b.Write(packetElement)
}
// fill Darwin specific header from the first raw packet byte, that we can access now
// WritePacket implements Tun: the two parts are copied behind the header that the system wants in front of a packet
func (t *DarwinTun) WritePacket(header, payload []byte) error {
var family byte
switch b.Byte(4) >> 4 {
switch header[0] >> 4 {
case 4:
family = unix.AF_INET
case 6:
family = unix.AF_INET6
default:
return &tcpip.ErrAborted{}
return xerrors.New("not an ip packet")
}
b.SetByte(3, family)
if _, err := t.tunFile.Write(b.Bytes()); err != nil {
if errors.Is(err, unix.EAGAIN) {
return &tcpip.ErrWouldBlock{}
}
return &tcpip.ErrAborted{}
}
return nil
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(utunHeaderSize + len(header) + len(payload)))
defer b.Release()
b.Extend(utunHeaderSize)[3] = family
_, _ = b.Write(header)
_, _ = b.Write(payload)
_, err := t.tunFile.Write(b.Bytes())
return err
}
// ReadPacket implements GVisorDevice method to read one packet from the tun device
// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line,
// which will make the stack call Wait which should implement desired push-back
func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
// read the bytes to the interface file
n, err := b.ReadFrom(t.tunFile)
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
b.Release()
return 0, nil, ErrQueueEmpty
// ReadPacket implements Tun: it reads into b, the packet follows the header that the system puts in front of it
func (t *DarwinTun) ReadPacket(b []byte) ([]byte, error) {
for {
n, err := t.tunFile.Read(b)
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) || err == nil && n <= utunHeaderSize {
// nothing on the line, or an empty packet
t.Wait()
continue
}
if err != nil {
return nil, err
}
return b[utunHeaderSize:n], nil
}
if err != nil {
b.Release()
return 0, nil, err
}
// discard empty or sub-empty packets
if n <= utunHeaderSize {
b.Release()
return 0, nil, ErrQueueEmpty
}
// network protocol version from first byte of the raw packet, the one that follows Darwin specific header
version := b.Byte(utunHeaderSize) >> 4
packetBuffer := buffer.MakeWithData(b.BytesFrom(utunHeaderSize))
return version, stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: packetBuffer,
IsForwardedPacket: true,
OnRelease: func() {
b.Release()
},
}), nil
}
// Wait blocks until tunFd is readable (or a short timeout elapses), rather
@@ -349,8 +313,8 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// happens to race a call already parked here.
//
// Reviewer feedback (XTLS/Xray-core#6580, P2): the original version
// discarded every error from the underlying kevent syscall. dispatchLoop
// (stack_gvisor_endpoint.go) calls ReadPacket() then Wait() in an
// discarded every error from the underlying kevent syscall. ReadPacket()
// reads and then calls Wait() in an
// unconditional tight loop -- if kevent started failing at runtime for a
// persistent reason (not just a benign EINTR), Wait() returning
// immediately every time reintroduces exactly the busy-spin this whole
@@ -378,14 +342,10 @@ func (t *DarwinTun) Wait() {
// real Darwin systems, or having just failed permanently above)
// instead of always -- a genuine bounded sleep actually yields the CPU
// instead of being a near-instant scheduler hint that lets the tight
// dispatchLoop caller spin just as hot as before.
// loop of ReadPacket() spin just as hot as before.
time.Sleep(time.Millisecond)
}
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
}
// open the interface, by creating new utunN if in the system and returning its file descriptor
func open(name string) (*os.File, error) {
ifIndex := -1
+5 -2
View File
@@ -6,7 +6,6 @@ import (
"net"
"github.com/xtls/xray-core/common/errors"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
type DefaultTun struct{}
@@ -35,10 +34,14 @@ func (t *DefaultTun) Index() (int, error) {
return 0, errors.New("Tun is not supported on your platform")
}
func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) {
func (t *DefaultTun) ReadPacket([]byte) ([]byte, error) {
return nil, errors.New("Tun is not supported on your platform")
}
func (t *DefaultTun) WritePacket(header, payload []byte) error {
return errors.New("Tun is not supported on your platform")
}
func setinterface(string, string, uintptr, *net.Interface) error {
return errors.New("Tun is not supported on your platform")
}
+96
View File
@@ -0,0 +1,96 @@
//go:build linux
package tun
import (
"encoding/binary"
"os"
"sync"
"golang.org/x/sys/unix"
)
// vnetSize is the size of the virtio_net_hdr that a tun with IFF_VNET_HDR has in front of every packet,
// unless it was told another one
const vnetSize = 10
// fdTun reads and writes the packets of a tun that is a file descriptor
type fdTun struct {
fd int
// file is what packets are read from: a read waits in the poller of the runtime and ends when file is closed
file *os.File
// vnet is the size of the virtio_net_hdr in front of every packet, zero if the tun has none.
// With it the system does not split what it sends to the tun and takes what is not split from it
vnet int
writeLock sync.RWMutex
closed bool
}
// newFdTun takes a file descriptor that does not block
func newFdTun(fd, vnet int) *fdTun {
return &fdTun{fd: fd, file: os.NewFile(uintptr(fd), "/dev/net/tun"), vnet: vnet}
}
func (t *fdTun) ReadPacket(b []byte) ([]byte, error) {
n, err := t.file.Read(b)
// what the virtio_net_hdr says is of no use here: no checksum is looked at, and a segment may have any size
return b[min(t.vnet, n):n], err
}
func (t *fdTun) WritePacket(header, payload []byte) error {
// a virtio_net_hdr of zeros leaves nothing to the system
var vnet [packetRoom]byte
return t.writev(vnet[:t.vnet], header, payload)
}
// Segments implements segmenter
func (t *fdTun) Segments() bool {
return t.vnet != 0
}
// WriteSegment implements segmenter
func (t *fdTun) WriteSegment(header []byte, payload [][]byte, mss int) error {
// the virtio_net_hdr and the headers go as one part
var b [packetRoom + ipv6HeaderSize + 60]byte
ip, gso := ipv4HeaderSize, byte(unix.VIRTIO_NET_HDR_GSO_TCPV4)
if header[0]>>4 == 6 {
ip, gso = ipv6HeaderSize, unix.VIRTIO_NET_HDR_GSO_TCPV6
}
size := 0
for _, p := range payload {
size += len(p)
}
b[0] = unix.VIRTIO_NET_HDR_F_NEEDS_CSUM
if size > mss {
b[1] = gso
binary.NativeEndian.PutUint16(b[4:], uint16(mss))
}
binary.NativeEndian.PutUint16(b[2:], uint16(len(header)))
// where the checksum starts, and where it is put from there
binary.NativeEndian.PutUint16(b[6:], uint16(ip))
binary.NativeEndian.PutUint16(b[8:], 16)
var parts [8][]byte
return t.writev(append(append(parts[:0], b[:t.vnet+copy(b[t.vnet:], header)]), payload...)...)
}
func (t *fdTun) writev(parts ...[]byte) error {
t.writeLock.RLock()
defer t.writeLock.RUnlock()
if t.closed {
return os.ErrClosed
}
for {
_, err := unix.Writev(t.fd, parts)
if err != unix.EINTR {
return err
}
}
}
func (t *fdTun) closeFile() error {
t.writeLock.Lock()
defer t.writeLock.Unlock()
t.closed = true
return t.file.Close()
}
+249 -80
View File
@@ -5,17 +5,17 @@ package tun
import (
"context"
"errors"
"io/fs"
"net"
"net/netip"
"os"
"os/exec"
"slices"
"strings"
"sync"
"unsafe"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
@@ -41,8 +41,17 @@ func procyield(cycles uint32)
type FreeBSDTun struct {
device tun.Device
options *Config
name string
tunIndex int
autoInterface bool
// local is the address the system itself has on the TUN; gateway is the
// address before it, which nothing owns, so a packet to it is routed into
// the TUN (see selectFreeBSDGateway).
gateway netip.Prefix
local netip.Addr
systemDNSSet bool
systemDNSDirty bool
systemRoutes []netip.Prefix
escapeMu sync.Mutex
@@ -59,10 +68,7 @@ type escapeRoute struct {
gateway netip.Addr
}
var (
_ Tun = (*FreeBSDTun)(nil)
_ GVisorDevice = (*FreeBSDTun)(nil)
)
var _ Tun = (*FreeBSDTun)(nil)
// NewTun builds new tun interface handler
func NewTun(options *Config) (Tun, error) {
@@ -108,15 +114,19 @@ func NewTun(options *Config) (Tun, error) {
return &FreeBSDTun{
device: tunDev,
options: options,
name: name,
tunIndex: iface.Index,
autoInterface: options.AutoOutboundsInterface != "",
gateway: gateway,
local: local,
}, nil
}
// selectFreeBSDGateway picks the first IPv4 prefix from the configured gateway
// list and the local address derived from it (the darwin semantics: the
// gateway is the remote side of the point-to-point pair, the local address is
// the next one after it), falling back to the same link-local default.
// interface gets the address right after the gateway, and the gateway, which
// nothing owns, is routed into the TUN), falling back to the same link-local
// default.
func selectFreeBSDGateway(configured []string) (netip.Prefix, netip.Addr, error) {
gateway := netip.MustParsePrefix(defaultFreeBSDGateway)
if len(configured) > 0 {
@@ -205,6 +215,7 @@ func (t *FreeBSDTun) Close() error {
_ = t.routeMonitor.Close()
}
})
t.unsetSystemDNS()
t.unsetEscapeFib()
routeErr := t.unsetSystemRoutes()
name, nameErr := t.Name()
@@ -218,6 +229,195 @@ func (t *FreeBSDTun) Close() error {
return xerrors.Combine(routeErr, closeErr)
}
// resolvconfRunner runs resolvconf(8) with the given stdin. Overridable for
// tests.
var resolvconfRunner = func(stdin string, args ...string) ([]byte, error) {
cmd := exec.Command("resolvconf", args...)
if stdin != "" {
cmd.Stdin = strings.NewReader(stdin)
}
return cmd.CombinedOutput()
}
// readResolverFile reads one of the files the applied check looks at.
// Overridable for tests.
var readResolverFile = os.ReadFile
// resolvconfApplied reports whether the nameserver resolvconf was handed
// reached a resolver. resolvconf exits 0 with resolvconf=NO in
// resolvconf.conf, and still records the entry for the interface, but runs no
// subscriber, so the exit status and `resolvconf -l` prove nothing; what the
// subscribers wrote does. The libc subscriber writes /etc/resolv.conf, where
// the nameserver also has to be the exclusive one. With
// local_unbound, local-unbound-setup turns that subscriber off and names
// unbound's forwarders file as unbound_conf instead, with resolv.conf left
// pointed at loopback, so that file is checked next.
func resolvconfApplied(addr netip.Addr) (bool, error) {
resolvConf, err := readResolverFile("/etc/resolv.conf")
if err != nil {
return false, err
}
if exclusiveNameserver(resolvConf, addr) {
return true, nil
}
resolvconfConf, err := readResolverFile("/etc/resolvconf.conf")
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
if path := shellAssignment(resolvconfConf, "unbound_conf"); path != "" {
forwarders, err := readResolverFile(path)
if err != nil {
return false, err
}
if hasDirective(forwarders, "forward-addr:", addr) {
return true, nil
}
}
return false, nil
}
// exclusiveNameserver reports whether resolv.conf lists addr as a nameserver
// and no other one except on loopback. With inclusive_interfaces in
// resolvconf.conf the exclusive marking of -x is ignored and the nameservers
// DHCP supplied stay listed, and the system falls back to them: that is the
// leak the takeover exists to close, so it does not count as applied. A
// loopback nameserver alongside is a local resolver that resolvconf feeds.
func exclusiveNameserver(resolvConf []byte, addr netip.Addr) bool {
found := false
for _, line := range strings.Split(string(resolvConf), "\n") {
fields := strings.Fields(line)
if len(fields) != 2 || fields[0] != "nameserver" {
continue
}
if fields[1] == addr.String() {
found = true
continue
}
if other, err := netip.ParseAddr(fields[1]); err != nil || !other.IsLoopback() {
return false
}
}
return found
}
// hasDirective reports whether a line of content is the keyword followed by
// the address, as in `forward-addr: 10.0.0.1`.
func hasDirective(content []byte, keyword string, addr netip.Addr) bool {
for _, line := range strings.Split(string(content), "\n") {
fields := strings.Fields(line)
if len(fields) == 2 && fields[0] == keyword && fields[1] == addr.String() {
return true
}
}
return false
}
// shellAssignment returns the value of `name=value` in a sh-style config such
// as resolvconf.conf, without surrounding quotes, or empty when it is not set.
func shellAssignment(content []byte, name string) string {
for _, line := range strings.Split(string(content), "\n") {
line = strings.TrimSpace(line)
value, ok := strings.CutPrefix(line, name+"=")
if !ok {
continue
}
return strings.Trim(value, `"'`)
}
return ""
}
// ConfigureSystemDNS hands the TUN's gateway address to resolvconf(8) as the
// exclusive nameserver, so name lookups resolve through Xray instead of
// leaking to the physical link.
//
// It acts only when the config opts in, and it verifies the data path first:
// unless a query to the gateway would actually be handled, host-wide
// resolution is left to the OS and an error returned. The caller does not
// start the TUN on an error, as the system DNS would bypass it.
func (t *FreeBSDTun) ConfigureSystemDNS(ctx context.Context, inboundTag string) error {
if !t.options.AutoSystemDnsToGateway {
return nil
}
if t.systemDNSSet {
return nil
}
// A previous removal may have failed. Retry before applying anything, so a
// stale entry does not silently outlive the attempt to clean it up.
if t.systemDNSDirty {
if err := t.revertSystemDNS(); err != nil {
return xerrors.New("previous system DNS removal still failing").Base(err)
}
}
// The resolver is pointed at the gateway: on FreeBSD the system has no
// address there, so a query to it is routed into the TUN and answered
// inside Xray. A query from this host carries the local address.
address := t.gateway.Addr()
if err := verifyDNSRouting(ctx, inboundTag, t.local.String(), address.String()); err != nil {
return xerrors.New("no DNS path at ", address.String(), ":53").Base(err)
}
// -x makes this interface's nameserver the only one in use, the
// equivalent of resolvectl's `domain ~.` plus `default-route true`:
// without it openresolv keeps the nameservers dhclient supplied listed
// alongside the gateway, and the system falls back to them.
if _, err := resolvconfRunner("nameserver "+address.String()+"\n", "-a", t.name, "-x"); err != nil {
return xerrors.New("resolvconf -a failed").Base(err)
}
applied, err := resolvconfApplied(address)
if err != nil {
return t.rollbackSystemDNS(xerrors.New("cannot confirm the system DNS").Base(err))
}
if !applied {
return t.rollbackSystemDNS(xerrors.New("resolvconf did not apply ", address.String(), " to any resolver (resolvconf=NO or inclusive_interfaces in resolvconf.conf?)"))
}
t.systemDNSSet = true
xerrors.LogInfo(ctx, "[tun] system DNS set to ", address.String(), " on ", t.name)
return nil
}
// rollbackSystemDNS undoes a takeover that did not take. A failed removal is
// recorded so the next attempt retries it, and is reported rather than
// swallowed.
func (t *FreeBSDTun) rollbackSystemDNS(cause error) error {
if _, err := resolvconfRunner("", "-d", t.name); err != nil {
t.systemDNSDirty = true
// Combine, because Base overwrites: reporting only the cause would hide
// the removal failure, and reporting only the removal failure would
// hide why the removal was attempted.
return xerrors.New("resolvconf -d failed, the entry may remain").Base(xerrors.Combine(err, cause))
}
return cause
}
// revertSystemDNS issues the removal and keeps the dirty flag in step with the
// outcome.
func (t *FreeBSDTun) revertSystemDNS() error {
_, err := resolvconfRunner("", "-d", t.name)
t.systemDNSDirty = err != nil
if err != nil {
return err
}
t.systemDNSSet = false
return nil
}
// unsetSystemDNS hands DNS back to the OS. Only meaningful when
// ConfigureSystemDNS applied something, or a previous removal failed.
func (t *FreeBSDTun) unsetSystemDNS() {
if !t.systemDNSSet && !t.systemDNSDirty {
return
}
if err := t.revertSystemDNS(); err != nil {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to remove the system DNS entry; run `resolvconf -d ", t.name, "` to clean up")
}
}
func destroyInterface(name string) {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
@@ -242,73 +442,43 @@ func (t *FreeBSDTun) Index() (int, error) {
return t.tunIndex, nil
}
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
defer b.Release()
// prepare Unix specific packet header
_, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0})
// copy the bytes of slices that compose the packet into the allocated buffer
for _, packetElement := range packet.AsSlices() {
_, _ = b.Write(packetElement)
}
// fill Unix specific header from the first raw packet byte, that we can access now
// WritePacket implements Tun: the two parts are copied behind the header that the system wants in front of a packet
func (t *FreeBSDTun) WritePacket(header, payload []byte) error {
var family byte
switch b.Byte(4) >> 4 {
switch header[0] >> 4 {
case 4:
family = unix.AF_INET
case 6:
family = unix.AF_INET6
default:
return &tcpip.ErrAborted{}
return xerrors.New("not an ip packet")
}
b.SetByte(3, family)
if _, err := t.device.File().Write(b.Bytes()); err != nil {
if errors.Is(err, unix.EAGAIN) {
return &tcpip.ErrWouldBlock{}
}
return &tcpip.ErrAborted{}
}
return nil
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(tunHeaderSize + len(header) + len(payload)))
defer b.Release()
b.Extend(tunHeaderSize)[3] = family
_, _ = b.Write(header)
_, _ = b.Write(payload)
_, err := t.device.File().Write(b.Bytes())
return err
}
// ReadPacket implements GVisorDevice method to read one packet from the tun device
// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line,
// which will make the stack call Wait which should implement desired push-back
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
// read the bytes to the interface file
n, err := b.ReadFrom(t.device.File())
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
b.Release()
return 0, nil, ErrQueueEmpty
// ReadPacket implements Tun: it reads into b, the packet follows the header that the system puts in front of it
func (t *FreeBSDTun) ReadPacket(b []byte) ([]byte, error) {
for {
n, err := t.device.File().Read(b)
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) || err == nil && n <= tunHeaderSize {
// nothing on the line, or an empty packet
t.Wait()
continue
}
if err != nil {
return nil, err
}
return b[tunHeaderSize:n], nil
}
if err != nil {
b.Release()
return 0, nil, err
}
// discard empty or sub-empty packets
if n <= tunHeaderSize {
b.Release()
return 0, nil, ErrQueueEmpty
}
// network protocol version from first byte of the raw packet, the one that follows Unix specific header
version := b.Byte(tunHeaderSize) >> 4
packetBuffer := buffer.MakeWithData(b.BytesFrom(tunHeaderSize))
return version, stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: packetBuffer,
IsForwardedPacket: true,
OnRelease: func() {
b.Release()
},
}), nil
}
// Wait some cpu cycles
@@ -316,17 +486,14 @@ func (t *FreeBSDTun) Wait() {
procyield(1)
}
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
}
const (
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
)
// ifAliasReq4 is struct in_aliasreq from netinet/in_var.h in the 64-byte
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs).
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs; the
// dstaddr slot is the broadcast address on a broadcast interface).
type ifAliasReq4 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet4
@@ -361,7 +528,7 @@ type addrLifetime6 struct {
// header macro does.
const siocaifaddrIn6 = 0x80000000 | (uintptr(unsafe.Sizeof(ifAliasReq6{})) << 16) | ('i' << 8) | 27
// setIPAddress assigns the local/remote point-to-point IPv4 pair and a
// setIPAddress assigns the local IPv4 address with the gateway's mask and a
// link-local IPv6 address to the interface, required for the routing to work
// (same scheme as the darwin implementation: local address is the one right
// after the gateway address).
@@ -374,17 +541,20 @@ func setIPAddress(name string, gateway netip.Prefix, local netip.Addr, ifIndex i
local4 := local.As4()
// The interface runs in broadcast mode (the wireguard tun library puts it
// there), so the second address slot of the request is the broadcast
// address, not a point-to-point peer. It stays empty and the kernel
// derives the broadcast address from the mask; the gateway is then an
// ordinary on-link address, reachable through the interface route, which
// is what the system routes and the system DNS takeover need. Filling it
// with the gateway made the gateway the broadcast address, and a plain
// socket cannot send to that.
ifReq4 := ifAliasReq4{
Addr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: local4,
},
Dstaddr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: gateway.Addr().As4(),
},
Mask: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
@@ -471,10 +641,9 @@ func (t *FreeBSDTun) setSystemRoutes() error {
return err
}
// Route through the interface, not a gateway: the tun(4) device is a
// broadcast interface here, so its point-to-point peer address doubles as
// the subnet broadcast and the kernel refuses to route to it (EACCES).
// Interface routes sidestep the gateway entirely (what wg-quick does on
// FreeBSD).
// broadcast interface here with no address resolution, so a next hop adds
// nothing. Interface routes sidestep the gateway entirely (what wg-quick
// does on FreeBSD).
for _, destination := range routes {
if err := execRoute(-1, unix.RTM_ADD, t.tunIndex, destination, netip.Addr{}); err != nil {
_ = t.unsetSystemRoutes()
+390
View File
@@ -0,0 +1,390 @@
//go:build freebsd
package tun
import (
"context"
"errors"
"io/fs"
"net/netip"
"strings"
"testing"
)
type probeCall struct {
inboundTag string
source string
address string
}
// stubDNSRouting replaces the routing probe for the duration of a test and
// records how it was called, so tests can assert the probe is representative.
func stubDNSRouting(t *testing.T, err error) *[]probeCall {
t.Helper()
original := verifyDNSRouting
calls := []probeCall{}
verifyDNSRouting = func(_ context.Context, inboundTag, source, address string) error {
calls = append(calls, probeCall{inboundTag, source, address})
return err
}
t.Cleanup(func() { verifyDNSRouting = original })
return &calls
}
// recorder installs a resolvconf stub for the duration of a test and returns
// the captured invocations, each as the arguments followed by the stdin in
// angle brackets when there was one. An empty failOn succeeds every call;
// otherwise the named subcommand fails.
func recorder(t *testing.T, failOn string) *[]string {
t.Helper()
original := resolvconfRunner
calls := []string{}
resolvconfRunner = func(stdin string, args ...string) ([]byte, error) {
call := strings.Join(args, " ")
if stdin != "" {
call += " <" + stdin + ">"
}
calls = append(calls, call)
if failOn != "" && len(args) > 0 && args[0] == failOn {
return nil, errors.New("boom")
}
return nil, nil
}
t.Cleanup(func() { resolvconfRunner = original })
return &calls
}
// files replaces the resolver files read by the applied check for the
// duration of a test. A path not in the map does not exist.
func files(t *testing.T, contents map[string]string) {
t.Helper()
original := readResolverFile
readResolverFile = func(path string) ([]byte, error) {
if data, ok := contents[path]; ok {
return []byte(data), nil
}
return nil, fs.ErrNotExist
}
t.Cleanup(func() { readResolverFile = original })
}
// takenOver is what the libc subscriber writes once the TUN's nameserver is
// the exclusive one.
const takenOver = "# Generated by resolvconf\nnameserver 192.168.100.1\n"
// untouched is the DHCP-supplied resolv.conf that resolvconf=NO leaves in
// place.
const untouched = "# Generated by resolvconf\nnameserver 10.0.0.1\n"
func optedInTun() *FreeBSDTun {
return &FreeBSDTun{
options: &Config{
Name: "tun7",
Gateway: []string{"192.168.100.1/30"},
AutoSystemDnsToGateway: true,
},
name: "tun7",
gateway: netip.MustParsePrefix("192.168.100.1/30"),
local: netip.MustParseAddr("192.168.100.2"),
}
}
func joined(calls []string) string {
return strings.Join(calls, " | ")
}
func TestConfigureSystemDNSDisabledByDefault(t *testing.T) {
probes := stubDNSRouting(t, nil)
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
t1.options.AutoSystemDnsToGateway = false
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(*calls) != 0 || len(*probes) != 0 {
t.Errorf("nothing may run when the option is off, got %q and %d probes", joined(*calls), len(*probes))
}
if t1.systemDNSSet {
t.Error("systemDNSSet must stay false when the option is off")
}
}
func TestConfigureSystemDNSLeavesOSDNSWhenNoRoute(t *testing.T) {
stubDNSRouting(t, errors.New("no route"))
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
t.Fatal("expected an error when the probe fails")
}
if len(*calls) != 0 {
t.Errorf("resolvconf must not run when the probe fails, got %q", joined(*calls))
}
if t1.systemDNSSet {
t.Error("systemDNSSet must stay false when the probe fails")
}
}
// On FreeBSD the addresses are the other way round from Linux: the local
// address is the one after the gateway, and the gateway itself has no owner,
// so a packet to it is routed into the TUN. The probe has to carry that pair,
// and the resolver has to be pointed at the gateway.
func TestConfigureSystemDNSProbeCarriesFreeBSDPair(t *testing.T) {
probes := stubDNSRouting(t, nil)
recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(*probes) != 1 {
t.Fatalf("expected one probe, got %d", len(*probes))
}
want := probeCall{"tun", "192.168.100.2", "192.168.100.1"}
if (*probes)[0] != want {
t.Errorf("probe = %+v, want %+v", (*probes)[0], want)
}
}
func TestConfigureSystemDNSAppliesResolvconf(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !t1.systemDNSSet {
t.Fatal("systemDNSSet should be true after a successful takeover")
}
want := "-a tun7 -x <nameserver 192.168.100.1\n>"
if got := joined(*calls); got != want {
t.Errorf("resolvconf calls = %q, want %q", got, want)
}
}
// resolvconf exits 0 with resolvconf=NO in resolvconf.conf, and still records
// the entry for the interface, but runs no subscriber, so success has to be
// confirmed in what the subscribers wrote.
func TestConfigureSystemDNSRefusesWhenResolvconfDidNotApply(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": untouched})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
t.Fatal("expected an error when the nameserver did not reach a resolver")
}
if t1.systemDNSSet {
t.Error("systemDNSSet must stay false when the takeover did not take")
}
if !strings.HasSuffix(joined(*calls), "-d tun7") {
t.Errorf("expected a removal after the failed check, got %q", joined(*calls))
}
}
func TestConfigureSystemDNSIdempotent(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
before := len(*calls)
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(*calls) != before {
t.Errorf("second call must be a no-op, calls went %d -> %d", before, len(*calls))
}
}
func TestConfigureSystemDNSAddFailureIsNotDirty(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "-a")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
t.Fatal("expected an error when resolvconf -a fails")
}
if t1.systemDNSSet || t1.systemDNSDirty {
t.Error("a failed add leaves nothing to clean up")
}
if len(*calls) != 1 {
t.Errorf("nothing else may run after a failed add, got %q", joined(*calls))
}
}
// If the removal after a failed check fails too, the entry may still be
// installed, so the state has to be remembered rather than silently dropped.
func TestConfigureSystemDNSRollbackFailureKeepsDirty(t *testing.T) {
stubDNSRouting(t, nil)
recorder(t, "-d")
files(t, map[string]string{"/etc/resolv.conf": untouched})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
t.Fatal("expected an error when the check fails")
}
if !t1.systemDNSDirty {
t.Error("a failed removal must leave the resolver marked dirty")
}
if t1.systemDNSSet {
t.Error("systemDNSSet must stay false when the takeover did not complete")
}
}
// A dirty resolver is retried before anything new is applied.
func TestConfigureSystemDNSRetriesDirtyBeforeApplying(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
t1.systemDNSDirty = true
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := joined(*calls)
if !strings.HasPrefix(got, "-d tun7 | -a tun7") {
t.Errorf("expected the stale removal first, got %q", got)
}
if t1.systemDNSDirty {
t.Error("a successful retry should clear the dirty flag")
}
if !t1.systemDNSSet {
t.Error("the takeover should proceed once the retry succeeds")
}
}
func TestUnsetSystemDNSRemoves(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
t.Fatalf("setup failed: %v", err)
}
*calls = nil
t1.unsetSystemDNS()
if t1.systemDNSSet {
t.Error("systemDNSSet should be false after unset")
}
if got := joined(*calls); got != "-d tun7" {
t.Errorf("unset calls = %q, want %q", got, "-d tun7")
}
t1.unsetSystemDNS()
if len(*calls) != 1 {
t.Errorf("unsetSystemDNS must be idempotent, got %q", joined(*calls))
}
}
func TestUnsetSystemDNSKeepsDirtyWhenRemoveFails(t *testing.T) {
stubDNSRouting(t, nil)
calls := recorder(t, "-d")
files(t, map[string]string{"/etc/resolv.conf": takenOver})
t1 := optedInTun()
t1.systemDNSSet = true
t1.unsetSystemDNS()
if !t1.systemDNSDirty {
t.Error("a failed removal during unset must be remembered")
}
if got := joined(*calls); got != "-d tun7" {
t.Errorf("expected a removal attempt, got %q", got)
}
}
func TestResolvconfAppliedLibc(t *testing.T) {
files(t, map[string]string{"/etc/resolv.conf": takenOver})
if ok, err := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); err != nil || !ok {
t.Errorf("resolv.conf listing the nameserver must count as applied, got %v, %v", ok, err)
}
}
func TestResolvconfAppliedNotWhenUntouched(t *testing.T) {
files(t, map[string]string{"/etc/resolv.conf": untouched})
if ok, err := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); err != nil || ok {
t.Errorf("an unchanged resolv.conf and no resolvconf.conf must not count as applied, got %v, %v", ok, err)
}
}
// With inclusive_interfaces in resolvconf.conf the exclusive marking is
// ignored and the DHCP nameservers stay listed, so the system falls back to
// them; that is the leak the option exists to close, and it must not pass.
func TestResolvconfAppliedNotWhenOthersRemain(t *testing.T) {
files(t, map[string]string{"/etc/resolv.conf": "nameserver 192.168.100.1\nnameserver 10.0.0.1\n"})
if ok, _ := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); ok {
t.Error("another nameserver alongside the TUN's must not count as applied")
}
}
// A loopback nameserver alongside is a local resolver that resolvconf feeds
// (local_unbound with the libc subscriber left on), not a leak.
func TestResolvconfAppliedWithLoopbackAlongside(t *testing.T) {
files(t, map[string]string{"/etc/resolv.conf": "nameserver 127.0.0.1\nnameserver 192.168.100.1\n"})
if ok, err := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); err != nil || !ok {
t.Errorf("a loopback nameserver alongside must still count as applied, got %v, %v", ok, err)
}
}
func TestResolvconfAppliedIgnoresPrefixMatches(t *testing.T) {
files(t, map[string]string{"/etc/resolv.conf": "nameserver 192.168.100.10\n"})
if ok, _ := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); ok {
t.Error("192.168.100.10 must not match 192.168.100.1")
}
}
// local-unbound-setup turns the libc subscriber off and has resolvconf write
// unbound's forwarders instead; resolv.conf then stays pointed at loopback.
func TestResolvconfAppliedUnbound(t *testing.T) {
files(t, map[string]string{
"/etc/resolv.conf": "# Generated by resolvconf\nnameserver 127.0.0.1\noptions edns0\n",
"/etc/resolvconf.conf": "libc=\"NO\"\nunbound_conf=\"/var/unbound/forward.conf\"\n",
"/var/unbound/forward.conf": "forward-zone:\n\tname: .\n\tforward-addr: 192.168.100.1\n",
})
if ok, err := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); err != nil || !ok {
t.Errorf("the nameserver in unbound's forwarders must count as applied, got %v, %v", ok, err)
}
}
func TestResolvconfAppliedNotWhenUnboundUntouched(t *testing.T) {
files(t, map[string]string{
"/etc/resolv.conf": "nameserver 127.0.0.1\n",
"/etc/resolvconf.conf": "libc=\"NO\"\nunbound_conf=\"/var/unbound/forward.conf\"\n",
"/var/unbound/forward.conf": "forward-zone:\n\tname: .\n\tforward-addr: 10.0.0.1\n",
})
if ok, _ := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); ok {
t.Error("unbound still forwarding to the old server must not count as applied")
}
}
func TestResolvconfAppliedReportsUnreadableResolvConf(t *testing.T) {
files(t, map[string]string{})
if _, err := resolvconfApplied(netip.MustParseAddr("192.168.100.1")); err == nil {
t.Error("a missing resolv.conf is an error, not a verdict")
}
}
+39 -118
View File
@@ -11,29 +11,16 @@ import (
"sync"
"github.com/vishvananda/netlink"
appdns "github.com/xtls/xray-core/app/dns"
"github.com/xtls/xray-core/common/errors"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
feature_dns "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/dns/localdns"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/routing"
routingsession "github.com/xtls/xray-core/features/routing/session"
"github.com/xtls/xray-core/proxy/dns"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
// LinuxTun is an object that handles tun network interface on linux
// current version is heavily stripped to do nothing more,
// then create a network interface, to be provided as file descriptor to gVisor ip stack
// then create a network interface, whose packets are read and written through its file descriptor
type LinuxTun struct {
tunFd int
*fdTun
tunLink netlink.Link
options *Config
ownsTun bool
@@ -108,81 +95,6 @@ func (t *LinuxTun) ifaceName() string {
return attrs.Name
}
// probeSourcePort is a representative client port for the routing probe. A real
// query arrives from an ephemeral port that cannot be known in advance, so this
// only matters for a rule that matches on a source port.
const probeSourcePort = 49152
// verifyDNSRouting reports whether a DNS query to address would actually be
// handled. Redirecting the system resolver at an address nothing answers would
// break name resolution outright, so the takeover only proceeds when routing
// hands such a query to a DNS-capable outbound.
//
// Overridable for tests.
var verifyDNSRouting = func(ctx context.Context, inboundTag, source, address string) error {
ip, err := netip.ParseAddr(address)
if err != nil {
return errors.New("invalid DNS address ", address).Base(err)
}
src, err := netip.ParseAddr(source)
if err != nil || src.Is4() != ip.Is4() {
return errors.New("invalid source address ", source).Base(err)
}
instance := core.MustFromContext(ctx)
// Any resolution path that could still reach the system resolver has to be
// refused, because pointing the system resolver at the TUN would close a
// loop through the DNS outbound. With no `dns` section Core installs such a
// client; with a `dns` section that has no name servers app/dns falls back
// to one; and a name server pointed at "localhost" is one even when
// independent upstreams are configured alongside it, because name servers
// are selected per domain.
switch dnsFeature := instance.GetFeature(feature_dns.ClientType()).(type) {
case *localdns.Client:
return errors.New("DNS feature is the system resolver, takeover would loop")
case *appdns.DNS:
if dnsFeature.MayUseSystemResolver() {
return errors.New("DNS configuration may resolve through the system resolver, takeover would loop")
}
}
router, ok := instance.GetFeature(routing.RouterType()).(routing.Router)
if !ok {
return errors.New("router feature unavailable")
}
// A real query from this interface carries a source address, and rules may
// match on it, so the probe has to carry one too.
queryCtx := session.ContextWithInbound(ctx, &session.Inbound{
Name: "tun",
Tag: inboundTag,
Source: xnet.UDPDestination(xnet.IPAddress(src.AsSlice()), probeSourcePort),
})
queryCtx = session.ContextWithOutbounds(queryCtx, []*session.Outbound{{
Target: xnet.UDPDestination(xnet.IPAddress(ip.AsSlice()), 53),
}})
route, err := router.PickRoute(routingsession.AsRoutingContext(queryCtx))
if err != nil {
return errors.New("no route for ", address, ":53").Base(err)
}
manager, ok := instance.GetFeature(outbound.ManagerType()).(outbound.Manager)
if !ok {
return errors.New("outbound manager unavailable")
}
handler := manager.GetHandler(route.GetOutboundTag())
if handler == nil {
return errors.New("outbound ", route.GetOutboundTag(), " does not exist")
}
if settings := handler.ProxySettings(); settings == nil || settings.Type != serial.GetMessageType(&dns.Config{}) {
return errors.New("outbound ", route.GetOutboundTag(), " does not handle DNS")
}
return nil
}
// ConfigureSystemDNS points systemd-resolved at this interface so name lookups
// resolve through Xray instead of leaking to the physical link.
//
@@ -287,19 +199,30 @@ var _ Tun = (*LinuxTun)(nil)
// NewTun builds new tun interface handler (linux specific)
func NewTun(options *Config) (Tun, error) {
tunFd, tunLink, fdProvided, err := openFromEnv(options.Name)
tunFd, vnet, tunLink, fdProvided, err := openFromEnv(options.Name)
if err != nil {
return nil, err
}
if fdProvided {
return &LinuxTun{
tunFd: tunFd,
fdTun: newFdTun(tunFd, vnet),
tunLink: tunLink,
options: options,
}, nil
}
tunFd, err = open(options.Name)
// with a virtio_net_hdr on every packet the system leaves its TCP segments as large as they are,
// with their checksums not completed, and takes such segments. Without it where that is refused
vnet = vnetSize
tunFd, err = open(options.Name, unix.IFF_VNET_HDR)
if err == nil && unix.IoctlSetInt(tunFd, unix.TUNSETOFFLOAD, unix.TUN_F_CSUM|unix.TUN_F_TSO4|unix.TUN_F_TSO6) != nil {
_ = unix.Close(tunFd)
err = unix.EINVAL
}
if err != nil {
vnet = 0
tunFd, err = open(options.Name, 0)
}
if err != nil {
return nil, err
}
@@ -311,7 +234,7 @@ func NewTun(options *Config) (Tun, error) {
}
linuxTun := &LinuxTun{
tunFd: tunFd,
fdTun: newFdTun(tunFd, vnet),
tunLink: tunLink,
options: options,
ownsTun: true,
@@ -320,55 +243,62 @@ func NewTun(options *Config) (Tun, error) {
return linuxTun, nil
}
func openFromEnv(expectedName string) (int, netlink.Link, bool, error) {
// openFromEnv takes the tun that was given to the process, with the size of the virtio_net_hdr of its packets
func openFromEnv(expectedName string) (int, int, netlink.Link, bool, error) {
fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" })
if fdStr == "" {
return -1, nil, false, nil
return -1, 0, nil, false, nil
}
fd, err := strconv.Atoi(fdStr)
if err != nil {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey).Base(err)
return -1, 0, nil, true, errors.New("invalid ", platform.TunFdKey).Base(err)
}
if fd < 3 {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor must be >= 3")
return -1, 0, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor must be >= 3")
}
ifr, err := unix.NewIfreq("")
if err != nil {
return -1, nil, true, err
return -1, 0, nil, true, err
}
if err = unix.IoctlIfreq(fd, unix.TUNGETIFF, ifr); err != nil {
return -1, nil, true, err
return -1, 0, nil, true, err
}
flags := ifr.Uint16()
if flags&unix.IFF_TUN == 0 {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor is not a TUN device")
return -1, 0, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor is not a TUN device")
}
if flags&unix.IFF_NO_PI == 0 {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device must use IFF_NO_PI")
return -1, 0, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device must use IFF_NO_PI")
}
vnet := 0
if flags&unix.IFF_VNET_HDR != 0 {
if vnet, err = unix.IoctlGetInt(fd, unix.TUNGETVNETHDRSZ); err != nil || vnet < vnetSize || vnet > packetRoom {
return -1, 0, nil, true, errors.New("invalid ", platform.TunFdKey, ": virtio_net_hdr of ", vnet, " bytes").Base(err)
}
}
actualName := ifr.Name()
if expectedName != "" && actualName != expectedName {
return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device name ", actualName, " does not match configured name ", expectedName)
return -1, 0, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device name ", actualName, " does not match configured name ", expectedName)
}
tunLink, err := netlink.LinkByName(actualName)
if err != nil {
return -1, nil, true, err
return -1, 0, nil, true, err
}
if err = unix.SetNonblock(fd, true); err != nil {
return -1, nil, true, err
return -1, 0, nil, true, err
}
return fd, tunLink, true, nil
return fd, vnet, tunLink, true, nil
}
// open the file that implements tun interface in the OS
func open(name string) (int, error) {
func open(name string, features int) (int, error) {
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
if err != nil {
return -1, err
@@ -380,7 +310,7 @@ func open(name string) (int, error) {
return 0, err
}
flags := unix.IFF_TUN | unix.IFF_NO_PI
flags := unix.IFF_TUN | unix.IFF_NO_PI | features
ifr.SetUint16(uint16(flags))
err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr)
if err != nil {
@@ -457,7 +387,7 @@ func (t *LinuxTun) Close() error {
if t.ownsTun {
_ = netlink.LinkSetDown(t.tunLink)
}
_ = unix.Close(t.tunFd)
_ = t.closeFile()
return nil
}
@@ -470,15 +400,6 @@ func (t *LinuxTun) Index() (int, error) {
return t.tunLink.Attrs().Index, nil
}
// newEndpoint builds new gVisor stack.LinkEndpoint from the tun interface file descriptor
func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) {
return fdbased.New(&fdbased.Options{
FDs: []int{t.tunFd},
MTU: t.options.MTU,
RXChecksumOffload: true,
})
}
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.BindToDevice(int(fd), iface.Name)
}
+96
View File
@@ -0,0 +1,96 @@
//go:build (linux && !android) || freebsd
package tun
import (
"context"
"net/netip"
appdns "github.com/xtls/xray-core/app/dns"
"github.com/xtls/xray-core/common/errors"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
feature_dns "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/dns/localdns"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/routing"
routingsession "github.com/xtls/xray-core/features/routing/session"
"github.com/xtls/xray-core/proxy/dns"
)
// probeSourcePort is a representative client port for the routing probe. A real
// query arrives from an ephemeral port that cannot be known in advance, so this
// only matters for a rule that matches on a source port.
const probeSourcePort = 49152
// verifyDNSRouting reports whether a DNS query to address would actually be
// handled. Redirecting the system resolver at an address nothing answers would
// break name resolution outright, so the takeover only proceeds when routing
// hands such a query to a DNS-capable outbound.
//
// Overridable for tests.
var verifyDNSRouting = func(ctx context.Context, inboundTag, source, address string) error {
ip, err := netip.ParseAddr(address)
if err != nil {
return errors.New("invalid DNS address ", address).Base(err)
}
src, err := netip.ParseAddr(source)
if err != nil || src.Is4() != ip.Is4() {
return errors.New("invalid source address ", source).Base(err)
}
instance := core.MustFromContext(ctx)
// Any resolution path that could still reach the system resolver has to be
// refused, because pointing the system resolver at the TUN would close a
// loop through the DNS outbound. With no `dns` section Core installs such a
// client; with a `dns` section that has no name servers app/dns falls back
// to one; and a name server pointed at "localhost" is one even when
// independent upstreams are configured alongside it, because name servers
// are selected per domain.
switch dnsFeature := instance.GetFeature(feature_dns.ClientType()).(type) {
case *localdns.Client:
return errors.New("DNS feature is the system resolver, takeover would loop")
case *appdns.DNS:
if dnsFeature.MayUseSystemResolver() {
return errors.New("DNS configuration may resolve through the system resolver, takeover would loop")
}
}
router, ok := instance.GetFeature(routing.RouterType()).(routing.Router)
if !ok {
return errors.New("router feature unavailable")
}
// A real query from this interface carries a source address, and rules may
// match on it, so the probe has to carry one too.
queryCtx := session.ContextWithInbound(ctx, &session.Inbound{
Name: "tun",
Tag: inboundTag,
Source: xnet.UDPDestination(xnet.IPAddress(src.AsSlice()), probeSourcePort),
})
queryCtx = session.ContextWithOutbounds(queryCtx, []*session.Outbound{{
Target: xnet.UDPDestination(xnet.IPAddress(ip.AsSlice()), 53),
}})
route, err := router.PickRoute(routingsession.AsRoutingContext(queryCtx))
if err != nil {
return errors.New("no route for ", address, ":53").Base(err)
}
manager, ok := instance.GetFeature(outbound.ManagerType()).(outbound.Manager)
if !ok {
return errors.New("outbound manager unavailable")
}
handler := manager.GetHandler(route.GetOutboundTag())
if handler == nil {
return errors.New("outbound ", route.GetOutboundTag(), " does not exist")
}
if settings := handler.ProxySettings(); settings == nil || settings.Type != serial.GetMessageType(&dns.Config{}) {
return errors.New("outbound ", route.GetOutboundTag(), " does not handle DNS")
}
return nil
}
+30 -50
View File
@@ -25,9 +25,6 @@ import (
"golang.org/x/sys/windows"
"golang.zx2c4.com/wintun"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
//go:linkname procyield runtime.procyield
@@ -35,7 +32,7 @@ func procyield(cycles uint32)
// WindowsTun is an object that handles tun network interface on Windows
// current version is heavily stripped to do nothing more,
// then create a network interface, to be provided as endpoint to gVisor ip stack
// then create a network interface, whose packets are read and written through a Wintun session
type WindowsTun struct {
sync.RWMutex
@@ -57,9 +54,6 @@ type WindowsTun struct {
// WindowsTun implements Tun
var _ Tun = (*WindowsTun)(nil)
// WindowsTun implements GVisorDevice
var _ GVisorDevice = (*WindowsTun)(nil)
// NewTun creates a Wintun interface with the given name. Should a Wintun
// interface with the same name exist, it tried to be reused.
func NewTun(options *Config) (Tun, error) {
@@ -368,6 +362,8 @@ func (t *WindowsTun) Close() error {
flushDNSCache()
}
if t.session != (wintun.Session{}) {
// ReadPacket may be waiting for a packet
_ = windows.SetEvent(t.readWait)
t.session.End()
}
if t.adapter != nil {
@@ -507,62 +503,46 @@ func (t *WindowsTun) Index() (int, error) {
return int(row.InterfaceIndex), nil
}
// WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *WindowsTun) WritePacket(packetBuffer *stack.PacketBuffer) tcpip.Error {
// WritePacket implements Tun: the two parts are copied into the ring of Wintun
func (t *WindowsTun) WritePacket(header, payload []byte) error {
t.RLock()
defer t.RUnlock()
if t.closed {
return &tcpip.ErrClosedForSend{}
return windows.ERROR_HANDLE_EOF
}
// request buffer from Wintun
packet, err := t.session.AllocateSendPacket(packetBuffer.Size())
packet, err := t.session.AllocateSendPacket(len(header) + len(payload))
if err != nil {
return &tcpip.ErrAborted{}
return err
}
// copy the bytes of slices that compose the packet into the allocated buffer
var index int
for _, packetElement := range packetBuffer.AsSlices() {
index += copy(packet[index:], packetElement)
}
// signal Wintun to send that buffer as the packet
copy(packet[copy(packet, header):], payload)
t.session.SendPacket(packet)
return nil
}
// ReadPacket implements GVisorDevice method to read one packet from the tun device
// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line,
// which will make the stack call Wait which should implement desired push-back
func (t *WindowsTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
packet, err := t.session.ReceivePacket()
if go_errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
return 0, nil, ErrQueueEmpty
}
if err != nil {
return 0, nil, err
}
version := packet[0] >> 4
packetBuffer := buffer.MakeWithView(buffer.NewViewWithData(packet))
return version, stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: packetBuffer,
IsForwardedPacket: true,
OnRelease: func() {
// ReadPacket implements Tun: it waits until Wintun has a packet and copies it out of the ring
func (t *WindowsTun) ReadPacket(b []byte) ([]byte, error) {
for {
t.RLock()
if t.closed {
t.RUnlock()
return nil, windows.ERROR_HANDLE_EOF
}
packet, err := t.session.ReceivePacket()
if err == nil {
n := copy(b, packet)
t.session.ReleaseReceivePacket(packet)
},
}), nil
}
func (t *WindowsTun) Wait() {
procyield(1)
_, _ = windows.WaitForSingleObject(t.readWait, windows.INFINITE)
}
func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
t.RUnlock()
return b[:n], nil
}
t.RUnlock()
if !go_errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
return nil, err
}
procyield(1)
_, _ = windows.WaitForSingleObject(t.readWait, windows.INFINITE)
}
}
const (
+89 -46
View File
@@ -11,11 +11,6 @@ import (
"github.com/xtls/xray-core/common/net"
)
type packet struct {
data []byte
dest *net.Destination
}
// sub-handler specifically for udp connections under main handler
type udpConnectionHandler struct {
sync.RWMutex
@@ -38,17 +33,15 @@ func newUdpConnectionHandler(handleConnection func(conn net.Conn, dest net.Desti
// HandlePacket handles UDP packets coming from tun, to forward to the dispatcher
// this custom handler support FullCone NAT of returning packets, binding connection only by the source addr:port
func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data []byte) {
func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data *buf.Buffer) {
data.UDP = &dst
u.RLock()
conn, found := u.udpConns[src]
if found {
select {
case conn.egress <- &packet{
data: data,
dest: &dst,
}:
default:
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full")
if !conn.queue.push(data) {
errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full")
data.Release()
}
u.RUnlock()
return
@@ -60,21 +53,16 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
conn, found = u.udpConns[src]
if !found {
egress := make(chan *packet, 1024)
conn = &udpConn{handler: u, egress: egress, src: src, dst: dst}
conn = &udpConn{handler: u, queue: newPacketQueue(udpQueueLimit), src: src, dst: dst}
u.udpConns[src] = conn
go u.handleConnection(conn, dst)
}
// send packet data to the egress channel, if it has buffer, or discard
select {
case conn.egress <- &packet{
data: data,
dest: &dst,
}:
default:
errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full 2")
// queue the packet for the reader of the connection, or discard it
if !conn.queue.push(data) {
errors.LogDebug(context.Background(), "drop udp with size ", data.Len(), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full 2")
data.Release()
}
}
@@ -83,7 +71,7 @@ func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
// Close runs twice per flow; a newer conn may already own this src.
if u.udpConns[conn.src] == conn {
delete(u.udpConns, conn.src)
close(conn.egress)
conn.queue.close()
}
u.Unlock()
}
@@ -92,38 +80,28 @@ func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
type udpConn struct {
handler *udpConnectionHandler
egress chan *packet
src net.Destination
dst net.Destination
queue *packetQueue
src net.Destination
dst net.Destination
}
func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
for {
e, ok := <-c.egress
if !ok {
return nil, io.EOF
}
b := buf.New()
if _, err := b.Write(e.data); err != nil {
errors.LogErrorInner(context.Background(), err, "drop packet to ", e.dest, " with size ", len(e.data))
b.Release()
continue
}
b.UDP = e.dest
return buf.MultiBuffer{b}, nil
b, ok := c.queue.pop()
if !ok {
return nil, io.EOF
}
return buf.MultiBuffer{b}, nil
}
// Read packets from the connection
func (c *udpConn) Read(p []byte) (int, error) {
e, ok := <-c.egress
b, ok := c.queue.pop()
if !ok {
return 0, io.EOF
}
n := copy(p, e.data)
if n != len(e.data) {
defer b.Release()
n := copy(p, b.Bytes())
if n != int(b.Len()) {
return 0, io.ErrShortBuffer
}
return n, nil
@@ -154,7 +132,7 @@ func (c *udpConn) Write(p []byte) (int, error) {
// sending packets back mean sending payload with source/destination reversed
err := c.handler.writePacket(p, c.dst, c.src)
if err != nil {
return 0, nil
return 0, err
}
return len(p), nil
@@ -185,3 +163,68 @@ func (c *udpConn) SetReadDeadline(t time.Time) error {
func (c *udpConn) SetWriteDeadline(t time.Time) error {
return nil
}
// udpQueueLimit bounds the packets waiting for one UDP flow; more are dropped.
const udpQueueLimit = 1024
// packetQueue holds the packets waiting for one UDP flow. Unlike a buffered
// channel of the same bound it only allocates for packets actually queued, so
// the many idle flows kept until the idle timeout cost next to nothing.
type packetQueue struct {
mu sync.Mutex
items []*buf.Buffer
limit int
notify chan struct{}
closed bool
}
func newPacketQueue(limit int) *packetQueue {
return &packetQueue{limit: limit, notify: make(chan struct{}, 1)}
}
// push queues b and reports whether it was accepted.
func (q *packetQueue) push(b *buf.Buffer) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed || len(q.items) >= q.limit {
return false
}
q.items = append(q.items, b)
select {
case q.notify <- struct{}{}:
default:
}
return true
}
// pop blocks until a packet is queued or the queue is closed and drained.
func (q *packetQueue) pop() (*buf.Buffer, bool) {
for {
q.mu.Lock()
if len(q.items) > 0 {
b := q.items[0]
q.items[0] = nil
q.items = q.items[1:]
if len(q.items) == 0 {
q.items = nil
}
q.mu.Unlock()
return b, true
}
if q.closed {
q.mu.Unlock()
return nil, false
}
q.mu.Unlock()
<-q.notify
}
}
func (q *packetQueue) close() {
q.mu.Lock()
defer q.mu.Unlock()
if !q.closed {
q.closed = true
close(q.notify)
}
}

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