Meow
2026-10-12 16:58:20 +00:00
committed by GitHub
parent 2bea2df6a1
commit 5a64db009b
27 changed files with 698 additions and 153 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;
}
+2 -2
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)]
+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) {
+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" {