mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-12 01:08:13 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab90334502 | ||
|
|
8d79265bce | ||
|
|
13396ff9c6 | ||
|
|
0fe8514f93 |
@@ -266,10 +266,9 @@ type Config struct {
|
|||||||
DisableFallback bool `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"`
|
DisableFallback bool `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"`
|
||||||
DisableFallbackIfMatch bool `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,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"`
|
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"`
|
||||||
Script string `protobuf:"bytes,15,opt,name=script,proto3" json:"script,omitempty"`
|
unknownFields protoimpl.UnknownFields
|
||||||
unknownFields protoimpl.UnknownFields
|
sizeCache protoimpl.SizeCache
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) Reset() {
|
func (x *Config) Reset() {
|
||||||
|
|||||||
@@ -75,6 +75,5 @@ message Config {
|
|||||||
|
|
||||||
bool enableParallelQuery = 14;
|
bool enableParallelQuery = 14;
|
||||||
|
|
||||||
// Absolute path to the Lua DNS query script.
|
|
||||||
string script = 15;
|
string script = 15;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -32,7 +32,6 @@ type DNS struct {
|
|||||||
matcherInfos []*DomainMatcherInfo
|
matcherInfos []*DomainMatcherInfo
|
||||||
checkSystem bool
|
checkSystem bool
|
||||||
script *scriptEngine
|
script *scriptEngine
|
||||||
scriptPath string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher.
|
// 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))
|
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{
|
return &DNS{
|
||||||
hosts: hosts,
|
hosts: hosts,
|
||||||
ipOption: &ipOption,
|
ipOption: &ipOption,
|
||||||
@@ -182,7 +188,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
|||||||
disableFallbackIfMatch: config.DisableFallbackIfMatch,
|
disableFallbackIfMatch: config.DisableFallbackIfMatch,
|
||||||
enableParallelQuery: config.EnableParallelQuery,
|
enableParallelQuery: config.EnableParallelQuery,
|
||||||
checkSystem: checkSystem,
|
checkSystem: checkSystem,
|
||||||
scriptPath: config.Script,
|
script: script,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,12 +199,10 @@ func (*DNS) Type() interface{} {
|
|||||||
|
|
||||||
// Start implements common.Runnable.
|
// Start implements common.Runnable.
|
||||||
func (s *DNS) Start() error {
|
func (s *DNS) Start() error {
|
||||||
if s.scriptPath != "" {
|
if s.script != nil {
|
||||||
engine, err := newScriptEngine(s.scriptPath, s)
|
if err := s.script.start(s); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.New("failed to initialize DNS script").Base(err)
|
return errors.New("failed to initialize DNS script").Base(err)
|
||||||
}
|
}
|
||||||
s.script = engine
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,13 @@ end
|
|||||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
engine, err := newScriptEngine(path, server)
|
engine, err := newScriptEngine(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := engine.start(server); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
b.Cleanup(engine.close)
|
b.Cleanup(engine.close)
|
||||||
for _, bench := range []struct {
|
for _, bench := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
+15
-8
@@ -15,16 +15,20 @@ import (
|
|||||||
const scriptExecutionTimeout = 6 * time.Second
|
const scriptExecutionTimeout = 6 * time.Second
|
||||||
|
|
||||||
type scriptEngine struct {
|
type scriptEngine struct {
|
||||||
pool *xlua.Pool
|
program *xlua.Program
|
||||||
|
pool *xlua.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScriptEngine(path string, server *DNS) (*scriptEngine, error) {
|
func newScriptEngine(script string) (*scriptEngine, error) {
|
||||||
program, err := xlua.CompileFile(path)
|
program, err := xlua.Compile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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,
|
scriptExecutionTimeout*20,
|
||||||
func(L *lua.LState) {
|
func(L *lua.LState) {
|
||||||
geodata.RegisterLua(L)
|
geodata.RegisterLua(L)
|
||||||
@@ -38,15 +42,18 @@ func newScriptEngine(path string, server *DNS) (*scriptEngine, error) {
|
|||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
errors.LogInfo(server.ctx, "DNS script initialized from ", path)
|
e.pool = pool
|
||||||
return &scriptEngine{pool: pool}, nil
|
errors.LogInfo(server.ctx, "DNS script initialized")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *scriptEngine) close() {
|
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) {
|
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(),
|
ctx: context.Background(),
|
||||||
clients: []*Client{{server: upstream, ipOption: &option, timeoutMs: time.Second}},
|
clients: []*Client{{server: upstream, ipOption: &option, timeoutMs: time.Second}},
|
||||||
}
|
}
|
||||||
engine, err := newScriptEngine(path, server)
|
engine, err := newScriptEngine(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := engine.start(server); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
defer engine.close()
|
defer engine.close()
|
||||||
|
|
||||||
ips, ttl, err := engine.query("good.example", option)
|
ips, ttl, err := engine.query("good.example", option)
|
||||||
@@ -164,11 +167,15 @@ end
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
engine, err := newScriptEngine(scriptPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
server := &DNS{
|
server := &DNS{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
hosts: hosts,
|
hosts: hosts,
|
||||||
ipOption: &option,
|
ipOption: &option,
|
||||||
scriptPath: scriptPath,
|
script: engine,
|
||||||
clients: []*Client{
|
clients: []*Client{
|
||||||
{id: "primary", server: primary, ipOption: &option, timeoutMs: 2 * time.Second},
|
{id: "primary", server: primary, ipOption: &option, timeoutMs: 2 * time.Second},
|
||||||
{id: "fallback", server: fallback, 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) {
|
func TestDNSScriptRejectsInvalidStartup(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name, script string
|
||||||
script string
|
compileError bool
|
||||||
}{
|
}{
|
||||||
{"syntax", "function HandleDNSQuery("},
|
{"syntax", "function HandleDNSQuery(", true},
|
||||||
{"missing hook", "value = 1"},
|
{"missing hook", "value = 1", false},
|
||||||
{"top-level error", `error("setup failed")`},
|
{"invalid hook", "HandleDNSQuery = true", false},
|
||||||
|
{"top-level error", `error("setup failed")`, false},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "script.lua")
|
path := filepath.Join(t.TempDir(), "script.lua")
|
||||||
if err := os.WriteFile(path, []byte(tc.script), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte(tc.script), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if err := server.Start(); err == nil {
|
||||||
t.Fatal("Start accepted an invalid DNS script")
|
t.Fatal("Start accepted an invalid DNS script")
|
||||||
}
|
}
|
||||||
if server.script != nil {
|
if server.script.pool != nil {
|
||||||
t.Fatal("Start retained a script engine after failure")
|
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) {
|
func TestDNSScriptFakeDNSOption(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "script.lua")
|
path := filepath.Join(t.TempDir(), "script.lua")
|
||||||
script := `
|
script := `
|
||||||
@@ -251,12 +296,16 @@ end
|
|||||||
answers: map[string]net.IP{"good.example": net.ParseIP("198.18.0.1")},
|
answers: map[string]net.IP{"good.example": net.ParseIP("198.18.0.1")},
|
||||||
ttl: 30,
|
ttl: 30,
|
||||||
}
|
}
|
||||||
|
engine, err := newScriptEngine(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
server := &DNS{
|
server := &DNS{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
hosts: hosts,
|
hosts: hosts,
|
||||||
ipOption: &option,
|
ipOption: &option,
|
||||||
scriptPath: path,
|
script: engine,
|
||||||
clients: []*Client{{id: "fake", server: upstream, ipOption: &option, timeoutMs: time.Second}},
|
clients: []*Client{{id: "fake", server: upstream, ipOption: &option, timeoutMs: time.Second}},
|
||||||
}
|
}
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
t.Fatal(err)
|
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"`
|
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"`
|
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"`
|
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"`
|
||||||
Script string `protobuf:"bytes,4,opt,name=script,proto3" json:"script,omitempty"`
|
unknownFields protoimpl.UnknownFields
|
||||||
unknownFields protoimpl.UnknownFields
|
sizeCache protoimpl.SizeCache
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) Reset() {
|
func (x *Config) Reset() {
|
||||||
|
|||||||
@@ -110,6 +110,5 @@ message Config {
|
|||||||
DomainStrategy domain_strategy = 1;
|
DomainStrategy domain_strategy = 1;
|
||||||
repeated RoutingRule rule = 2;
|
repeated RoutingRule rule = 2;
|
||||||
repeated BalancingRule balancing_rule = 3;
|
repeated BalancingRule balancing_rule = 3;
|
||||||
// Absolute path to the Lua routing script.
|
|
||||||
string script = 4;
|
string script = 4;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -30,9 +30,9 @@ func (r *Router) RegisterLua(L *lua.LState) {
|
|||||||
module.RawSetString("LocalOS", lua.LString(runtime.GOOS))
|
module.RawSetString("LocalOS", lua.LString(runtime.GOOS))
|
||||||
|
|
||||||
module.RawSetString("PickOutbound", L.NewFunction(func(L *lua.LState) int {
|
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 {
|
if !ok {
|
||||||
L.ArgError(2, "balancer tag must be a string")
|
L.ArgError(1, "balancer tag must be a string")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
balancer, found := (*r.balancers.Load())[string(tag)]
|
balancer, found := (*r.balancers.Load())[string(tag)]
|
||||||
|
|||||||
+10
-6
@@ -20,7 +20,6 @@ import (
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
domainStrategy Config_DomainStrategy
|
domainStrategy Config_DomainStrategy
|
||||||
rules atomic.Pointer[[]*Rule]
|
rules atomic.Pointer[[]*Rule]
|
||||||
scriptPath string
|
|
||||||
script *scriptEngine
|
script *scriptEngine
|
||||||
balancers atomic.Pointer[map[string]*Balancer]
|
balancers atomic.Pointer[map[string]*Balancer]
|
||||||
dns dns.Client
|
dns dns.Client
|
||||||
@@ -42,12 +41,19 @@ type Route struct {
|
|||||||
// Init initializes the Router.
|
// Init initializes the Router.
|
||||||
func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) error {
|
func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) error {
|
||||||
r.domainStrategy = config.DomainStrategy
|
r.domainStrategy = config.DomainStrategy
|
||||||
r.scriptPath = config.Script
|
|
||||||
r.dns = d
|
r.dns = d
|
||||||
r.ctx = ctx
|
r.ctx = ctx
|
||||||
r.ohm = ohm
|
r.ohm = ohm
|
||||||
r.dispatcher = dispatcher
|
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.rules.Store(new([]*Rule))
|
||||||
r.balancers.Store(&map[string]*Balancer{})
|
r.balancers.Store(&map[string]*Balancer{})
|
||||||
return r.ReloadRules(config, false)
|
return r.ReloadRules(config, false)
|
||||||
@@ -228,12 +234,10 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
|
|||||||
|
|
||||||
// Start implements common.Runnable.
|
// Start implements common.Runnable.
|
||||||
func (r *Router) Start() error {
|
func (r *Router) Start() error {
|
||||||
if r.scriptPath != "" {
|
if r.script != nil {
|
||||||
engine, err := newScriptEngine(r.scriptPath, r)
|
if err := r.script.start(r); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.New("failed to initialize routing script").Base(err)
|
return errors.New("failed to initialize routing script").Base(err)
|
||||||
}
|
}
|
||||||
r.script = engine
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-8
@@ -16,16 +16,20 @@ import (
|
|||||||
const scriptExecutionTimeout = 6 * time.Second
|
const scriptExecutionTimeout = 6 * time.Second
|
||||||
|
|
||||||
type scriptEngine struct {
|
type scriptEngine struct {
|
||||||
pool *xlua.Pool
|
program *xlua.Program
|
||||||
|
pool *xlua.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScriptEngine(path string, router *Router) (*scriptEngine, error) {
|
func newScriptEngine(script string) (*scriptEngine, error) {
|
||||||
program, err := xlua.CompileFile(path)
|
program, err := xlua.Compile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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,
|
scriptExecutionTimeout*20,
|
||||||
func(L *lua.LState) {
|
func(L *lua.LState) {
|
||||||
geodata.RegisterLua(L)
|
geodata.RegisterLua(L)
|
||||||
@@ -40,15 +44,18 @@ func newScriptEngine(path string, router *Router) (*scriptEngine, error) {
|
|||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
errors.LogInfo(router.ctx, "routing script initialized from ", path)
|
e.pool = pool
|
||||||
return &scriptEngine{pool: pool}, nil
|
errors.LogInfo(router.ctx, "routing script initialized")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *scriptEngine) close() {
|
func (e *scriptEngine) close() {
|
||||||
e.pool.Close()
|
if e.pool != nil {
|
||||||
|
e.pool.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *scriptEngine) pickRoute(ctx routing.Context) (routing.Route, error) {
|
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) {
|
func TestRouterScriptStartup(t *testing.T) {
|
||||||
for _, tc := range []struct{ name, script string }{
|
for _, tc := range []struct {
|
||||||
{"syntax error", "function HandleRoute("},
|
name, script string
|
||||||
{"missing hook", "value = 1"},
|
compileError bool
|
||||||
{"initialization error", `error("setup failed")`},
|
}{
|
||||||
|
{"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) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
r := new(Router)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
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 {
|
if err := r.Start(); err == nil {
|
||||||
t.Fatal("Start accepted an invalid routing script")
|
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) {
|
func TestRouterScriptRouting(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name, body string
|
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: "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: "no match", body: `return nil`, wantErr: common.ErrNoClue, wantCalls: "2"},
|
||||||
{name: "empty tag", body: `return ""`, 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: "string error", body: `return nil, nil, "blocked"`, wantMessage: "blocked", wantCalls: "2"},
|
||||||
{name: "invalid tag", body: `return false`, wantMessage: "outboundTag", wantCalls: "2"},
|
{name: "invalid tag", body: `return false`, wantMessage: "outboundTag", wantCalls: "2"},
|
||||||
{name: "invalid rule", body: `return "lua-out", false`, wantMessage: "ruleTag", 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, `
|
r := startLuaRouter(t, `
|
||||||
local router = require("xray.router")
|
local router = require("xray.router")
|
||||||
function HandleRoute()
|
function HandleRoute()
|
||||||
local tag, err = router:PickOutbound("balance")
|
local tag, err = router.PickOutbound("balance")
|
||||||
return tag, "balanced", err
|
return tag, "balanced", err
|
||||||
end`, nil, config("old"))
|
end`, nil, config("old"))
|
||||||
pick := func(want string) {
|
pick := func(want string) {
|
||||||
@@ -236,7 +275,7 @@ func TestRouterScriptConcurrentBalancerReload(t *testing.T) {
|
|||||||
r := startLuaRouter(t, `
|
r := startLuaRouter(t, `
|
||||||
local router = require("xray.router")
|
local router = require("xray.router")
|
||||||
function HandleRoute()
|
function HandleRoute()
|
||||||
local tag, err = router:PickOutbound("balance")
|
local tag, err = router.PickOutbound("balance")
|
||||||
return tag, nil, err
|
return tag, nil, err
|
||||||
end`, nil, config("a"))
|
end`, nil, config("a"))
|
||||||
|
|
||||||
@@ -367,10 +406,10 @@ end`)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer instance.Close()
|
|
||||||
if err := instance.Start(); err != nil {
|
if err := instance.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer instance.Close()
|
||||||
r := instance.GetFeature(routing.RouterType()).(*Router)
|
r := instance.GetFeature(routing.RouterType()).(*Router)
|
||||||
route, err := r.PickRoute(newLuaRouteTestContext())
|
route, err := r.PickRoute(newLuaRouteTestContext())
|
||||||
if err != nil || route.GetOutboundTag() != "direct" || route.GetRuleTag() != "outer-route" {
|
if err != nil || route.GetOutboundTag() != "direct" || route.GetRuleTag() != "outer-route" {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode returns an inline script, optionally compressed with gzip at the highest level.
|
||||||
|
func Encode(code []byte, zip bool) (string, error) {
|
||||||
|
if !zip {
|
||||||
|
return "base64:" + base64.StdEncoding.EncodeToString(code), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data bytes.Buffer
|
||||||
|
writer, err := gzip.NewWriterLevel(&data, gzip.BestCompression)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("failed to compress gzip").Base(err)
|
||||||
|
}
|
||||||
|
if _, err := writer.Write(code); err != nil {
|
||||||
|
return "", errors.New("failed to compress gzip").Base(err)
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return "", errors.New("failed to compress gzip").Base(err)
|
||||||
|
}
|
||||||
|
return "base64+gzip:" + base64.StdEncoding.EncodeToString(data.Bytes()), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
glua "github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeRoundTrip(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
code []byte
|
||||||
|
}{
|
||||||
|
{"empty", nil},
|
||||||
|
{"padding", []byte("x")},
|
||||||
|
{"unicode", []byte(`value = "你好"`)},
|
||||||
|
{"multiline", []byte("-- comment\r\nvalue = 42\n")},
|
||||||
|
{"binary", []byte{0, 0xff, 0xfe, 1, 2}},
|
||||||
|
{"large", bytes.Repeat([]byte("-- repeated code\n"), 32768)},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
for _, zip := range []bool{false, true} {
|
||||||
|
prefix := "base64:"
|
||||||
|
if zip {
|
||||||
|
prefix = "base64+gzip:"
|
||||||
|
}
|
||||||
|
t.Run(prefix, func(t *testing.T) {
|
||||||
|
script, err := Encode(tc.code, zip)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
encoded, ok := strings.CutPrefix(script, prefix)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("encoded script does not start with %q", prefix)
|
||||||
|
}
|
||||||
|
code, err := decode(encoded, zip)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(code, tc.code) {
|
||||||
|
t.Fatal("decoded bytes differ from input")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeBestCompression(t *testing.T) {
|
||||||
|
script, err := Encode([]byte("value = 42"), true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(script, "base64+gzip:"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// The gzip XFL header byte is 2 for the highest compression level.
|
||||||
|
if len(data) < 10 || data[8] != 2 {
|
||||||
|
t.Fatal("gzip header does not indicate highest compression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeCompile(t *testing.T) {
|
||||||
|
for _, zip := range []bool{false, true} {
|
||||||
|
script, err := Encode([]byte(`value = "你好"`), zip)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
program, err := Compile(script)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
state, err := program.NewState(context.Background(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := state.GetGlobal("value"); got != glua.LString("你好") {
|
||||||
|
state.Close()
|
||||||
|
t.Fatalf("value = %v, want 你好", got)
|
||||||
|
}
|
||||||
|
state.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-7
@@ -1,11 +1,17 @@
|
|||||||
package lua
|
package lua
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/platform"
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
"github.com/yuin/gopher-lua/parse"
|
"github.com/yuin/gopher-lua/parse"
|
||||||
)
|
)
|
||||||
@@ -19,24 +25,54 @@ type Program struct {
|
|||||||
// Implementations must close partial states on failure; callers own successful states.
|
// Implementations must close partial states on failure; callers own successful states.
|
||||||
type LStateFactory func(context.Context) (*glua.LState, error)
|
type LStateFactory func(context.Context) (*glua.LState, error)
|
||||||
|
|
||||||
// CompileFile reads and compiles a Lua file once.
|
// Compile reads a Lua file or decodes an inline script and compiles it into an immutable program.
|
||||||
func CompileFile(path string) (*Program, error) {
|
func Compile(script string) (*Program, error) {
|
||||||
f, err := os.Open(path)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
proto, err := glua.Compile(chunk, path)
|
proto, err := glua.Compile(chunk, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Program{proto: proto}, nil
|
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
|
// NewState creates a state, runs register, executes the program under ctx, and
|
||||||
// runs validate. It removes the initialization context before returning a state
|
// runs validate. It removes the initialization context before returning a state
|
||||||
// owned by the caller.
|
// owned by the caller.
|
||||||
|
|||||||
+122
-3
@@ -1,10 +1,14 @@
|
|||||||
package lua
|
package lua
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
glua "github.com/yuin/gopher-lua"
|
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 {
|
if err := os.WriteFile(path, []byte("value = (value or 0) + 1"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
program, err := CompileFile(path)
|
program, err := Compile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
first, err := program.NewState(context.Background(), nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if err := os.WriteFile(path, []byte("while true do end"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
program, err := CompileFile(path)
|
program, err := Compile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -60,7 +67,7 @@ func TestNewStateClosesFailedValidation(t *testing.T) {
|
|||||||
if err := os.WriteFile(path, []byte("value = 1"), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte("value = 1"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
program, err := CompileFile(path)
|
program, err := Compile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-9
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/geodata"
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NameServerConfig struct {
|
type NameServerConfig struct {
|
||||||
@@ -273,6 +272,17 @@ func (m *HostsWrapper) Build() ([]*dns.Config_HostMapping, error) {
|
|||||||
|
|
||||||
// Build implements Buildable
|
// Build implements Buildable
|
||||||
func (c *DNSConfig) Build() (*dns.Config, error) {
|
func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||||
|
if c.Script != "" {
|
||||||
|
if c.DisableFallback || c.DisableFallbackIfMatch || c.EnableParallelQuery {
|
||||||
|
return nil, errors.New("DNS script cannot be used with disableFallback, disableFallbackIfMatch or enableParallelQuery")
|
||||||
|
}
|
||||||
|
for i, server := range c.Servers {
|
||||||
|
if len(server.Domains) > 0 || server.SkipFallback || server.FinalQuery {
|
||||||
|
return nil, errors.New("DNS script cannot be used with domains, skipFallback or finalQuery in server ", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config := &dns.Config{
|
config := &dns.Config{
|
||||||
Tag: c.Tag,
|
Tag: c.Tag,
|
||||||
DisableCache: c.DisableCache,
|
DisableCache: c.DisableCache,
|
||||||
@@ -282,14 +292,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
|||||||
DisableFallbackIfMatch: c.DisableFallbackIfMatch,
|
DisableFallbackIfMatch: c.DisableFallbackIfMatch,
|
||||||
EnableParallelQuery: c.EnableParallelQuery,
|
EnableParallelQuery: c.EnableParallelQuery,
|
||||||
QueryStrategy: resolveQueryStrategy(c.QueryStrategy),
|
QueryStrategy: resolveQueryStrategy(c.QueryStrategy),
|
||||||
}
|
Script: c.Script,
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ClientIP != nil {
|
if c.ClientIP != nil {
|
||||||
|
|||||||
+42
-37
@@ -2,8 +2,7 @@ package conf_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"strings"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
@@ -125,41 +124,7 @@ func TestDNSConfigParsing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDNSScriptConfig(t *testing.T) {
|
func TestDNSNameServerID(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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsed DNSConfig
|
var parsed DNSConfig
|
||||||
if err := json.Unmarshal([]byte(`{"servers":[{"id":"primary","address":"1.1.1.1"}]}`), &parsed); err != nil {
|
if err := json.Unmarshal([]byte(`{"servers":[{"id":"primary","address":"1.1.1.1"}]}`), &parsed); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -172,3 +137,43 @@ func TestDNSScriptConfig(t *testing.T) {
|
|||||||
t.Fatalf("nameserver IDs = %v, want primary", built.NameServer)
|
t.Fatalf("nameserver IDs = %v, want primary", built.NameServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDNSScriptConfig(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{name: "script only", input: `{"script":"base64:%"}`},
|
||||||
|
{name: "compatible fields", input: `{"script":"base64:%","servers":["1.1.1.1",{"id":"primary","address":"8.8.8.8","expectedIPs":["8.8.8.8"],"queryStrategy":"UseIPv4","disableCache":true}],"hosts":{"example.com":"127.0.0.1"},"disableCache":true}`},
|
||||||
|
{name: "disabled options", input: `{"script":"base64:%","disableFallback":false,"disableFallbackIfMatch":false,"enableParallelQuery":false,"servers":[{"address":"1.1.1.1","domains":[],"skipFallback":false,"finalQuery":false}]}`},
|
||||||
|
{name: "domains", input: `{"script":"base64:%","servers":[{"address":"1.1.1.1","domains":["domain:example.com"]}]}`, wantError: "domains"},
|
||||||
|
{name: "skip fallback", input: `{"script":"base64:%","servers":[{"address":"1.1.1.1","skipFallback":true}]}`, wantError: "skipFallback"},
|
||||||
|
{name: "final query", input: `{"script":"base64:%","servers":[{"address":"1.1.1.1","finalQuery":true}]}`, wantError: "finalQuery"},
|
||||||
|
{name: "later server", input: `{"script":"base64:%","servers":["1.1.1.1",{"address":"8.8.8.8","finalQuery":true}]}`, wantError: "server 1"},
|
||||||
|
{name: "disable fallback", input: `{"script":"base64:%","disableFallback":true}`, wantError: "disableFallback"},
|
||||||
|
{name: "disable fallback if match", input: `{"script":"base64:%","disableFallbackIfMatch":true}`, wantError: "disableFallbackIfMatch"},
|
||||||
|
{name: "parallel query", input: `{"script":"base64:%","enableParallelQuery":true}`, wantError: "enableParallelQuery"},
|
||||||
|
{name: "empty script", input: `{"script":"","disableFallback":true,"disableFallbackIfMatch":true,"enableParallelQuery":true,"servers":[{"address":"1.1.1.1","domains":["example.com"],"skipFallback":true,"finalQuery":true}]}`},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var config DNSConfig
|
||||||
|
if err := json.Unmarshal([]byte(test.input), &config); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
built, err := config.Build()
|
||||||
|
if test.wantError != "" {
|
||||||
|
if built != nil || err == nil || !strings.Contains(err.Error(), "DNS script cannot be used with ") || !strings.Contains(err.Error(), test.wantError) {
|
||||||
|
t.Fatalf("Build() = %v, %v; want nil config and script conflict error mentioning %s", built, err, test.wantError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if built.Script != config.Script || len(built.NameServer) != len(config.Servers) {
|
||||||
|
t.Fatalf("Build() did not preserve script or servers: %v", built)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/xtls/xray-core/app/router"
|
"github.com/xtls/xray-core/app/router"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/geodata"
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -95,13 +94,10 @@ func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
|
|||||||
func (c *RouterConfig) Build() (*router.Config, error) {
|
func (c *RouterConfig) Build() (*router.Config, error) {
|
||||||
config := new(router.Config)
|
config := new(router.Config)
|
||||||
|
|
||||||
if c.Script != "" {
|
if c.Script != "" && (c.DomainStrategy != nil || len(c.RuleList) > 0) {
|
||||||
path, err := platform.ResolveLuaFile(c.Script)
|
return nil, errors.New("routing script cannot be used with domainStrategy or rules")
|
||||||
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()
|
config.DomainStrategy = c.getDomainStrategy()
|
||||||
|
|
||||||
|
|||||||
+22
-22
@@ -2,8 +2,7 @@ package conf_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"strings"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
@@ -240,36 +239,37 @@ func TestRouterConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRouterScriptConfig(t *testing.T) {
|
func TestRouterScriptConfig(t *testing.T) {
|
||||||
dir := t.TempDir()
|
for _, test := range []struct {
|
||||||
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
|
name string
|
||||||
script string
|
input string
|
||||||
wantError bool
|
wantError bool
|
||||||
}{
|
}{
|
||||||
{"relative", "route.lua", false},
|
{name: "script only", input: `{"script":"missing.lua"}`},
|
||||||
{"absolute", path, false},
|
{name: "empty rules", input: `{"script":"missing.lua","rules":[]}`},
|
||||||
{"missing", "missing.lua", true},
|
{name: "balancers", input: `{"script":"missing.lua","balancers":[{"tag":"b1","selector":["direct"]}]}`},
|
||||||
{"directory", dir, true},
|
{name: "default domain strategy", input: `{"script":"missing.lua","domainStrategy":"AsIs"}`, wantError: true},
|
||||||
|
{name: "IP domain strategy", input: `{"script":"missing.lua","domainStrategy":"IPIfNonMatch"}`, wantError: true},
|
||||||
|
{name: "empty domain strategy", input: `{"script":"missing.lua","domainStrategy":""}`, wantError: true},
|
||||||
|
{name: "rules", input: `{"script":"missing.lua","rules":[{"outboundTag":"direct","domain":["example.com"]}]}`, wantError: true},
|
||||||
|
{name: "empty script", input: `{"script":"","domainStrategy":"IPOnDemand","rules":[{"outboundTag":"direct","domain":["example.com"]}]}`},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
built, err := (&RouterConfig{Script: tc.script}).Build()
|
var config RouterConfig
|
||||||
if tc.wantError {
|
if err := json.Unmarshal([]byte(test.input), &config); err != nil {
|
||||||
if err == nil {
|
t.Fatal(err)
|
||||||
t.Fatal("Build accepted invalid script path")
|
}
|
||||||
|
built, err := config.Build()
|
||||||
|
if test.wantError {
|
||||||
|
if built != nil || err == nil || !strings.Contains(err.Error(), "routing script cannot be used with domainStrategy or rules") {
|
||||||
|
t.Fatalf("Build() = %v, %v; want nil config and script conflict error", built, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if built.Script != path {
|
if built.Script != config.Script || len(built.Rule) != len(config.RuleList) || len(built.BalancingRule) != len(config.Balancers) {
|
||||||
t.Fatalf("script path = %q, want %q", built.Script, path)
|
t.Fatalf("Build() did not preserve script, rules or balancers: %v", built)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package all
|
|||||||
import (
|
import (
|
||||||
"github.com/xtls/xray-core/main/commands/all/api"
|
"github.com/xtls/xray-core/main/commands/all/api"
|
||||||
"github.com/xtls/xray-core/main/commands/all/convert"
|
"github.com/xtls/xray-core/main/commands/all/convert"
|
||||||
|
"github.com/xtls/xray-core/main/commands/all/lua"
|
||||||
"github.com/xtls/xray-core/main/commands/all/tls"
|
"github.com/xtls/xray-core/main/commands/all/tls"
|
||||||
"github.com/xtls/xray-core/main/commands/base"
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
)
|
)
|
||||||
@@ -12,6 +13,7 @@ func init() {
|
|||||||
base.RootCommand.Commands,
|
base.RootCommand.Commands,
|
||||||
api.CmdAPI,
|
api.CmdAPI,
|
||||||
convert.CmdConvert,
|
convert.CmdConvert,
|
||||||
|
lua.CmdLua,
|
||||||
tls.CmdTLS,
|
tls.CmdTLS,
|
||||||
cmdUUID,
|
cmdUUID,
|
||||||
cmdX25519,
|
cmdX25519,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
xlua "github.com/xtls/xray-core/common/lua"
|
||||||
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdEncode = &base.Command{
|
||||||
|
UsageLine: "{{.Exec}} lua encode [-gzip=false] [file | stdin:]",
|
||||||
|
Short: "Encode a Lua script for inline configuration",
|
||||||
|
Long: `
|
||||||
|
Encode one file or stdin: as an inline Lua script for the script configuration field.
|
||||||
|
Files are read from the given path without searching configuration directories.
|
||||||
|
The original bytes are preserved; the script is not executed or validated.
|
||||||
|
The encoded string is written to stdout on one line, without quotes.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
-gzip=false
|
||||||
|
Disable compression and output base64: instead of base64+gzip:.
|
||||||
|
By default, gzip uses the highest compression level.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
{{.Exec}} lua encode script.lua
|
||||||
|
{{.Exec}} lua encode -gzip=false script.lua
|
||||||
|
{{.Exec}} lua encode stdin:
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var gzipEnabled = cmdEncode.Flag.Bool("gzip", true, "Compress the script with gzip.")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdEncode.Run = executeEncode
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeEncode(cmd *base.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
cmd.Usage()
|
||||||
|
}
|
||||||
|
var code []byte
|
||||||
|
var err error
|
||||||
|
if args[0] == "stdin:" {
|
||||||
|
code, err = io.ReadAll(os.Stdin)
|
||||||
|
} else {
|
||||||
|
code, err = os.ReadFile(args[0])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to read script: %s", err)
|
||||||
|
}
|
||||||
|
script, err := xlua.Encode(code, *gzipEnabled)
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to encode script: %s", err)
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintln(os.Stdout, script); err != nil {
|
||||||
|
base.Fatalf("failed to write encoded script: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import "github.com/xtls/xray-core/main/commands/base"
|
||||||
|
|
||||||
|
// CmdLua holds all Lua subcommands.
|
||||||
|
var CmdLua = &base.Command{
|
||||||
|
UsageLine: "{{.Exec}} lua",
|
||||||
|
Short: "Lua tools",
|
||||||
|
Long: "{{.Exec}} {{.LongName}} provides tools for Lua scripts.",
|
||||||
|
Commands: []*base.Command{
|
||||||
|
cmdEncode,
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user