mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-19 07:44:26 +00:00
dns: reduce Lua allocations with flat arguments and returns and native IP slice userdata
This commit is contained in:
+39
-89
@@ -11,8 +11,7 @@ import (
|
|||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterLua makes xray.dns available to require in an LState. The caller
|
// RegisterLua makes xray.dns available to require in an LState.
|
||||||
// owns the state and registers modules before running the script top level.
|
|
||||||
func (s *DNS) RegisterLua(L *lua.LState) {
|
func (s *DNS) RegisterLua(L *lua.LState) {
|
||||||
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
||||||
servers := L.NewTable()
|
servers := L.NewTable()
|
||||||
@@ -22,16 +21,15 @@ func (s *DNS) RegisterLua(L *lua.LState) {
|
|||||||
server.RawSetString("id", lua.LString(client.id))
|
server.RawSetString("id", lua.LString(client.id))
|
||||||
|
|
||||||
server.RawSetString("query", L.NewFunction(func(L *lua.LState) int {
|
server.RawSetString("query", L.NewFunction(func(L *lua.LState) int {
|
||||||
q := L.CheckTable(2)
|
domain, ok := L.Get(2).(lua.LString)
|
||||||
domain, ok := q.RawGetString("domain").(lua.LString)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
L.RaiseError("server:query requires a domain")
|
L.RaiseError("server:query requires a domain")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
option := featureDNS.IPOption{
|
option := featureDNS.IPOption{
|
||||||
IPv4Enable: q.RawGetString("ipv4") == lua.LTrue,
|
IPv4Enable: L.CheckBool(3),
|
||||||
IPv6Enable: q.RawGetString("ipv6") == lua.LTrue,
|
IPv6Enable: L.CheckBool(4),
|
||||||
FakeEnable: q.RawGetString("fake") == lua.LTrue,
|
FakeEnable: L.CheckBool(5),
|
||||||
}
|
}
|
||||||
ctx := L.Context()
|
ctx := L.Context()
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
@@ -46,22 +44,18 @@ func (s *DNS) RegisterLua(L *lua.LState) {
|
|||||||
} else {
|
} else {
|
||||||
ips, ttl, err = client.QueryIP(ctx, string(domain), option)
|
ips, ttl, err = client.QueryIP(ctx, string(domain), option)
|
||||||
}
|
}
|
||||||
result := L.CreateTable(0, 3)
|
addresses := L.NewUserData()
|
||||||
addresses := L.CreateTable(len(ips), 0)
|
addresses.Value = ips
|
||||||
for j, ip := range ips {
|
L.Push(addresses)
|
||||||
address := L.NewUserData()
|
L.Push(lua.LNumber(ttl))
|
||||||
address.Value = ip
|
|
||||||
addresses.RawSetInt(j+1, address)
|
|
||||||
}
|
|
||||||
result.RawSetString("ips", addresses)
|
|
||||||
result.RawSetString("ttl", lua.LNumber(ttl))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ud := L.NewUserData()
|
ud := L.NewUserData()
|
||||||
ud.Value = err
|
ud.Value = err
|
||||||
result.RawSetString("error", ud)
|
L.Push(ud)
|
||||||
|
} else {
|
||||||
|
L.Push(lua.LNil)
|
||||||
}
|
}
|
||||||
L.Push(result)
|
return 3
|
||||||
return 1
|
|
||||||
}))
|
}))
|
||||||
servers.RawSetInt(i+1, server)
|
servers.RawSetInt(i+1, server)
|
||||||
}
|
}
|
||||||
@@ -72,15 +66,9 @@ func (s *DNS) RegisterLua(L *lua.LState) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallLuaHook invokes handleDNSQuery on a state owned by the caller. Domain and option
|
// CallLuaHook invokes handleDNSQuery in the supplied state.
|
||||||
// must already have passed DNS normalization, hosts, and address-family handling.
|
// Returned slices and IP bytes may share storage with DNS caches or matcher inputs.
|
||||||
// The caller serializes access to its state; ctx cancels Lua execution and upstream calls.
|
|
||||||
func (s *DNS) CallLuaHook(L *lua.LState, ctx context.Context, domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
func (s *DNS) CallLuaHook(L *lua.LState, ctx context.Context, domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||||
q := L.CreateTable(0, 4)
|
|
||||||
q.RawSetString("domain", lua.LString(strings.ToLower(domain)))
|
|
||||||
q.RawSetString("ipv4", lua.LBool(option.IPv4Enable))
|
|
||||||
q.RawSetString("ipv6", lua.LBool(option.IPv6Enable))
|
|
||||||
q.RawSetString("fake", lua.LBool(option.FakeEnable))
|
|
||||||
previous := L.Context()
|
previous := L.Context()
|
||||||
L.SetContext(ctx)
|
L.SetContext(ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -92,89 +80,51 @@ func (s *DNS) CallLuaHook(L *lua.LState, ctx context.Context, domain string, opt
|
|||||||
}()
|
}()
|
||||||
fn := L.GetGlobal("handleDNSQuery")
|
fn := L.GetGlobal("handleDNSQuery")
|
||||||
if fn.Type() != lua.LTFunction {
|
if fn.Type() != lua.LTFunction {
|
||||||
return nil, 0, errors.New("DNS script must define handleDNSQuery(q)")
|
return nil, 0, errors.New("DNS script must define handleDNSQuery(domain, ipv4, ipv6, fake)")
|
||||||
}
|
}
|
||||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 1, Protect: true}, q); err != nil {
|
if err := L.CallByParam(lua.P{Fn: fn, NRet: 3, Protect: true},
|
||||||
|
lua.LString(strings.ToLower(domain)), lua.LBool(option.IPv4Enable),
|
||||||
|
lua.LBool(option.IPv6Enable), lua.LBool(option.FakeEnable)); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
value := L.Get(-1)
|
addresses, ttlValue, errorValue := L.Get(-3), L.Get(-2), L.Get(-1)
|
||||||
L.Pop(1)
|
L.Pop(3)
|
||||||
ips, ttl, err := decodeLuaDNSResult(value, option)
|
ips, ttl, err := readLuaDNSResult(addresses, ttlValue, errorValue)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil, 0, ctx.Err()
|
return nil, 0, ctx.Err()
|
||||||
}
|
}
|
||||||
return ips, ttl, err
|
return ips, ttl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeLuaDNSResult(value lua.LValue, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
func readLuaDNSResult(addresses, ttlValue, errorValue lua.LValue) ([]net.IP, uint32, error) {
|
||||||
table, ok := value.(*lua.LTable)
|
if errorValue != lua.LNil {
|
||||||
if !ok {
|
if ud, ok := errorValue.(*lua.LUserData); ok {
|
||||||
return nil, 0, errors.New("DNS script result must be a table")
|
|
||||||
}
|
|
||||||
if v := table.RawGetString("error"); v != lua.LNil {
|
|
||||||
if ud, ok := v.(*lua.LUserData); ok {
|
|
||||||
if err, ok := ud.Value.(error); ok {
|
if err, ok := ud.Value.(error); ok {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s, ok := v.(lua.LString); ok {
|
if s, ok := errorValue.(lua.LString); ok {
|
||||||
return nil, 0, errors.New(string(s))
|
return nil, 0, errors.New(string(s))
|
||||||
}
|
}
|
||||||
return nil, 0, errors.New("DNS script error must be an error or string")
|
return nil, 0, errors.New("DNS script error must be an error or string")
|
||||||
}
|
}
|
||||||
ttlValue, ok := table.RawGetString("ttl").(lua.LNumber)
|
ttl, ok := ttlValue.(lua.LNumber)
|
||||||
if !ok || ttlValue < 0 || ttlValue > math.MaxUint32 || math.Trunc(float64(ttlValue)) != float64(ttlValue) {
|
if !ok || ttl < 0 || ttl > math.MaxUint32 || math.Trunc(float64(ttl)) != float64(ttl) {
|
||||||
return nil, 0, errors.New("DNS script returned invalid TTL")
|
return nil, 0, errors.New("DNS script returned invalid TTL")
|
||||||
}
|
}
|
||||||
var ips []net.IP
|
if addresses == lua.LNil {
|
||||||
switch addresses := table.RawGetString("ips").(type) {
|
return nil, 0, featureDNS.ErrEmptyResponse
|
||||||
case *lua.LTable:
|
}
|
||||||
ips = make([]net.IP, 0, addresses.Len())
|
ud, ok := addresses.(*lua.LUserData)
|
||||||
for i := 1; i <= addresses.Len(); i++ {
|
if !ok {
|
||||||
ip, err := decodeLuaIP(addresses.RawGetInt(i), i, option)
|
return nil, 0, errors.New("DNS script IPs must be native IP slice userdata")
|
||||||
if err != nil {
|
}
|
||||||
return nil, 0, err
|
ips, ok := ud.Value.([]net.IP)
|
||||||
}
|
if !ok {
|
||||||
ips = append(ips, ip)
|
return nil, 0, errors.New("DNS script IPs must be native IP slice userdata")
|
||||||
}
|
|
||||||
case *lua.LUserData:
|
|
||||||
addressesIP, ok := addresses.Value.([]net.IP)
|
|
||||||
if !ok {
|
|
||||||
return nil, 0, errors.New("DNS script result.ips must be an array")
|
|
||||||
}
|
|
||||||
ips = make([]net.IP, 0, len(addressesIP))
|
|
||||||
for i, ip := range addressesIP {
|
|
||||||
valid, err := validateLuaIP(ip, i+1, option)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
ips = append(ips, valid)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, 0, errors.New("DNS script result.ips must be an array")
|
|
||||||
}
|
}
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
return nil, 0, featureDNS.ErrEmptyResponse
|
return nil, 0, featureDNS.ErrEmptyResponse
|
||||||
}
|
}
|
||||||
return ips, uint32(ttlValue), nil
|
return ips, uint32(ttl), nil
|
||||||
}
|
|
||||||
|
|
||||||
func decodeLuaIP(value lua.LValue, index int, option featureDNS.IPOption) (net.IP, error) {
|
|
||||||
address, ok := value.(*lua.LUserData)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("DNS script returned invalid address at index ", index)
|
|
||||||
}
|
|
||||||
ip, ok := address.Value.(net.IP)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("DNS script returned invalid address at index ", index)
|
|
||||||
}
|
|
||||||
return validateLuaIP(ip, index, option)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateLuaIP(ip net.IP, index int, option featureDNS.IPOption) (net.IP, error) {
|
|
||||||
ip4 := ip.To4()
|
|
||||||
if ip.To16() == nil || (ip4 != nil && !option.IPv4Enable) || (ip4 == nil && !option.IPv6Enable) {
|
|
||||||
return nil, errors.New("DNS script returned invalid or disabled address at index ", index)
|
|
||||||
}
|
|
||||||
return append(net.IP(nil), ip...), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-75
@@ -3,107 +3,84 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
go_errors "errors"
|
go_errors "errors"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
featureDNS "github.com/xtls/xray-core/features/dns"
|
featureDNS "github.com/xtls/xray-core/features/dns"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecodeLuaDNSResultNativeIP(t *testing.T) {
|
func TestReadLuaDNSResult(t *testing.T) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
ip := net.ParseIP("127.0.0.1")
|
want := []net.IP{net.ParseIP("8.8.8.8"), {127, 0, 0, 1}, net.ParseIP("::1")}
|
||||||
address := L.NewUserData()
|
|
||||||
address.Value = ip
|
|
||||||
addresses := L.NewTable()
|
|
||||||
addresses.RawSetInt(1, address)
|
|
||||||
result := L.NewTable()
|
|
||||||
result.RawSetString("ips", addresses)
|
|
||||||
result.RawSetString("ttl", lua.LNumber(60))
|
|
||||||
got, ttl, err := decodeLuaDNSResult(result, featureDNS.IPOption{IPv4Enable: true})
|
|
||||||
if err != nil || ttl != 60 || len(got) != 1 || !got[0].Equal(ip) {
|
|
||||||
t.Fatalf("decodeLuaDNSResult() = %v, %d, %v", got, ttl, err)
|
|
||||||
}
|
|
||||||
addresses.RawSetInt(1, lua.LString("127.0.0.1"))
|
|
||||||
if _, _, err := decodeLuaDNSResult(result, featureDNS.IPOption{IPv4Enable: true}); err == nil {
|
|
||||||
t.Fatal("decodeLuaDNSResult accepted a string IP")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeLuaDNSResultNativeSliceCopiesIP(t *testing.T) {
|
|
||||||
L := lua.NewState()
|
|
||||||
defer L.Close()
|
|
||||||
original := net.ParseIP("8.8.8.8")
|
|
||||||
addresses := L.NewUserData()
|
addresses := L.NewUserData()
|
||||||
addresses.Value = []net.IP{original}
|
addresses.Value = want
|
||||||
result := L.NewTable()
|
ips, ttl, err := readLuaDNSResult(addresses, lua.LNumber(45), lua.LNil)
|
||||||
result.RawSetString("ips", addresses)
|
if err != nil || ttl != 45 || len(ips) != len(want) {
|
||||||
result.RawSetString("ttl", lua.LNumber(45))
|
t.Fatalf("readLuaDNSResult() = %v, TTL %d, %v", ips, ttl, err)
|
||||||
ips, ttl, err := decodeLuaDNSResult(result, featureDNS.IPOption{IPv4Enable: true})
|
|
||||||
if err != nil || ttl != 45 || len(ips) != 1 || !ips[0].Equal(original) {
|
|
||||||
t.Fatalf("decodeLuaDNSResult() = %v, TTL %d, %v", ips, ttl, err)
|
|
||||||
}
|
}
|
||||||
original[len(original)-1] = 9
|
for i := range want {
|
||||||
if !ips[0].Equal(net.ParseIP("8.8.8.8")) {
|
if !ips[i].Equal(want[i]) {
|
||||||
t.Fatalf("decoded IP changed with input: %v", ips[0])
|
t.Fatalf("IP %d = %v, want %v", i, ips[i], want[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecodeLuaDNSResultValidation(t *testing.T) {
|
func TestReadLuaDNSResultValidation(t *testing.T) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
option := featureDNS.IPOption{IPv4Enable: true}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
change func(*lua.LTable, *lua.LTable)
|
change func(*[3]lua.LValue)
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"fractional TTL", func(result, _ *lua.LTable) { result.RawSetString("ttl", lua.LNumber(1.5)) }, "invalid TTL"},
|
{"fractional TTL", func(v *[3]lua.LValue) { v[1] = lua.LNumber(1.5) }, "invalid TTL"},
|
||||||
{"oversized TTL", func(result, _ *lua.LTable) { result.RawSetString("ttl", lua.LNumber(4294967296)) }, "invalid TTL"},
|
{"oversized TTL", func(v *[3]lua.LValue) { v[1] = lua.LNumber(4294967296) }, "invalid TTL"},
|
||||||
{"string address", func(_, addresses *lua.LTable) { addresses.RawSetInt(1, lua.LString("127.0.0.1")) }, "invalid address"},
|
{"negative TTL", func(v *[3]lua.LValue) { v[1] = lua.LNumber(-1) }, "invalid TTL"},
|
||||||
{"missing addresses", func(result, _ *lua.LTable) { result.RawSetString("ips", lua.LString("127.0.0.1")) }, "must be an array"},
|
{"NaN TTL", func(v *[3]lua.LValue) { v[1] = lua.LNumber(math.NaN()) }, "invalid TTL"},
|
||||||
{"script error", func(result, _ *lua.LTable) { result.RawSetString("error", lua.LString("blocked by script")) }, "blocked by script"},
|
{"missing TTL", func(v *[3]lua.LValue) { v[1] = lua.LNil }, "invalid TTL"},
|
||||||
|
{"string IPs", func(v *[3]lua.LValue) { v[0] = lua.LString("127.0.0.1") }, "native IP slice"},
|
||||||
|
{"wrong userdata", func(v *[3]lua.LValue) { v[0].(*lua.LUserData).Value = net.ParseIP("127.0.0.1") }, "native IP slice"},
|
||||||
|
{"script error", func(v *[3]lua.LValue) { v[2] = lua.LString("blocked by script") }, "blocked by script"},
|
||||||
|
{"invalid error", func(v *[3]lua.LValue) { v[2] = lua.LTrue }, "error or string"},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
address := L.NewUserData()
|
addresses := L.NewUserData()
|
||||||
address.Value = net.ParseIP("127.0.0.1")
|
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
||||||
addresses := L.NewTable()
|
values := [3]lua.LValue{addresses, lua.LNumber(60), lua.LNil}
|
||||||
addresses.RawSetInt(1, address)
|
tc.change(&values)
|
||||||
result := L.NewTable()
|
_, _, err := readLuaDNSResult(values[0], values[1], values[2])
|
||||||
result.RawSetString("ips", addresses)
|
|
||||||
result.RawSetString("ttl", lua.LNumber(60))
|
|
||||||
tc.change(result, addresses)
|
|
||||||
_, _, err := decodeLuaDNSResult(result, option)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||||
t.Fatalf("decodeLuaDNSResult error = %v, want %q", err, tc.want)
|
t.Fatalf("readLuaDNSResult error = %v, want %q", err, tc.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
address := L.NewUserData()
|
addresses := L.NewUserData()
|
||||||
address.Value = net.ParseIP("127.0.0.1")
|
addresses.Value = []net.IP(nil)
|
||||||
addresses := L.NewTable()
|
for _, empty := range []lua.LValue{addresses, lua.LNil} {
|
||||||
addresses.RawSetInt(1, address)
|
if _, _, err := readLuaDNSResult(empty, lua.LNumber(0), lua.LNil); !go_errors.Is(err, featureDNS.ErrEmptyResponse) {
|
||||||
result := L.NewTable()
|
t.Fatalf("empty result error = %v, want ErrEmptyResponse", err)
|
||||||
result.RawSetString("ips", addresses)
|
}
|
||||||
result.RawSetString("ttl", lua.LNumber(60))
|
|
||||||
if _, _, err := decodeLuaDNSResult(result, featureDNS.IPOption{IPv6Enable: true}); err == nil {
|
|
||||||
t.Fatal("decodeLuaDNSResult accepted IPv4 with IPv6-only option")
|
|
||||||
}
|
}
|
||||||
result.RawSetString("ips", L.NewTable())
|
wantErr := go_errors.New("upstream failed")
|
||||||
if _, _, err := decodeLuaDNSResult(result, option); !go_errors.Is(err, featureDNS.ErrEmptyResponse) {
|
errorValue := L.NewUserData()
|
||||||
t.Fatalf("empty result error = %v, want ErrEmptyResponse", err)
|
errorValue.Value = wantErr
|
||||||
|
if _, _, err := readLuaDNSResult(lua.LNil, lua.LNil, errorValue); err != wantErr {
|
||||||
|
t.Fatalf("upstream error = %v, want original error %v", err, wantErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCallLuaHookCancellation(t *testing.T) {
|
func TestCallLuaHookCancellation(t *testing.T) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
if err := L.DoString(`function handleDNSQuery(q) while true do end end`); err != nil {
|
if err := L.DoString(`function handleDNSQuery(domain, ipv4, ipv6, fake) while true do end end`); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
@@ -120,16 +97,14 @@ func TestCallLuaHookCancellation(t *testing.T) {
|
|||||||
func TestCallLuaHookNormalizesDomain(t *testing.T) {
|
func TestCallLuaHookNormalizesDomain(t *testing.T) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
address := L.NewUserData()
|
addresses := L.NewUserData()
|
||||||
address.Value = net.ParseIP("127.0.0.1")
|
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
||||||
L.SetGlobal("ip", address)
|
L.SetGlobal("ips", addresses)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
function handleDNSQuery(q)
|
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
assert(type(q) == "table")
|
assert(domain == "example.com")
|
||||||
assert(q.domain == "example.com")
|
assert(ipv4 and not ipv6 and not fake)
|
||||||
assert(q.ipv4 and not q.ipv6 and not q.fake)
|
return ips, 60, nil
|
||||||
assert(q.ctx == nil)
|
|
||||||
return {ips = {ip}, ttl = 60}
|
|
||||||
end
|
end
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -140,6 +115,66 @@ func TestCallLuaHookNormalizesDomain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCallLuaHookRestoresState(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"success", `return ips, 60`, false},
|
||||||
|
{"error", `error("failed")`, true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
L := lua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
addresses := L.NewUserData()
|
||||||
|
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
||||||
|
L.SetGlobal("ips", addresses)
|
||||||
|
if err := L.DoString("function handleDNSQuery() " + tc.body + " end"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
previous, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
L.SetContext(previous)
|
||||||
|
L.Push(lua.LTrue)
|
||||||
|
_, _, err := (&DNS{}).CallLuaHook(L, context.Background(), "example.com", featureDNS.IPOption{IPv4Enable: true})
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Fatalf("hook error = %v, want error %t", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
if L.Context() != previous || L.GetTop() != 1 || L.Get(1) != lua.LTrue {
|
||||||
|
t.Fatal("hook did not restore the previous context and stack")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLuaDNSServerQuery(t *testing.T) {
|
||||||
|
L := lua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
geodata.RegisterLua(L)
|
||||||
|
option := featureDNS.IPOption{IPv4Enable: true}
|
||||||
|
ips := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("8.8.8.8")}
|
||||||
|
server := &DNS{clients: []*Client{{server: &benchmarkLuaNameServer{ips: ips}, ipOption: &option, timeoutMs: time.Second}}}
|
||||||
|
server.RegisterLua(L)
|
||||||
|
if err := L.DoString(`
|
||||||
|
local server = require("xray.dns").servers[1]
|
||||||
|
local matcher = require("xray.geodata").ipMatcher({"127.0.0.0/8"})
|
||||||
|
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
|
local ips, ttl, err = server:query(domain, ipv4, ipv6, fake)
|
||||||
|
assert(type(ips) == "userdata" and not err)
|
||||||
|
assert(matcher:anyMatch(ips))
|
||||||
|
local matched = matcher:filterIPs(ips)
|
||||||
|
return matched, ttl, err
|
||||||
|
end
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, ttl, err := server.CallLuaHook(L, context.Background(), "example.com", option)
|
||||||
|
if err != nil || ttl != 60 || len(got) != 1 || !got[0].Equal(ips[0]) {
|
||||||
|
t.Fatalf("server query = %v, TTL %d, %v", got, ttl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type benchmarkLuaNameServer struct {
|
type benchmarkLuaNameServer struct {
|
||||||
ips []net.IP
|
ips []net.IP
|
||||||
}
|
}
|
||||||
@@ -163,8 +198,8 @@ func BenchmarkLuaDNSHookCall(b *testing.B) {
|
|||||||
server.RegisterLua(L)
|
server.RegisterLua(L)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
local server = require("xray.dns").servers[1]
|
local server = require("xray.dns").servers[1]
|
||||||
function handleDNSQuery(q)
|
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
return server:query(q)
|
return server:query(domain, ipv4, ipv6, fake)
|
||||||
end
|
end
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ func newScriptEngine(path string, server *DNS) (*scriptEngine, error) {
|
|||||||
}
|
}
|
||||||
if L.GetGlobal("handleDNSQuery").Type() != lua.LTFunction {
|
if L.GetGlobal("handleDNSQuery").Type() != lua.LTFunction {
|
||||||
L.Close()
|
L.Close()
|
||||||
return nil, errors.New("DNS script must define handleDNSQuery(q)")
|
return nil, errors.New("DNS script must define handleDNSQuery(domain, ipv4, ipv6, fake)")
|
||||||
}
|
}
|
||||||
return L, nil
|
return L, nil
|
||||||
})
|
})
|
||||||
|
|||||||
+11
-11
@@ -46,12 +46,12 @@ for _, server in ipairs(servers) do
|
|||||||
end
|
end
|
||||||
assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are required")
|
assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are required")
|
||||||
|
|
||||||
function handleDNSQuery(q)
|
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
local answer = by_id.primary:query(q)
|
local ips, ttl, err = by_id.primary:query(domain, ipv4, ipv6, fake)
|
||||||
if not answer.error and us_ips:anyMatch(answer.ips) then
|
if not err and us_ips:anyMatch(ips) then
|
||||||
return answer
|
return ips, ttl, nil
|
||||||
end
|
end
|
||||||
return by_id.fallback:query(q)
|
return by_id.fallback:query(domain, ipv4, ipv6, fake)
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
scriptPath := filepath.Join(t.TempDir(), "geoip_fallback.lua")
|
scriptPath := filepath.Join(t.TempDir(), "geoip_fallback.lua")
|
||||||
@@ -144,12 +144,12 @@ func TestDNSScriptHookErrorAndFakeDNSOption(t *testing.T) {
|
|||||||
local server = require("xray.dns").servers[1]
|
local server = require("xray.dns").servers[1]
|
||||||
local log = require("xray.log")
|
local log = require("xray.log")
|
||||||
log.info("DNS script loaded")
|
log.info("DNS script loaded")
|
||||||
function handleDNSQuery(q)
|
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
log.debug("DNS query: ", q.domain)
|
log.debug("DNS query: ", domain)
|
||||||
if q.domain == "bad.example" then error("script failure") end
|
if domain == "bad.example" then error("script failure") end
|
||||||
local answer = server:query(q)
|
local ips, ttl, err = server:query(domain, ipv4, ipv6, fake)
|
||||||
if answer.error then log.error("DNS failed: ", answer.error) end
|
if err then log.error("DNS failed: ", err) end
|
||||||
return answer
|
return ips, ttl, err
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestDNSScriptConfig(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
t.Setenv("xray.location.confdir", dir)
|
t.Setenv("xray.location.confdir", dir)
|
||||||
path := filepath.Join(dir, "lookup.lua")
|
path := filepath.Join(dir, "lookup.lua")
|
||||||
if err := os.WriteFile(path, []byte("function handleDNSQuery(q) end"), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte("function handleDNSQuery(domain, ipv4, ipv6, fake) end"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user