mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-12 01:08:13 +03:00
Lua: Support inline scripts with optional gzip compression
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -75,6 +75,5 @@ message Config {
|
||||
|
||||
bool enableParallelQuery = 14;
|
||||
|
||||
// Absolute path to the Lua DNS query script.
|
||||
string script = 15;
|
||||
}
|
||||
|
||||
+10
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user