Lua: Support inline scripts with optional gzip compression

This commit is contained in:
Meo597
2026-10-12 03:58:48 +08:00
parent 0fe8514f93
commit 13396ff9c6
17 changed files with 344 additions and 165 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
}
+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
+15 -8
View File
@@ -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) {
+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)
+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;
}
+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
}
+15 -8
View File
@@ -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) {
+45 -6
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
@@ -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" {
+43 -7
View File
@@ -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.
+122 -3
View File
@@ -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)
}
}
+1 -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 {
@@ -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 {
+3 -37
View File
@@ -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)
+1 -8
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,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()
+2 -38
View File
@@ -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)
}
})
}
}