diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index 053721239..f12e060b0 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -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() { diff --git a/app/dns/config.proto b/app/dns/config.proto index ca85582fe..4ab634030 100644 --- a/app/dns/config.proto +++ b/app/dns/config.proto @@ -75,6 +75,5 @@ message Config { bool enableParallelQuery = 14; - // Absolute path to the Lua DNS query script. string script = 15; } diff --git a/app/dns/dns.go b/app/dns/dns.go index 9943da09d..e452bda5e 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -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 } diff --git a/app/dns/lua_benchmark_test.go b/app/dns/lua_benchmark_test.go index ac4f6b8c8..245492fd3 100644 --- a/app/dns/lua_benchmark_test.go +++ b/app/dns/lua_benchmark_test.go @@ -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 diff --git a/app/dns/script.go b/app/dns/script.go index 012d44f7a..c465d175a 100644 --- a/app/dns/script.go +++ b/app/dns/script.go @@ -15,16 +15,20 @@ 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, func(L *lua.LState) { geodata.RegisterLua(L) @@ -38,15 +42,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) { diff --git a/app/dns/script_test.go b/app/dns/script_test.go index cea0bb1c7..506a79b13 100644 --- a/app/dns/script_test.go +++ b/app/dns/script_test.go @@ -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) diff --git a/app/router/config.pb.go b/app/router/config.pb.go index 2eea46d5f..d3c596311 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -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() { diff --git a/app/router/config.proto b/app/router/config.proto index 09b64b0a3..01532375a 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -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; } diff --git a/app/router/router.go b/app/router/router.go index 97ffba13c..cd695d8ea 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -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 } diff --git a/app/router/script.go b/app/router/script.go index b4a720b56..2893e0624 100644 --- a/app/router/script.go +++ b/app/router/script.go @@ -16,16 +16,20 @@ 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, func(L *lua.LState) { geodata.RegisterLua(L) @@ -40,15 +44,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) { diff --git a/app/router/script_test.go b/app/router/script_test.go index 75f52db75..599ebbf7d 100644 --- a/app/router/script_test.go +++ b/app/router/script_test.go @@ -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 @@ -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" { diff --git a/common/lua/program.go b/common/lua/program.go index 38eb225fc..24a745f91 100644 --- a/common/lua/program.go +++ b/common/lua/program.go @@ -1,11 +1,17 @@ package lua import ( - "bufio" + "bytes" + "compress/gzip" "context" + "encoding/base64" + "io" "os" + "strings" "time" + "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" ) @@ -19,24 +25,54 @@ type Program struct { // 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]" + if s, ok := strings.CutPrefix(script, "base64+gzip:"); ok { + code, err = decode(s, true) + } else if s, ok := strings.CutPrefix(script, "base64:"); ok { + code, err = decode(s, false) + } else if name, err = platform.ResolveLuaFile(script); err == nil { + code, err = os.ReadFile(name) + } 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 } +func decode(s string, zip bool) ([]byte, error) { + code, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, errors.New("failed to decode base64").Base(err) + } + if !zip { + return code, nil + } + + reader, err := gzip.NewReader(bytes.NewReader(code)) + if err != nil { + return nil, errors.New("failed to decompress gzip").Base(err) + } + defer reader.Close() + code, err = io.ReadAll(reader) + if err != nil { + return nil, errors.New("failed to decompress gzip").Base(err) + } + return code, 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. diff --git a/common/lua/program_test.go b/common/lua/program_test.go index f4593f7e6..51675599b 100644 --- a/common/lua/program_test.go +++ b/common/lua/program_test.go @@ -1,10 +1,14 @@ package lua import ( + "bytes" + "compress/gzip" "context" + "encoding/base64" "errors" "os" "path/filepath" + "strings" "testing" glua "github.com/yuin/gopher-lua" @@ -15,10 +19,13 @@ func TestProgramStatesAreIndependent(t *testing.T) { if err := os.WriteFile(path, []byte("value = (value or 0) + 1"), 0o600); err != nil { t.Fatal(err) } - program, err := CompileFile(path) + program, err := Compile(path) if err != nil { t.Fatal(err) } + if program.proto.SourceName != path { + t.Fatalf("program source = %q, want %q", program.proto.SourceName, path) + } first, err := program.NewState(context.Background(), nil, nil) if err != nil { t.Fatal(err) @@ -40,7 +47,7 @@ func TestProgramInitializationObservesCancellation(t *testing.T) { if err := os.WriteFile(path, []byte("while true do end"), 0o600); err != nil { t.Fatal(err) } - program, err := CompileFile(path) + program, err := Compile(path) if err != nil { t.Fatal(err) } @@ -60,7 +67,7 @@ func TestNewStateClosesFailedValidation(t *testing.T) { if err := os.WriteFile(path, []byte("value = 1"), 0o600); err != nil { t.Fatal(err) } - program, err := CompileFile(path) + program, err := Compile(path) if err != nil { t.Fatal(err) } @@ -74,3 +81,115 @@ func TestNewStateClosesFailedValidation(t *testing.T) { t.Fatalf("state = %v, error = %v, checked state closed = %t", L, err, checked != nil && checked.IsClosed()) } } + +func compressSource(t *testing.T, code string) []byte { + t.Helper() + var data bytes.Buffer + writer := gzip.NewWriter(&data) + if _, err := writer.Write([]byte(code)); err != nil { + t.Fatal(err) + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + return data.Bytes() +} + +func TestCompileInline(t *testing.T) { + const code = `value = "你好"` + for _, tc := range []struct { + name, script string + value glua.LValue + }{ + {"base64", "base64:" + base64.StdEncoding.EncodeToString([]byte(code)), glua.LString("你好")}, + {"base64+gzip", "base64+gzip:" + base64.StdEncoding.EncodeToString(compressSource(t, code)), glua.LString("你好")}, + {"empty base64", "base64:", glua.LNil}, + } { + t.Run(tc.name, func(t *testing.T) { + program, err := Compile(tc.script) + if err != nil { + t.Fatal(err) + } + if program.proto.SourceName != "[inline]" { + t.Fatalf("program source = %q, want [inline]", program.proto.SourceName) + } + state, err := program.NewState(context.Background(), nil, nil) + if err != nil { + t.Fatal(err) + } + defer state.Close() + if got := state.GetGlobal("value"); got != tc.value { + t.Fatalf("value = %v, want %v", got, tc.value) + } + }) + } +} + +func TestCompileRejectsInvalidInline(t *testing.T) { + corrupt := compressSource(t, "value = 1") + corrupt[len(corrupt)-8] ^= 0xff + for _, tc := range []struct { + name, script string + wantErr error + wantMessage string + }{ + {"base64", "base64:%", base64.CorruptInputError(0), "failed to decode base64"}, + {"gzip base64", "base64+gzip:%", base64.CorruptInputError(0), "failed to decode base64"}, + {"gzip header", "base64+gzip:" + base64.StdEncoding.EncodeToString([]byte("value = 'uncompressed'")), gzip.ErrHeader, "failed to decompress gzip"}, + {"gzip checksum", "base64+gzip:" + base64.StdEncoding.EncodeToString(corrupt), gzip.ErrChecksum, "failed to decompress gzip"}, + } { + t.Run(tc.name, func(t *testing.T) { + program, err := Compile(tc.script) + if program != nil || !errors.Is(err, tc.wantErr) || !strings.Contains(err.Error(), tc.wantMessage) { + t.Fatalf("Compile = %v, %v; want nil program and %q error wrapping %v", program, err, tc.wantMessage, tc.wantErr) + } + }) + } +} + +func TestCompileInlineDiagnostics(t *testing.T) { + for _, tc := range []struct { + code string + compileError bool + }{ + {"function broken(", true}, + {`error("initialization failed")`, false}, + } { + program, err := Compile("base64:" + base64.StdEncoding.EncodeToString([]byte(tc.code))) + if tc.compileError { + if program != nil || err == nil { + t.Fatalf("Compile = %v, %v; want nil program and syntax error", program, err) + } + } else { + if err != nil { + t.Fatal(err) + } + state, initErr := program.NewState(context.Background(), nil, nil) + if state != nil { + state.Close() + t.Fatal("NewState returned a state after failed initialization") + } + err = initErr + } + if err == nil || !strings.Contains(err.Error(), "[inline]") { + t.Fatalf("Lua error = %v, want [inline] diagnostic", err) + } + } +} + +func TestCompileRejectsReadFailure(t *testing.T) { + path := filepath.Join(t.TempDir(), "unreadable.lua") + if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Chmod(path, 0); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.Chmod(path, 0o600) }) + if _, err := os.ReadFile(path); !errors.Is(err, os.ErrPermission) { + t.Skip("file read permissions are not enforced") + } + if program, err := Compile(path); program != nil || !errors.Is(err, os.ErrPermission) { + t.Fatalf("Compile = %v, %v; want nil program and permission error", program, err) + } +} diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 07b482742..4fb5d5b1e 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -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 { @@ -282,14 +281,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 { diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index 5657f0c70..e0065ccd3 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -2,8 +2,6 @@ package conf_test import ( "encoding/json" - "os" - "path/filepath" "testing" "github.com/google/go-cmp/cmp" @@ -30,6 +28,7 @@ func TestDNSConfigParsing(t *testing.T) { testCases := []TestCase{ { Input: `{ + "script": "base64:%", "servers": [{ "address": "8.8.8.8", "port": 5353, @@ -54,6 +53,7 @@ func TestDNSConfigParsing(t *testing.T) { }`, Parser: parserCreator(), Output: &dns.Config{ + Script: "base64:%", NameServer: []*dns.NameServer{ { Address: &net.Endpoint{ @@ -125,41 +125,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) diff --git a/infra/conf/router.go b/infra/conf/router.go index ba5cfeef2..825f7782e 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -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,7 @@ 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 - } + config.Script = c.Script config.DomainStrategy = c.getDomainStrategy() diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go index 04e4bcd83..4fa28bc44 100644 --- a/infra/conf/router_test.go +++ b/infra/conf/router_test.go @@ -2,8 +2,6 @@ package conf_test import ( "encoding/json" - "os" - "path/filepath" "testing" "time" _ "unsafe" @@ -31,6 +29,7 @@ func TestRouterConfig(t *testing.T) { runMultiTestCase(t, []TestCase{ { Input: `{ + "script": "missing.lua", "domainStrategy": "AsIs", "rules": [ { @@ -92,6 +91,7 @@ func TestRouterConfig(t *testing.T) { }`, Parser: createParser(), Output: &router.Config{ + Script: "missing.lua", DomainStrategy: router.Config_AsIs, BalancingRule: []*router.BalancingRule{ { @@ -238,39 +238,3 @@ 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 { - name string - script string - wantError bool - }{ - {"relative", "route.lua", false}, - {"absolute", path, false}, - {"missing", "missing.lua", true}, - {"directory", dir, true}, - } { - 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") - } - return - } - if err != nil { - t.Fatal(err) - } - if built.Script != path { - t.Fatalf("script path = %q, want %q", built.Script, path) - } - }) - } -}