mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-16 12:34:01 +00:00
Compare commits
23
Commits
CA
..
tls-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969e5b6c11 | ||
|
|
206a24a9f0 | ||
|
|
ac5d8a83ba | ||
|
|
8703fdb101 | ||
|
|
10879a4a8b | ||
|
|
0888beb38a | ||
|
|
34acded003 | ||
|
|
676a600edb | ||
|
|
c77a4d0e49 | ||
|
|
5a64db009b | ||
|
|
2bea2df6a1 | ||
|
|
fb2dcedbb7 | ||
|
|
7c2e05dc47 | ||
|
|
1dd2a0bd3c | ||
|
|
70ef97d564 | ||
|
|
701af60772 | ||
|
|
8855145e58 | ||
|
|
c7dbfd5e1e | ||
|
|
aa7aa9ad0a | ||
|
|
2cc08769a5 | ||
|
|
5af2e9c228 | ||
|
|
836a6fed38 | ||
|
|
2a14c50775 |
@@ -470,6 +470,9 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
|
||||
return // DO NOT CHANGE: the traffic shouldn't be processed by default outbound if the specified outbound tag doesn't exist (yet), e.g., VLESS Reverse Proxy
|
||||
}
|
||||
} else {
|
||||
if err != common.ErrNoClue {
|
||||
errors.LogErrorInner(ctx, err, "failed to pick route for ", destination)
|
||||
}
|
||||
errors.LogInfo(ctx, "default route for ", destination)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-4
@@ -93,6 +93,7 @@ type NameServer struct {
|
||||
UnexpectedIp []*geodata.IPRule `protobuf:"bytes,13,rep,name=unexpected_ip,json=unexpectedIp,proto3" json:"unexpected_ip,omitempty"`
|
||||
ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"`
|
||||
PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"`
|
||||
Id string `protobuf:"bytes,18,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -239,6 +240,13 @@ func (x *NameServer) GetPolicyID() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *NameServer) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// NameServer list used by this DNS client.
|
||||
@@ -258,6 +266,7 @@ 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"`
|
||||
Script string `protobuf:"bytes,15,opt,name=script,proto3" json:"script,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -369,6 +378,13 @@ func (x *Config) GetEnableParallelQuery() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetScript() string {
|
||||
if x != nil {
|
||||
return x.Script
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Config_HostMapping struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Domain *geodata.DomainRule `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
@@ -435,7 +451,7 @@ var File_app_dns_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_app_dns_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\xde\x05\n" +
|
||||
"\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\xee\x05\n" +
|
||||
"\n" +
|
||||
"NameServer\x123\n" +
|
||||
"\aaddress\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\aaddress\x12\x1b\n" +
|
||||
@@ -461,10 +477,11 @@ const file_app_dns_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"actUnprior\x18\x0e \x01(\bR\n" +
|
||||
"actUnprior\x12\x1a\n" +
|
||||
"\bpolicyID\x18\x11 \x01(\rR\bpolicyIDB\x0f\n" +
|
||||
"\bpolicyID\x18\x11 \x01(\rR\bpolicyID\x12\x0e\n" +
|
||||
"\x02id\x18\x12 \x01(\tR\x02idB\x0f\n" +
|
||||
"\r_disableCacheB\r\n" +
|
||||
"\v_serveStaleB\x12\n" +
|
||||
"\x10_serveExpiredTTLJ\x04\b\x04\x10\x05\"\x82\x05\n" +
|
||||
"\x10_serveExpiredTTLJ\x04\b\x04\x10\x05\"\x9a\x05\n" +
|
||||
"\x06Config\x129\n" +
|
||||
"\vname_server\x18\x05 \x03(\v2\x18.xray.app.dns.NameServerR\n" +
|
||||
"nameServer\x12\x1b\n" +
|
||||
@@ -480,7 +497,8 @@ const file_app_dns_config_proto_rawDesc = "" +
|
||||
"\x0fdisableFallback\x18\n" +
|
||||
" \x01(\bR\x0fdisableFallback\x126\n" +
|
||||
"\x16disableFallbackIfMatch\x18\v \x01(\bR\x16disableFallbackIfMatch\x120\n" +
|
||||
"\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x1a}\n" +
|
||||
"\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x12\x16\n" +
|
||||
"\x06script\x18\x0f \x01(\tR\x06script\x1a}\n" +
|
||||
"\vHostMapping\x127\n" +
|
||||
"\x06domain\x18\x02 \x01(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12\x0e\n" +
|
||||
"\x02ip\x18\x03 \x03(\fR\x02ip\x12%\n" +
|
||||
|
||||
@@ -27,6 +27,7 @@ message NameServer {
|
||||
repeated xray.common.geodata.IPRule unexpected_ip = 13;
|
||||
bool actUnprior = 14;
|
||||
uint32 policyID = 17;
|
||||
string id = 18;
|
||||
}
|
||||
|
||||
enum QueryStrategy {
|
||||
@@ -73,4 +74,6 @@ message Config {
|
||||
bool disableFallbackIfMatch = 11;
|
||||
|
||||
bool enableParallelQuery = 14;
|
||||
|
||||
string script = 15;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type DNS struct {
|
||||
domainMatcher geodata.DomainMatcher
|
||||
matcherInfos []*DomainMatcherInfo
|
||||
checkSystem bool
|
||||
script *scriptEngine
|
||||
}
|
||||
|
||||
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher.
|
||||
@@ -169,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,
|
||||
@@ -180,6 +188,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
||||
disableFallbackIfMatch: config.DisableFallbackIfMatch,
|
||||
enableParallelQuery: config.EnableParallelQuery,
|
||||
checkSystem: checkSystem,
|
||||
script: script,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -190,11 +199,19 @@ func (*DNS) Type() interface{} {
|
||||
|
||||
// Start implements common.Runnable.
|
||||
func (s *DNS) Start() error {
|
||||
if s.script != nil {
|
||||
if err := s.script.start(s); err != nil {
|
||||
return errors.New("failed to initialize DNS script").Base(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (s *DNS) Close() error {
|
||||
if s.script != nil {
|
||||
s.script.close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -279,6 +296,9 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
|
||||
}
|
||||
|
||||
// Name servers lookup
|
||||
if s.script != nil {
|
||||
return s.script.query(domain, option)
|
||||
}
|
||||
if s.enableParallelQuery {
|
||||
return s.parallelQuery(domain, option)
|
||||
} else {
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
featureDNS "github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/dns/localdns"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// luaDNSServer adapts configured and local DNS to the same Lua API.
|
||||
type luaDNSServer struct {
|
||||
id string
|
||||
name string
|
||||
query func(context.Context, string, featureDNS.IPOption) ([]net.IP, uint32, error)
|
||||
}
|
||||
|
||||
// RegisterLua makes xray.dns available to scripts backed by client.
|
||||
func RegisterLua(L *lua.LState, client featureDNS.Client) {
|
||||
var servers []luaDNSServer
|
||||
switch client := client.(type) {
|
||||
case *DNS:
|
||||
servers = luaServers(client)
|
||||
case *localdns.Client:
|
||||
servers = []luaDNSServer{{
|
||||
id: "localhost",
|
||||
name: "localhost",
|
||||
query: func(_ context.Context, domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
return client.LookupIP(domain, option)
|
||||
},
|
||||
}}
|
||||
}
|
||||
registerLua(L, servers, client)
|
||||
}
|
||||
|
||||
// registerLua makes xray.dns available to DNS scripts.
|
||||
func (s *DNS) registerLua(L *lua.LState) {
|
||||
registerLua(L, luaServers(s), nil)
|
||||
}
|
||||
|
||||
func luaServers(s *DNS) []luaDNSServer {
|
||||
servers := make([]luaDNSServer, len(s.clients))
|
||||
for i, client := range s.clients {
|
||||
servers[i] = luaDNSServer{id: client.id, name: client.Name(), query: client.QueryIP}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
|
||||
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
||||
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
|
||||
|
||||
serverList := L.CreateTable(len(servers), 0)
|
||||
for i, client := range servers {
|
||||
server := L.CreateTable(0, 2)
|
||||
|
||||
server.RawSetString("ID", lua.LString(client.id))
|
||||
|
||||
server.RawSetString("Query", L.NewFunction(func(L *lua.LState) int {
|
||||
domain, ok := L.Get(2).(lua.LString)
|
||||
if !ok {
|
||||
L.RaiseError("server:Query requires a domain")
|
||||
return 0
|
||||
}
|
||||
option := featureDNS.IPOption{
|
||||
IPv4Enable: L.CheckBool(3),
|
||||
IPv6Enable: L.CheckBool(4),
|
||||
FakeEnable: L.CheckBool(5),
|
||||
}
|
||||
ctx := L.Context()
|
||||
if ctx == nil {
|
||||
L.RaiseError("server:Query requires an active DNS query")
|
||||
return 0
|
||||
}
|
||||
var ips []net.IP
|
||||
var ttl uint32
|
||||
var err error
|
||||
if !option.FakeEnable && strings.EqualFold(client.name, "FakeDNS") {
|
||||
err = featureDNS.ErrEmptyResponse
|
||||
} else {
|
||||
ips, ttl, err = client.query(ctx, string(domain), option)
|
||||
}
|
||||
pushIPs(L, ips)
|
||||
xlua.PushNumber(L, ttl)
|
||||
xlua.PushError(L, err)
|
||||
return 3
|
||||
}))
|
||||
serverList.RawSetInt(i+1, server)
|
||||
}
|
||||
|
||||
module := L.CreateTable(0, 2)
|
||||
if servers != nil {
|
||||
module.RawSetString("Servers", serverList)
|
||||
}
|
||||
if client != nil {
|
||||
module.RawSetString("Query", newLuaClientQuery(L, client, pushIPs))
|
||||
}
|
||||
L.Push(module)
|
||||
return 1
|
||||
})
|
||||
}
|
||||
|
||||
func newLuaClientQuery(L *lua.LState, client featureDNS.Client, pushIPs func(*lua.LState, []net.IP)) *lua.LFunction {
|
||||
return L.NewFunction(func(L *lua.LState) int {
|
||||
domain, ok := L.Get(1).(lua.LString)
|
||||
if !ok {
|
||||
L.RaiseError("dns.Query requires a domain")
|
||||
return 0
|
||||
}
|
||||
option := featureDNS.IPOption{
|
||||
IPv4Enable: L.CheckBool(2),
|
||||
IPv6Enable: L.CheckBool(3),
|
||||
FakeEnable: L.CheckBool(4),
|
||||
}
|
||||
if L.Context() == nil {
|
||||
L.RaiseError("dns.Query requires an active DNS query")
|
||||
return 0
|
||||
}
|
||||
ips, ttl, err := client.LookupIP(string(domain), option)
|
||||
pushIPs(L, ips)
|
||||
xlua.PushNumber(L, ttl)
|
||||
xlua.PushError(L, err)
|
||||
return 3
|
||||
})
|
||||
}
|
||||
|
||||
// callLuaQuery runs HandleDNSQuery and leaves (ips, ttl, err) on the stack.
|
||||
func callLuaQuery(L *lua.LState, domain string, option featureDNS.IPOption) error {
|
||||
fn := L.GetGlobal("HandleDNSQuery")
|
||||
if fn.Type() != lua.LTFunction {
|
||||
return errors.New("DNS script must define HandleDNSQuery(...)")
|
||||
}
|
||||
|
||||
return 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))
|
||||
}
|
||||
|
||||
// readLuaQueryResult reads (ips, ttl, err) from the stack without copying the IPs.
|
||||
func readLuaQueryResult(L *lua.LState) ([]net.IP, uint32, error) {
|
||||
if err := xlua.ReadError(L.Get(-1), "DNS script error must be an error or string"); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
ttl, err := xlua.ReadUint32(L.Get(-2), "DNS script returned invalid TTL")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
addresses := L.Get(-3)
|
||||
if addresses == lua.LNil {
|
||||
return nil, 0, featureDNS.ErrEmptyResponse
|
||||
}
|
||||
ips, err := xlua.ReadUserData[[]net.IP](addresses, "DNS script IPs must be native IP slice userdata")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, 0, featureDNS.ErrEmptyResponse
|
||||
}
|
||||
|
||||
return ips, ttl, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
featureDNS "github.com/xtls/xray-core/features/dns"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// BenchmarkLuaDNSHook isolates scalar argument bridging and a fixed return.
|
||||
// It excludes upstream queries, result decoding, and state pool management.
|
||||
func BenchmarkLuaDNSHook(b *testing.B) {
|
||||
L := lua.NewState()
|
||||
b.Cleanup(L.Close)
|
||||
if err := L.DoString(`
|
||||
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||
return true
|
||||
end
|
||||
`); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
L.SetContext(context.Background())
|
||||
option := featureDNS.IPOption{IPv4Enable: true}
|
||||
if err := callLuaQuery(L, "example.com", option); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if L.Get(-3) != lua.LTrue {
|
||||
b.Fatal("hook did not return true")
|
||||
}
|
||||
L.Pop(3)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := callLuaQuery(L, "example.com", option); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
L.Pop(3)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLuaDNSQuery queries the same preselected, in-memory upstream.
|
||||
// client_query compares Client.QueryIP to a preloaded server:Query hook.
|
||||
// script_query additionally measures production pool and timeout management.
|
||||
// These cases do not measure DNS.LookupIP server selection or network latency.
|
||||
func BenchmarkLuaDNSQuery(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
option := featureDNS.IPOption{IPv4Enable: true}
|
||||
ip := net.ParseIP("127.0.0.1")
|
||||
upstream := &benchmarkLuaNameServer{ips: []net.IP{ip}}
|
||||
client := &Client{server: upstream, ipOption: &option, timeoutMs: time.Second}
|
||||
server := &DNS{ctx: ctx, clients: []*Client{client}}
|
||||
const script = `
|
||||
local server = require("xray.dns").Servers[1]
|
||||
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||
return server:Query(domain, ipv4, ipv6, fake)
|
||||
end
|
||||
`
|
||||
L := lua.NewState()
|
||||
b.Cleanup(L.Close)
|
||||
server.registerLua(L)
|
||||
if err := L.DoString(script); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
L.SetContext(ctx)
|
||||
|
||||
path := filepath.Join(b.TempDir(), "query.lua")
|
||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
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
|
||||
query func() ([]net.IP, uint32, error)
|
||||
}{
|
||||
{"client_query/native", func() ([]net.IP, uint32, error) {
|
||||
return client.QueryIP(ctx, "example.com", option)
|
||||
}},
|
||||
{"client_query/lua", func() ([]net.IP, uint32, error) {
|
||||
if err := callLuaQuery(L, "example.com", option); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
ips, ttl, err := readLuaQueryResult(L)
|
||||
L.Pop(3)
|
||||
return ips, ttl, err
|
||||
}},
|
||||
{"script_query/lua", func() ([]net.IP, uint32, error) {
|
||||
return engine.query("example.com", option)
|
||||
}},
|
||||
} {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
ips, ttl, err := bench.query()
|
||||
if err != nil || ttl != 60 || len(ips) != 1 || !ips[0].Equal(ip) {
|
||||
b.Fatalf("query() = %v, TTL %d, %v; want %v, TTL 60", ips, ttl, err, ip)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ips, ttl, err = bench.query()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
if ttl != 60 || len(ips) != 1 || !ips[0].Equal(ip) {
|
||||
b.Fatalf("query() = %v, TTL %d; want %v, TTL 60", ips, ttl, ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
featureDNS "github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/dns/localdns"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestReadLuaQueryResult(t *testing.T) {
|
||||
wantIPs := []net.IP{net.ParseIP("8.8.8.8"), {127, 0, 0, 1}, net.ParseIP("::1")}
|
||||
nativeErr := go_errors.New("upstream failed")
|
||||
for _, tc := range []struct {
|
||||
name, values string
|
||||
wantIPs []net.IP
|
||||
wantTTL uint32
|
||||
wantErr error
|
||||
wantMessage string
|
||||
}{
|
||||
{name: "IPs", values: `ips, 45`, wantIPs: wantIPs, wantTTL: 45},
|
||||
{name: "nil IPs", values: `nil, 0`, wantErr: featureDNS.ErrEmptyResponse},
|
||||
{name: "empty IPs", values: `emptyIPs, 0`, wantErr: featureDNS.ErrEmptyResponse},
|
||||
{name: "native error", values: `nil, nil, nativeError`, wantErr: nativeErr},
|
||||
{name: "string error", values: `nil, nil, "blocked"`, wantMessage: "blocked"},
|
||||
{name: "fractional TTL", values: `ips, 1.5`, wantMessage: "invalid TTL"},
|
||||
{name: "oversized TTL", values: `ips, 4294967296`, wantMessage: "invalid TTL"},
|
||||
{name: "negative TTL", values: `ips, -1`, wantMessage: "invalid TTL"},
|
||||
{name: "NaN TTL", values: `ips, 0/0`, wantMessage: "invalid TTL"},
|
||||
{name: "missing TTL", values: `ips`, wantMessage: "invalid TTL"},
|
||||
{name: "string IPs", values: `"127.0.0.1", 60`, wantMessage: "native IP slice"},
|
||||
{name: "wrong userdata", values: `ip, 60`, wantMessage: "native IP slice"},
|
||||
{name: "invalid error", values: `ips, 60, false`, wantMessage: "error or string"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
for name, value := range map[string]any{"ips": wantIPs, "ip": wantIPs[0], "emptyIPs": []net.IP(nil), "nativeError": nativeErr} {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value
|
||||
L.SetGlobal(name, ud)
|
||||
}
|
||||
fn, err := L.LoadString("return " + tc.values)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 3, Protect: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ips, ttl, err := readLuaQueryResult(L)
|
||||
switch {
|
||||
case tc.wantErr != nil:
|
||||
if err != tc.wantErr {
|
||||
t.Fatalf("error = %v, want original error %v", err, tc.wantErr)
|
||||
}
|
||||
case tc.wantMessage != "":
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantMessage) {
|
||||
t.Fatalf("error = %v, want %q", err, tc.wantMessage)
|
||||
}
|
||||
case err != nil:
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl != tc.wantTTL || len(ips) != len(tc.wantIPs) {
|
||||
t.Fatalf("result = %v, TTL %d; want %v, TTL %d", ips, ttl, tc.wantIPs, tc.wantTTL)
|
||||
}
|
||||
for i := range ips {
|
||||
if !ips[i].Equal(tc.wantIPs[i]) {
|
||||
t.Fatalf("IP %d = %v, want %v", i, ips[i], tc.wantIPs[i])
|
||||
}
|
||||
}
|
||||
if len(ips) != 0 && &ips[0] != &tc.wantIPs[0] {
|
||||
t.Fatal("result copied the IP slice")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallLuaQueryCancellation(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
if err := L.DoString(`function HandleDNSQuery(domain, ipv4, ipv6, fake) while true do end end`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
L.SetContext(ctx)
|
||||
err := callLuaQuery(L, "example.com", featureDNS.IPOption{IPv4Enable: true})
|
||||
if err == nil {
|
||||
t.Fatal("callLuaQuery did not stop after context cancellation")
|
||||
}
|
||||
if L.Context() != ctx {
|
||||
t.Fatal("callLuaQuery changed the Lua state's context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallLuaQuery(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(domain, ipv4, ipv6, fake)
|
||||
assert(domain == "example.com")
|
||||
assert(ipv4 and not ipv6 and not fake)
|
||||
return ips, 60, nil
|
||||
end
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := callLuaQuery(L, "ExAmPlE.CoM", featureDNS.IPOption{IPv4Enable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if L.GetTop() != 3 || L.Get(1) != addresses || L.Get(2) != lua.LNumber(60) || L.Get(3) != lua.LNil {
|
||||
t.Fatal("callLuaQuery did not leave the three query results on the 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").BuildIPMatcher("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(#ips == 2 and ips[1]:String() == "127.0.0.1" and ips[2]:String() == "8.8.8.8")
|
||||
assert(matcher:Match(ips[1]) and not matcher:Match(ips[2]))
|
||||
assert(matcher:AnyMatch(ips))
|
||||
local matched, unmatched = matcher:FilterIPs(ips)
|
||||
assert(#matched == 1 and #unmatched == 1)
|
||||
assert(matched[1]:Equal(ips[1]) and unmatched[1]:Equal(ips[2]))
|
||||
return matched, ttl, err
|
||||
end
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
L.SetContext(context.Background())
|
||||
if err := callLuaQuery(L, "example.com", option); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ttl, err := readLuaQueryResult(L)
|
||||
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 luaDNSClient struct {
|
||||
featureDNS.Client
|
||||
lookup func(string, featureDNS.IPOption) ([]net.IP, uint32, error)
|
||||
}
|
||||
|
||||
func (c *luaDNSClient) LookupIP(domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
return c.lookup(domain, option)
|
||||
}
|
||||
|
||||
func TestLuaDNSClientQuery(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.SetContext(context.Background())
|
||||
geodata.RegisterLua(L)
|
||||
want := []net.IP{{127, 0, 0, 1}, net.ParseIP("::1")}
|
||||
client := &luaDNSClient{lookup: func(domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
if domain != "MiXeD.Example." || !option.IPv4Enable || option.IPv6Enable || !option.FakeEnable {
|
||||
t.Fatalf("dns.Query arguments = %q, %+v", domain, option)
|
||||
}
|
||||
return want, 42, nil
|
||||
}}
|
||||
RegisterLua(L, client)
|
||||
if err := L.DoString(`
|
||||
local dns = require("xray.dns")
|
||||
local matcher = require("xray.geodata").BuildIPMatcher("127.0.0.1")
|
||||
assert(dns.Servers == nil)
|
||||
ips, ttl, err = dns.Query("MiXeD.Example.", true, false, true)
|
||||
assert(not err and ttl == 42 and matcher:AnyMatch(ips))
|
||||
assert(#ips == 2 and ips[1]:String() == "127.0.0.1" and ips[2]:String() == "::1")
|
||||
assert(matcher:Match(ips[1]) and not matcher:Match(ips[2]))
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := L.GetGlobal("ips").(*lua.LUserData).Value.([]net.IP)
|
||||
if &got[0] != &want[0] {
|
||||
t.Fatal("dns.Query copied the IP slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaDNSLocalClient(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.SetContext(context.Background())
|
||||
RegisterLua(L, localdns.New())
|
||||
if err := L.DoString(`
|
||||
local dns = require("xray.dns")
|
||||
assert(dns.Servers[1].ID == "localhost")
|
||||
serverIPs, _, serverErr = dns.Servers[1]:Query("127.0.0.1", true, false, false)
|
||||
clientIPs, _, clientErr = dns.Query("127.0.0.1", true, false, false)
|
||||
assert(not serverErr and not clientErr)
|
||||
assert(#serverIPs == 1 and #clientIPs == 1)
|
||||
assert(serverIPs[1]:String() == "127.0.0.1" and serverIPs[1]:Equal(clientIPs[1]))
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"serverIPs", "clientIPs"} {
|
||||
ips := L.GetGlobal(name).(*lua.LUserData).Value.([]net.IP)
|
||||
if len(ips) != 1 || !ips[0].Equal(net.ParseIP("127.0.0.1")) {
|
||||
t.Fatalf("%s = %v", name, ips)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaDNSQueryEmptyIPs(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ips []net.IP
|
||||
}{
|
||||
{"nil", nil},
|
||||
{"empty", []net.IP{}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.SetContext(context.Background())
|
||||
L.SetGlobal("expectNil", lua.LBool(tc.ips == nil))
|
||||
client := &luaDNSClient{lookup: func(string, featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
return tc.ips, 0, featureDNS.ErrEmptyResponse
|
||||
}}
|
||||
registerLua(L, []luaDNSServer{{query: func(_ context.Context, domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
return client.LookupIP(domain, option)
|
||||
}}}, client)
|
||||
if err := L.DoString(`
|
||||
local dns = require("xray.dns")
|
||||
for _, query in ipairs({
|
||||
function() return dns.Servers[1]:Query("empty.example", true, false, false) end,
|
||||
function() return dns.Query("empty.example", true, false, false) end,
|
||||
}) do
|
||||
local ips, ttl, err = query()
|
||||
assert(ttl == 0 and err)
|
||||
if expectNil then
|
||||
assert(ips == nil)
|
||||
else
|
||||
assert(type(ips) == "userdata" and #ips == 0)
|
||||
assert(not pcall(function() return ips[1] end))
|
||||
end
|
||||
end
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type benchmarkLuaNameServer struct {
|
||||
ips []net.IP
|
||||
}
|
||||
|
||||
func (*benchmarkLuaNameServer) Name() string { return "benchmark" }
|
||||
func (*benchmarkLuaNameServer) IsDisableCache() bool { return true }
|
||||
func (s *benchmarkLuaNameServer) QueryIP(context.Context, string, featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
return s.ips, 60, nil
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type Server interface {
|
||||
|
||||
// Client is the interface for DNS client.
|
||||
type Client struct {
|
||||
id string
|
||||
server Server
|
||||
skipFallback bool
|
||||
expectedIPs geodata.IPMatcher
|
||||
@@ -97,7 +98,7 @@ func NewClient(
|
||||
ipOption dns.IPOption,
|
||||
updateRules func(bool),
|
||||
) (*Client, error) {
|
||||
client := &Client{}
|
||||
client := &Client{id: ns.Id}
|
||||
err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
|
||||
// Create a new server for each client for now
|
||||
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
|
||||
|
||||
@@ -49,5 +49,5 @@ func NewLocalNameServer() *LocalNameServer {
|
||||
|
||||
// NewLocalDNSClient creates localdns client object for directly lookup in system DNS.
|
||||
func NewLocalDNSClient(ipOption dns.IPOption) *Client {
|
||||
return &Client{server: NewLocalNameServer(), ipOption: &ipOption}
|
||||
return &Client{id: "localhost", server: NewLocalNameServer(), ipOption: &ipOption}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const scriptExecutionTimeout = 6 * time.Second
|
||||
|
||||
type scriptEngine struct {
|
||||
program *xlua.Program
|
||||
pool *xlua.Pool
|
||||
}
|
||||
|
||||
func newScriptEngine(script string) (*scriptEngine, error) {
|
||||
program, err := xlua.Compile(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &scriptEngine{program: program}, nil
|
||||
}
|
||||
|
||||
func (e *scriptEngine) start(server *DNS) error {
|
||||
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
|
||||
scriptExecutionTimeout*20,
|
||||
256,
|
||||
func(L *lua.LState) {
|
||||
geodata.RegisterLua(L)
|
||||
log.RegisterLua(L)
|
||||
server.registerLua(L)
|
||||
},
|
||||
func(L *lua.LState) error {
|
||||
if L.GetGlobal("HandleDNSQuery").Type() != lua.LTFunction {
|
||||
return errors.New("DNS script must define HandleDNSQuery(...)")
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.pool = pool
|
||||
errors.LogInfo(server.ctx, "DNS script initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *scriptEngine) close() {
|
||||
if e.pool != nil {
|
||||
e.pool.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *scriptEngine) query(domain string, option dns.IPOption) (ips []net.IP, ttl uint32, queryErr error) {
|
||||
if err := e.pool.WithState(nil, 0, func(L *lua.LState) error {
|
||||
if err := callLuaQuery(L, domain, option); err != nil {
|
||||
return err
|
||||
}
|
||||
ips, ttl, queryErr = readLuaQueryResult(L)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return ips, ttl, queryErr
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
featureDNS "github.com/xtls/xray-core/features/dns"
|
||||
)
|
||||
|
||||
type scriptNameServer struct {
|
||||
name string
|
||||
answers map[string]net.IP
|
||||
errors map[string]error
|
||||
ttl uint32
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *scriptNameServer) Name() string { return s.name }
|
||||
func (s *scriptNameServer) IsDisableCache() bool { return true }
|
||||
|
||||
func (s *scriptNameServer) QueryIP(ctx context.Context, domain string, _ featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
s.calls++
|
||||
if err := s.errors[domain]; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
ip, ok := s.answers[domain]
|
||||
if !ok {
|
||||
return nil, 0, featureDNS.ErrEmptyResponse
|
||||
}
|
||||
return []net.IP{ip}, s.ttl, nil
|
||||
}
|
||||
|
||||
func TestDNSScriptQuery(t *testing.T) {
|
||||
wantIP := net.ParseIP("127.0.0.1")
|
||||
upstreamErr := go_errors.New("upstream failed")
|
||||
for _, tc := range []struct {
|
||||
name, body string
|
||||
wantIPs []net.IP
|
||||
wantTTL uint32
|
||||
wantErr error
|
||||
wantMessage string
|
||||
wantCalls uint32
|
||||
}{
|
||||
{name: "IPs", body: `return server:Query(domain, ipv4, ipv6, fake)`, wantIPs: []net.IP{wantIP}, wantTTL: 60, wantCalls: 2},
|
||||
{name: "empty result", body: `return nil, 0`, wantErr: featureDNS.ErrEmptyResponse, wantCalls: 2},
|
||||
{name: "upstream error", body: `return server:Query("failed.example", ipv4, ipv6, fake)`, wantErr: upstreamErr, wantCalls: 2},
|
||||
{name: "string error", body: `return nil, nil, "blocked"`, wantMessage: "blocked", wantCalls: 2},
|
||||
{name: "invalid result", body: `return false, 0`, wantMessage: "native IP slice", wantCalls: 2},
|
||||
{name: "execution error", body: `error("execution failed")`, wantMessage: "execution failed", wantCalls: 1},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
script := `
|
||||
local server = require("xray.dns").Servers[1]
|
||||
local calls = 0
|
||||
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||
calls = calls + 1
|
||||
if domain == "count.example" then
|
||||
local ips, _, err = server:Query("good.example", ipv4, ipv6, fake)
|
||||
return ips, calls, err
|
||||
end
|
||||
` + tc.body + `
|
||||
end
|
||||
`
|
||||
path := filepath.Join(t.TempDir(), "query.lua")
|
||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
option := featureDNS.IPOption{IPv4Enable: true}
|
||||
upstream := &scriptNameServer{
|
||||
name: "test",
|
||||
answers: map[string]net.IP{"good.example": wantIP},
|
||||
errors: map[string]error{"failed.example": upstreamErr},
|
||||
ttl: 60,
|
||||
}
|
||||
server := &DNS{
|
||||
ctx: context.Background(),
|
||||
clients: []*Client{{server: upstream, ipOption: &option, timeoutMs: time.Second}},
|
||||
}
|
||||
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)
|
||||
switch {
|
||||
case tc.wantErr != nil:
|
||||
if err != tc.wantErr {
|
||||
t.Fatalf("query error = %v, want original error %v", err, tc.wantErr)
|
||||
}
|
||||
case tc.wantMessage != "":
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantMessage) {
|
||||
t.Fatalf("query error = %v, want %q", err, tc.wantMessage)
|
||||
}
|
||||
case err != nil:
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl != tc.wantTTL || len(ips) != len(tc.wantIPs) {
|
||||
t.Fatalf("query = %v, TTL %d; want %v, TTL %d", ips, ttl, tc.wantIPs, tc.wantTTL)
|
||||
}
|
||||
for i := range ips {
|
||||
if !ips[i].Equal(tc.wantIPs[i]) {
|
||||
t.Fatalf("IP %d = %v, want %v", i, ips[i], tc.wantIPs[i])
|
||||
}
|
||||
}
|
||||
|
||||
ips, calls, err := engine.query("count.example", option)
|
||||
if err != nil || calls != tc.wantCalls || len(ips) != 1 || !ips[0].Equal(wantIP) {
|
||||
t.Fatalf("next query = %v, calls %d, %v; want %v, calls %d", ips, calls, err, wantIP, tc.wantCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSScriptGeoIPFallback(t *testing.T) {
|
||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||
script := `
|
||||
local servers = require("xray.dns").Servers
|
||||
local us_ips = require("xray.geodata").BuildIPMatcher("geoip:us")
|
||||
|
||||
local by_id = {}
|
||||
for _, server in ipairs(servers) do
|
||||
by_id[server.ID] = server
|
||||
end
|
||||
assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are required")
|
||||
|
||||
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
|
||||
if not err and us_ips:AnyMatch(ips) then
|
||||
return ips, ttl, nil
|
||||
end
|
||||
return by_id.fallback:Query(domain, ipv4, ipv6, fake)
|
||||
end
|
||||
`
|
||||
scriptPath := filepath.Join(t.TempDir(), "geoip_fallback.lua")
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
primary := &scriptNameServer{
|
||||
name: "primary",
|
||||
answers: map[string]net.IP{
|
||||
"us.example": net.ParseIP("2001:4860:4860::8888"),
|
||||
"other.example": net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
ttl: 30,
|
||||
}
|
||||
fallback := &scriptNameServer{
|
||||
name: "fallback",
|
||||
answers: map[string]net.IP{"other.example": net.ParseIP("9.9.9.9")},
|
||||
ttl: 60,
|
||||
}
|
||||
option := featureDNS.IPOption{IPv4Enable: true, IPv6Enable: true}
|
||||
hosts, err := NewStaticHosts(nil)
|
||||
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,
|
||||
script: engine,
|
||||
clients: []*Client{
|
||||
{id: "primary", server: primary, ipOption: &option, timeoutMs: 2 * time.Second},
|
||||
{id: "fallback", server: fallback, ipOption: &option, timeoutMs: 2 * time.Second},
|
||||
},
|
||||
}
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
for _, tc := range []struct {
|
||||
domain string
|
||||
ip net.IP
|
||||
ttl uint32
|
||||
}{
|
||||
{"Us.Example.", net.ParseIP("2001:4860:4860::8888"), 30},
|
||||
{"other.example", net.ParseIP("9.9.9.9"), 60},
|
||||
} {
|
||||
ips, ttl, err := server.LookupIP(tc.domain, option)
|
||||
if err != nil {
|
||||
t.Fatalf("LookupIP(%q): %v", tc.domain, err)
|
||||
}
|
||||
if ttl != tc.ttl || len(ips) != 1 || !ips[0].Equal(tc.ip) {
|
||||
t.Fatalf("LookupIP(%q) = %v, TTL %d; want %v, TTL %d", tc.domain, ips, ttl, tc.ip, tc.ttl)
|
||||
}
|
||||
}
|
||||
if primary.calls != 2 || fallback.calls != 1 {
|
||||
t.Fatalf("upstream calls: primary %d, fallback %d; want 2 and 1", primary.calls, fallback.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSScriptRejectsInvalidStartup(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, script string
|
||||
compileError bool
|
||||
}{
|
||||
{"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, 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.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 := `
|
||||
local server = require("xray.dns").Servers[1]
|
||||
local log = require("xray.log")
|
||||
log.Info("DNS script loaded")
|
||||
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||
log.Debug("DNS query: ", domain)
|
||||
local ips, ttl, err = server:Query(domain, ipv4, ipv6, fake)
|
||||
if err then log.Error("DNS failed: ", err) end
|
||||
return ips, ttl, err
|
||||
end
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
option := featureDNS.IPOption{IPv4Enable: true}
|
||||
hosts, err := NewStaticHosts(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
upstream := &scriptNameServer{
|
||||
name: "FakeDNS",
|
||||
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,
|
||||
script: engine,
|
||||
clients: []*Client{{id: "fake", server: upstream, ipOption: &option, timeoutMs: time.Second}},
|
||||
}
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
if _, _, err := server.LookupIP("good.example", option); err != featureDNS.ErrEmptyResponse {
|
||||
t.Fatalf("FakeDNS without FakeEnable = %v, want ErrEmptyResponse", err)
|
||||
}
|
||||
if upstream.calls != 0 {
|
||||
t.Fatalf("FakeDNS was queried without FakeEnable: %d calls", upstream.calls)
|
||||
}
|
||||
withFake := featureDNS.IPOption{IPv4Enable: true, FakeEnable: true}
|
||||
ips, ttl, err := server.LookupIP("good.example", withFake)
|
||||
if err != nil || ttl != 30 || len(ips) != 1 || !ips[0].Equal(net.ParseIP("198.18.0.1")) {
|
||||
t.Fatalf("FakeDNS with FakeEnable = %v, TTL %d, %v", ips, ttl, err)
|
||||
}
|
||||
if upstream.calls != 1 {
|
||||
t.Fatalf("FakeDNS query count = %d, want 1", upstream.calls)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ import (
|
||||
|
||||
const idleTimeout = 30 * time.Second
|
||||
|
||||
// DO NOT EDIT. Ignored by geodata, rejected by Lua.
|
||||
const luaGuardHeader = "\x12\x01\x00"
|
||||
|
||||
type stage struct {
|
||||
target string
|
||||
temp string
|
||||
@@ -170,6 +173,10 @@ func (d *downloader) downloadOne(asset *Asset) (stage, error) {
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := temp.WriteString(luaGuardHeader); err != nil {
|
||||
temp.Close()
|
||||
return stage{}, err
|
||||
}
|
||||
if err := d.fetch(asset.Url, temp); err != nil {
|
||||
temp.Close()
|
||||
return stage{}, err
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
commongeodata "github.com/xtls/xray-core/common/geodata"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/yuin/gopher-lua/parse"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestDownloadOneLuaGuard(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
body string
|
||||
code string
|
||||
}{
|
||||
{"geodata", "\x0a\x03\x0a\x01A", "A"},
|
||||
{"prefixed geodata", luaGuardHeader + "\x0a\x03\x0a\x01A", "A"},
|
||||
{"Lua", "function HandleRoute() end", ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("xray.location.asset", dir)
|
||||
if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(tc.body))
|
||||
}))
|
||||
defer server.Close()
|
||||
d := &downloader{ctx: context.Background(), httpClient: server.Client()}
|
||||
staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tx, err := swapAll([]stage{staged})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tx.commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(staged.target)
|
||||
if err != nil || string(data) != luaGuardHeader+tc.body {
|
||||
t.Fatalf("downloaded data = % x, %v", data, err)
|
||||
}
|
||||
program, err := xlua.Compile(staged.target)
|
||||
var syntaxErr *parse.Error
|
||||
if program != nil || !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
|
||||
t.Fatalf("want Lua rejection at the first byte, got %v, %v", program, err)
|
||||
}
|
||||
if tc.code != "" {
|
||||
var list commongeodata.GeoSiteList
|
||||
if err := proto.Unmarshal(data, &list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(list.Entry) != 1 || list.Entry[0].Code != tc.code {
|
||||
t.Fatalf("header changed geodata: %v", &list)
|
||||
}
|
||||
if _, err := commongeodata.ParseDomainRules([]string{"ext:" + filepath.Base(staged.target) + ":" + tc.code}, commongeodata.Domain_Full); err != nil {
|
||||
t.Fatalf("geodata loader rejected the header: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound
|
||||
Mark: streamSettings.SocketSettings.Mark,
|
||||
})
|
||||
}
|
||||
if streamSettings != nil && streamSettings.ProtocolName == "splithttp" {
|
||||
if streamSettings != nil && (streamSettings.ProtocolName == "splithttp" || streamSettings.ProtocolName == "masque") {
|
||||
ctx = session.ContextWithAllowedNetwork(ctx, net.Network_UDP)
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -587,6 +587,7 @@ 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"`
|
||||
Script string `protobuf:"bytes,4,opt,name=script,proto3" json:"script,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -642,6 +643,13 @@ func (x *Config) GetBalancingRule() []*BalancingRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetScript() string {
|
||||
if x != nil {
|
||||
return x.Script
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_app_router_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_app_router_config_proto_rawDesc = "" +
|
||||
@@ -699,11 +707,12 @@ const file_app_router_config_proto_rawDesc = "" +
|
||||
"\tbaselines\x18\x03 \x03(\x03R\tbaselines\x12\x1a\n" +
|
||||
"\bexpected\x18\x04 \x01(\x05R\bexpected\x12\x16\n" +
|
||||
"\x06maxRTT\x18\x05 \x01(\x03R\x06maxRTT\x12\x1c\n" +
|
||||
"\ttolerance\x18\x06 \x01(\x02R\ttolerance\"\x96\x02\n" +
|
||||
"\ttolerance\x18\x06 \x01(\x02R\ttolerance\"\xae\x02\n" +
|
||||
"\x06Config\x12O\n" +
|
||||
"\x0fdomain_strategy\x18\x01 \x01(\x0e2&.xray.app.router.Config.DomainStrategyR\x0edomainStrategy\x120\n" +
|
||||
"\x04rule\x18\x02 \x03(\v2\x1c.xray.app.router.RoutingRuleR\x04rule\x12E\n" +
|
||||
"\x0ebalancing_rule\x18\x03 \x03(\v2\x1e.xray.app.router.BalancingRuleR\rbalancingRule\"B\n" +
|
||||
"\x0ebalancing_rule\x18\x03 \x03(\v2\x1e.xray.app.router.BalancingRuleR\rbalancingRule\x12\x16\n" +
|
||||
"\x06script\x18\x04 \x01(\tR\x06script\"B\n" +
|
||||
"\x0eDomainStrategy\x12\b\n" +
|
||||
"\x04AsIs\x10\x00\x12\x10\n" +
|
||||
"\fIpIfNonMatch\x10\x02\x12\x0e\n" +
|
||||
|
||||
@@ -110,4 +110,5 @@ message Config {
|
||||
DomainStrategy domain_strategy = 1;
|
||||
repeated RoutingRule rule = 2;
|
||||
repeated BalancingRule balancing_rule = 3;
|
||||
string script = 4;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const (
|
||||
luaContextType = "xray.router.Context"
|
||||
luaAttributesType = "xray.router.Attributes"
|
||||
)
|
||||
|
||||
// RegisterLua makes xray.router available to routing scripts.
|
||||
func (r *Router) RegisterLua(L *lua.LState) {
|
||||
registerLuaContext(L)
|
||||
|
||||
L.PreloadModule("xray.router", func(L *lua.LState) int {
|
||||
module := L.CreateTable(0, 7)
|
||||
|
||||
module.RawSetString("NetworkUnknown", lua.LNumber(net.Network_Unknown))
|
||||
module.RawSetString("NetworkTCP", lua.LNumber(net.Network_TCP))
|
||||
module.RawSetString("NetworkUDP", lua.LNumber(net.Network_UDP))
|
||||
module.RawSetString("NetworkUNIX", lua.LNumber(net.Network_UNIX))
|
||||
module.RawSetString("LocalOS", lua.LString(runtime.GOOS))
|
||||
|
||||
module.RawSetString("PickOutbound", L.NewFunction(func(L *lua.LState) int {
|
||||
tag, ok := L.Get(1).(lua.LString)
|
||||
if !ok {
|
||||
L.ArgError(1, "balancer tag must be a string")
|
||||
return 0
|
||||
}
|
||||
balancer, found := (*r.balancers.Load())[string(tag)]
|
||||
if !found {
|
||||
xlua.PushNil(L)
|
||||
xlua.PushError(L, errors.New("balancer ", tag, " not found"))
|
||||
return 2
|
||||
}
|
||||
outboundTag, err := balancer.PickOutbound()
|
||||
xlua.PushString(L, outboundTag)
|
||||
xlua.PushError(L, err)
|
||||
return 2
|
||||
}))
|
||||
|
||||
module.RawSetString("FindProcess", L.NewFunction(func(L *lua.LState) int {
|
||||
pid, name, path, err := findProcess(checkLuaContext(L), net.FindProcess)
|
||||
xlua.PushNumber(L, pid)
|
||||
xlua.PushString(L, name)
|
||||
xlua.PushString(L, path)
|
||||
xlua.PushError(L, err)
|
||||
return 4
|
||||
}))
|
||||
|
||||
L.Push(module)
|
||||
return 1
|
||||
})
|
||||
}
|
||||
|
||||
func registerLuaContext(L *lua.LState) {
|
||||
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
|
||||
attributes := L.NewTypeMetatable(luaAttributesType)
|
||||
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||
values := L.CheckUserData(1).Value.(map[string]string)
|
||||
key := L.CheckString(2)
|
||||
if value, found := values[key]; found {
|
||||
xlua.PushString(L, value)
|
||||
} else {
|
||||
xlua.PushNil(L)
|
||||
}
|
||||
return 1
|
||||
}))
|
||||
methods := L.CreateTable(0, 4)
|
||||
L.SetFuncs(methods, map[string]lua.LGFunction{
|
||||
"GetSourceIPs": func(L *lua.LState) int {
|
||||
pushIPs(L, checkLuaContext(L).GetSourceIPs())
|
||||
return 1
|
||||
},
|
||||
"GetTargetIPs": func(L *lua.LState) int {
|
||||
pushIPs(L, checkLuaContext(L).GetTargetIPs())
|
||||
return 1
|
||||
},
|
||||
"GetLocalIPs": func(L *lua.LState) int {
|
||||
pushIPs(L, checkLuaContext(L).GetLocalIPs())
|
||||
return 1
|
||||
},
|
||||
"GetAttributes": func(L *lua.LState) int {
|
||||
values := L.NewUserData()
|
||||
values.Value = checkLuaContext(L).GetAttributes()
|
||||
L.SetMetatable(values, attributes)
|
||||
L.Push(values)
|
||||
return 1
|
||||
},
|
||||
})
|
||||
L.SetField(L.NewTypeMetatable(luaContextType), "__index", methods)
|
||||
}
|
||||
|
||||
func checkLuaContext(L *lua.LState) routing.Context {
|
||||
ctx, ok := L.CheckUserData(1).Value.(routing.Context)
|
||||
if !ok {
|
||||
L.ArgError(1, "routing context expected")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// callLuaRoute runs HandleRoute and leaves (outboundTag, ruleTag, err) on the stack.
|
||||
func callLuaRoute(L *lua.LState, ctx routing.Context) error {
|
||||
fn := L.GetGlobal("HandleRoute")
|
||||
if fn.Type() != lua.LTFunction {
|
||||
return errors.New("routing script must define HandleRoute(...)")
|
||||
}
|
||||
|
||||
value := L.NewUserData()
|
||||
value.Value = ctx
|
||||
L.SetMetatable(value, L.GetTypeMetatable(luaContextType))
|
||||
|
||||
return L.CallByParam(lua.P{Fn: fn, NRet: 3, Protect: true},
|
||||
value,
|
||||
lua.LString(ctx.GetInboundTag()),
|
||||
lua.LNumber(ctx.GetSourcePort()),
|
||||
lua.LNumber(ctx.GetTargetPort()),
|
||||
lua.LNumber(ctx.GetLocalPort()),
|
||||
lua.LString(strings.ToLower(ctx.GetTargetDomain())),
|
||||
lua.LNumber(ctx.GetNetwork()),
|
||||
lua.LString(ctx.GetProtocol()),
|
||||
lua.LString(ctx.GetUser()),
|
||||
lua.LNumber(ctx.GetVlessRoute()),
|
||||
lua.LBool(ctx.GetSkipDNSResolve()))
|
||||
}
|
||||
|
||||
// readLuaRouteResult reads (outboundTag, ruleTag, err) from the stack.
|
||||
func readLuaRouteResult(L *lua.LState) (string, string, error) {
|
||||
if err := xlua.ReadError(L.Get(-1), "routing script error must be an error or string"); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
outboundTag, err := xlua.ReadOptionalString(L.Get(-3), "routing script outboundTag must be a string or nil")
|
||||
if err != nil || outboundTag == "" {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
ruleTag, err := xlua.ReadOptionalString(L.Get(-2), "routing script ruleTag must be a string")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return outboundTag, ruleTag, nil
|
||||
}
|
||||
|
||||
type processFinder func(string, string, uint16, string, uint16) (int, string, string, error)
|
||||
|
||||
func findProcess(ctx routing.Context, finder processFinder) (int, string, string, error) {
|
||||
sources := ctx.GetSourceIPs()
|
||||
if len(sources) == 0 {
|
||||
return 0, "", "", errors.New("process lookup requires a source IP")
|
||||
}
|
||||
var network string
|
||||
switch ctx.GetNetwork() {
|
||||
case net.Network_TCP:
|
||||
network = "tcp"
|
||||
case net.Network_UDP:
|
||||
network = "udp"
|
||||
default:
|
||||
return 0, "", "", errors.New("process lookup requires TCP or UDP")
|
||||
}
|
||||
targetIP, targetPort := "", uint16(0)
|
||||
if targets := ctx.GetTargetIPs(); len(targets) > 0 {
|
||||
targetIP, targetPort = targets[0].String(), uint16(ctx.GetTargetPort())
|
||||
}
|
||||
return finder(network, sources[0].String(), uint16(ctx.GetSourcePort()), targetIP, targetPort)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
routing_session "github.com/xtls/xray-core/features/routing/session"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func benchmarkRouteContext(target net.Destination) *routing_session.Context {
|
||||
// Use the production context: its IP getters construct a slice per call.
|
||||
// The cached IP slices in luaRouteTestContext would undercount this cost.
|
||||
return &routing_session.Context{
|
||||
Inbound: &session.Inbound{
|
||||
Tag: "in",
|
||||
Source: net.TCPDestination(net.LocalHostIP, 1234),
|
||||
Local: net.TCPDestination(net.LocalHostIP, 5678),
|
||||
},
|
||||
Outbound: &session.Outbound{Target: target},
|
||||
Content: &session.Content{Protocol: "tls"},
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkRouteState(b *testing.B, r *Router, script string) *lua.LState {
|
||||
b.Helper()
|
||||
L := lua.NewState()
|
||||
b.Cleanup(L.Close)
|
||||
r.RegisterLua(L)
|
||||
geodata.RegisterLua(L)
|
||||
if err := L.DoString(script); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
L.SetContext(context.Background())
|
||||
return L
|
||||
}
|
||||
|
||||
// BenchmarkLuaRouteHook isolates argument bridging and a fixed-return hook.
|
||||
// It excludes rules, result decoding, the state pool, and Route construction.
|
||||
func BenchmarkLuaRouteHook(b *testing.B) {
|
||||
L := benchmarkRouteState(b, new(Router), `
|
||||
function HandleRoute(ctx, inboundTag, sourcePort, targetPort, localPort,
|
||||
targetDomain, network, protocol, user, vlessRoute, skipDNSResolve)
|
||||
return "out", "rule"
|
||||
end
|
||||
`)
|
||||
ctx := benchmarkRouteContext(net.TCPDestination(net.LocalHostIP, 443))
|
||||
if err := callLuaRoute(L, ctx); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
outboundTag, ruleTag, err := readLuaRouteResult(L)
|
||||
L.Pop(3)
|
||||
if err != nil || outboundTag != "out" || ruleTag != "rule" {
|
||||
b.Fatalf("hook() = %q, %q, %v", outboundTag, ruleTag, err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := callLuaRoute(L, ctx); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
L.Pop(3)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLuaRoute compares equivalent ordered rules on the same session.
|
||||
// rules returns tags only; pick_route uses Router.PickRoute on both sides.
|
||||
// All compilation, matcher construction, and pool startup are outside timing.
|
||||
func BenchmarkLuaRoute(b *testing.B) {
|
||||
for _, name := range []string{"scalar", "ip", "domain", "domain_32_last"} {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
config, script, ctx, wantTag, wantRule := benchmarkRouteFixture(b, name)
|
||||
native := new(Router)
|
||||
if err := native.Init(context.Background(), config, nil, nil, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
L := benchmarkRouteState(b, native, script)
|
||||
|
||||
path := filepath.Join(b.TempDir(), "route.lua")
|
||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
scripted := new(Router)
|
||||
if err := scripted.Init(context.Background(), &Config{Script: path}, nil, nil, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if err := scripted.Start(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() {
|
||||
if err := scripted.Close(); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
for _, bench := range []struct {
|
||||
name string
|
||||
route func() (string, string, error)
|
||||
}{
|
||||
{"rules/native", func() (string, string, error) {
|
||||
rule, _, err := native.pickRouteInternal(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
tag, err := rule.GetTag()
|
||||
return tag, rule.RuleTag, err
|
||||
}},
|
||||
{"rules/lua", func() (string, string, error) {
|
||||
if err := callLuaRoute(L, ctx); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
tag, ruleTag, err := readLuaRouteResult(L)
|
||||
L.Pop(3)
|
||||
return tag, ruleTag, err
|
||||
}},
|
||||
{"pick_route/native", func() (string, string, error) {
|
||||
return benchmarkPickRoute(native, ctx)
|
||||
}},
|
||||
{"pick_route/lua", func() (string, string, error) {
|
||||
return benchmarkPickRoute(scripted, ctx)
|
||||
}},
|
||||
} {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
// Validate and warm both paths before measuring steady state.
|
||||
tag, ruleTag, err := bench.route()
|
||||
if err != nil || tag != wantTag || ruleTag != wantRule {
|
||||
b.Fatalf("route() = %q, %q, %v; want %q, %q", tag, ruleTag, err, wantTag, wantRule)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tag, ruleTag, err = bench.route()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
if tag != wantTag || ruleTag != wantRule {
|
||||
b.Fatalf("route() = %q, %q; want %q, %q", tag, ruleTag, wantTag, wantRule)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkPickRoute(r *Router, ctx routing.Context) (string, string, error) {
|
||||
route, err := r.PickRoute(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return route.GetOutboundTag(), route.GetRuleTag(), nil
|
||||
}
|
||||
|
||||
func benchmarkRouteFixture(b *testing.B, name string) (*Config, string, routing.Context, string, string) {
|
||||
b.Helper()
|
||||
config := new(Config)
|
||||
ctx := benchmarkRouteContext(net.TCPDestination(net.LocalHostIP, 443))
|
||||
prelude := `local router = require("xray.router")
|
||||
local geodata = require("xray.geodata")
|
||||
`
|
||||
body := `if inboundTag == "in" and network == router.NetworkTCP then return "out", "rule" end`
|
||||
wantTag, wantRule := "out", "rule"
|
||||
if name == "scalar" || name == "ip" {
|
||||
rule := &RoutingRule{
|
||||
TargetTag: &RoutingRule_Tag{Tag: wantTag},
|
||||
RuleTag: wantRule,
|
||||
InboundTag: []string{"in"},
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
}
|
||||
if name == "ip" {
|
||||
var err error
|
||||
rule.Ip, err = geodata.ParseIPRules([]string{"127.0.0.0/8"})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
prelude += `local matcher = geodata.BuildIPMatcher("127.0.0.0/8")` + "\n"
|
||||
body = `if inboundTag == "in" and network == router.NetworkTCP and matcher:AnyMatch(ctx:GetTargetIPs()) then return "out", "rule" end`
|
||||
}
|
||||
config.Rule = []*RoutingRule{rule}
|
||||
} else {
|
||||
count := 1
|
||||
if name == "domain_32_last" {
|
||||
count = 32
|
||||
}
|
||||
var rules strings.Builder
|
||||
rules.WriteString("local rules = {\n")
|
||||
for i := 0; i < count; i++ {
|
||||
domain := fmt.Sprintf("route-%d.example.com", i)
|
||||
tag, ruleTag := fmt.Sprintf("out-%d", i), fmt.Sprintf("rule-%d", i)
|
||||
domains, err := geodata.ParseDomainRules([]string{"full:" + domain}, geodata.Domain_Domain)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
config.Rule = append(config.Rule, &RoutingRule{
|
||||
TargetTag: &RoutingRule_Tag{Tag: tag}, RuleTag: ruleTag, Domain: domains,
|
||||
})
|
||||
fmt.Fprintf(&rules, "{geodata.BuildDomainMatcher(%q), %q, %q},\n", "full:"+domain, tag, ruleTag)
|
||||
if i == count-1 {
|
||||
ctx.Outbound.Target = net.TCPDestination(net.DomainAddress(domain), 443)
|
||||
wantTag, wantRule = tag, ruleTag
|
||||
}
|
||||
}
|
||||
rules.WriteString("}\n")
|
||||
prelude += rules.String()
|
||||
body = `for i = 1, #rules do
|
||||
local rule = rules[i]
|
||||
if rule[1]:MatchAny(targetDomain) then return rule[2], rule[3] end
|
||||
end`
|
||||
}
|
||||
script := prelude + `function HandleRoute(ctx, inboundTag, sourcePort, targetPort, localPort,
|
||||
targetDomain, network, protocol, user, vlessRoute, skipDNSResolve)
|
||||
` + body + "\nend\n"
|
||||
return config, script, ctx, wantTag, wantRule
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
routing_session "github.com/xtls/xray-core/features/routing/session"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type luaRouteTestContext struct {
|
||||
*routing_session.Context
|
||||
sourceIPs, targetIPs, localIPs []net.IP
|
||||
}
|
||||
|
||||
func (c *luaRouteTestContext) GetSourceIPs() []net.IP { return c.sourceIPs }
|
||||
func (c *luaRouteTestContext) GetTargetIPs() []net.IP { return c.targetIPs }
|
||||
func (c *luaRouteTestContext) GetLocalIPs() []net.IP { return c.localIPs }
|
||||
|
||||
func newLuaRouteTestContext() *luaRouteTestContext {
|
||||
return &luaRouteTestContext{
|
||||
Context: &routing_session.Context{
|
||||
Inbound: &session.Inbound{
|
||||
Tag: "in", VlessRoute: 4321,
|
||||
Source: net.TCPDestination(net.LocalHostIP, 1234),
|
||||
Local: net.TCPDestination(net.LocalHostIP, 5678),
|
||||
User: &protocol.MemoryUser{Email: "user@example.com"},
|
||||
},
|
||||
Outbound: &session.Outbound{
|
||||
Target: net.TCPDestination(net.LocalHostIP, 443),
|
||||
RouteTarget: net.TCPDestination(net.DomainAddress("MiXeD.Example."), 443),
|
||||
},
|
||||
Content: &session.Content{
|
||||
Protocol: "tls", Attributes: map[string]string{"key": "value"}, SkipDNSResolve: true,
|
||||
},
|
||||
},
|
||||
sourceIPs: []net.IP{{127, 0, 0, 2}},
|
||||
targetIPs: []net.IP{{127, 0, 0, 3}},
|
||||
localIPs: []net.IP{{127, 0, 0, 1}},
|
||||
}
|
||||
}
|
||||
|
||||
func newLuaRouterState(t *testing.T, script string) *lua.LState {
|
||||
t.Helper()
|
||||
r := new(Router)
|
||||
if err := r.Init(context.Background(), &Config{}, nil, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
L := lua.NewState()
|
||||
t.Cleanup(L.Close)
|
||||
r.RegisterLua(L)
|
||||
geodata.RegisterLua(L)
|
||||
if err := L.DoString(script); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return L
|
||||
}
|
||||
|
||||
func TestLuaRouteBinding(t *testing.T) {
|
||||
L := newLuaRouterState(t, `
|
||||
local router = require("xray.router")
|
||||
local matcher = require("xray.geodata").BuildIPMatcher("127.0.0.0/8")
|
||||
assert(router.NetworkUnknown == 0 and router.NetworkTCP == 2)
|
||||
assert(router.NetworkUDP == 3 and router.NetworkUNIX == 4)
|
||||
assert(router.BuildIPMatcher == nil and router.BuildDomainMatcher == nil)
|
||||
function HandleRoute(ctx, inboundTag, sourcePort, targetPort, localPort,
|
||||
targetDomain, network, protocol, user, vlessRoute, skipDNSResolve, ...)
|
||||
assert(select("#", ...) == 0)
|
||||
assert(inboundTag == "in" and sourcePort == 1234 and targetPort == 443 and localPort == 5678)
|
||||
assert(targetDomain == "mixed.example." and network == router.NetworkTCP)
|
||||
assert(protocol == "tls" and user == "user@example.com" and vlessRoute == 4321 and skipDNSResolve)
|
||||
assert(ctx.GetNetwork == nil and ctx.Context == nil)
|
||||
savedContext = ctx
|
||||
sourceIPs, targetIPs, localIPs = ctx:GetSourceIPs(), ctx:GetTargetIPs(), ctx:GetLocalIPs()
|
||||
attributes = ctx:GetAttributes()
|
||||
assert(#sourceIPs == 1 and #targetIPs == 1 and #localIPs == 1)
|
||||
assert(sourceIPs[1]:String() == "127.0.0.2" and targetIPs[1]:String() == "127.0.0.3")
|
||||
assert(localIPs[1]:String() == "127.0.0.1")
|
||||
assert(matcher:Match(sourceIPs[1]) and matcher:Match(targetIPs[1]) and matcher:Match(localIPs[1]))
|
||||
assert(matcher:AnyMatch(sourceIPs) and matcher:AnyMatch(targetIPs) and matcher:AnyMatch(localIPs))
|
||||
local matched = matcher:FilterIPs(targetIPs)
|
||||
assert(#matched == 1 and matched[1]:Equal(targetIPs[1]))
|
||||
assert(attributes.key == "value" and attributes.missing == nil)
|
||||
assert(not pcall(function() attributes.key = "changed" end))
|
||||
return "out", "rule"
|
||||
end`)
|
||||
|
||||
ctx := newLuaRouteTestContext()
|
||||
if err := callLuaRoute(L, ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if L.GetTop() != 3 || L.Get(1) != lua.LString("out") || L.Get(2) != lua.LString("rule") || L.Get(3) != lua.LNil {
|
||||
t.Fatal("callLuaRoute did not leave the three route results on the stack")
|
||||
}
|
||||
if L.GetGlobal("savedContext").(*lua.LUserData).Value != ctx {
|
||||
t.Fatal("routing context was copied")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
want []net.IP
|
||||
}{
|
||||
{"sourceIPs", ctx.sourceIPs},
|
||||
{"targetIPs", ctx.targetIPs},
|
||||
{"localIPs", ctx.localIPs},
|
||||
} {
|
||||
got := L.GetGlobal(tc.name).(*lua.LUserData).Value.([]net.IP)
|
||||
if &got[0] != &tc.want[0] {
|
||||
t.Fatalf("%s storage was copied", tc.name)
|
||||
}
|
||||
}
|
||||
ctx.Content.Attributes["key"] = "updated"
|
||||
L.SetGlobal("expectedOS", lua.LString(runtime.GOOS))
|
||||
if err := L.DoString(`
|
||||
assert(attributes.key == "updated")
|
||||
assert(require("xray.router").LocalOS == expectedOS)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaRouteEmptyIPs(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ips []net.IP
|
||||
}{
|
||||
{"nil", nil},
|
||||
{"empty", []net.IP{}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
L := newLuaRouterState(t, `
|
||||
function HandleRoute(ctx)
|
||||
for _, name in ipairs({"GetSourceIPs", "GetTargetIPs", "GetLocalIPs"}) do
|
||||
local ips = ctx[name](ctx)
|
||||
if expectNil then
|
||||
assert(ips == nil)
|
||||
else
|
||||
assert(type(ips) == "userdata" and #ips == 0)
|
||||
assert(not pcall(function() return ips[1] end))
|
||||
end
|
||||
end
|
||||
return "out"
|
||||
end
|
||||
`)
|
||||
L.SetGlobal("expectNil", lua.LBool(tc.ips == nil))
|
||||
ctx := newLuaRouteTestContext()
|
||||
ctx.sourceIPs, ctx.targetIPs, ctx.localIPs = tc.ips, tc.ips, tc.ips
|
||||
if err := callLuaRoute(L, ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLuaRouteResult(t *testing.T) {
|
||||
nativeErr := go_errors.New("native failure")
|
||||
for _, tc := range []struct {
|
||||
name, values string
|
||||
wantTag, wantRule string
|
||||
wantErr error
|
||||
wantMessage string
|
||||
}{
|
||||
{name: "route", values: `"out", "rule"`, wantTag: "out", wantRule: "rule"},
|
||||
{name: "no match", values: `nil`},
|
||||
{name: "empty tag", values: `""`},
|
||||
{name: "no match ignores rule", values: `nil, false`},
|
||||
{name: "empty tag ignores rule", values: `"", false`},
|
||||
{name: "missing rule", values: `"out"`, wantTag: "out"},
|
||||
{name: "invalid tag", values: `1`, wantMessage: "outboundTag"},
|
||||
{name: "invalid rule", values: `"out", false`, wantMessage: "ruleTag"},
|
||||
{name: "string error", values: `nil, nil, "script failure"`, wantMessage: "script failure"},
|
||||
{name: "native error", values: `nil, nil, nativeError`, wantErr: nativeErr},
|
||||
{name: "error overrides invalid tags", values: `false, false, nativeError`, wantErr: nativeErr},
|
||||
{name: "invalid error", values: `"out", "rule", false`, wantMessage: "error or string"},
|
||||
{name: "wrong error userdata", values: `"out", "rule", wrongError`, wantMessage: "error or string"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
for name, value := range map[string]any{"nativeError": nativeErr, "wrongError": "not a native error"} {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value
|
||||
L.SetGlobal(name, ud)
|
||||
}
|
||||
fn, err := L.LoadString("return " + tc.values)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 3, Protect: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outboundTag, ruleTag, err := readLuaRouteResult(L)
|
||||
if outboundTag != tc.wantTag || ruleTag != tc.wantRule {
|
||||
t.Fatalf("result = %q, %q, %v; want %q, %q", outboundTag, ruleTag, err, tc.wantTag, tc.wantRule)
|
||||
}
|
||||
switch {
|
||||
case tc.wantErr != nil:
|
||||
if err != tc.wantErr {
|
||||
t.Fatalf("error = %v, want original error", err)
|
||||
}
|
||||
case tc.wantMessage != "":
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantMessage) {
|
||||
t.Fatalf("error = %v, want %q", err, tc.wantMessage)
|
||||
}
|
||||
case err != nil:
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallLuaRouteCancellation(t *testing.T) {
|
||||
L := newLuaRouterState(t, `function HandleRoute() while true do end end`)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
L.SetContext(ctx)
|
||||
if err := callLuaRoute(L, &routing_session.Context{}); err == nil {
|
||||
t.Fatal("callLuaRoute did not stop after context cancellation")
|
||||
}
|
||||
if L.Context() != ctx {
|
||||
t.Fatal("callLuaRoute changed the Lua state's context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindProcess(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, network, target string
|
||||
targetPort uint16
|
||||
modify func(*luaRouteTestContext)
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "TCP", network: "tcp", target: "127.0.0.3", targetPort: 443},
|
||||
{name: "UDP", network: "udp", target: "127.0.0.3", targetPort: 443, modify: func(c *luaRouteTestContext) {
|
||||
c.Outbound.Target.Network = net.Network_UDP
|
||||
}},
|
||||
{name: "domain target", network: "tcp", modify: func(c *luaRouteTestContext) { c.targetIPs = nil }},
|
||||
{name: "missing source", modify: func(c *luaRouteTestContext) { c.sourceIPs = nil }, wantErr: true},
|
||||
{name: "unsupported network", modify: func(c *luaRouteTestContext) {
|
||||
c.Outbound.Target.Network = net.Network_UNIX
|
||||
}, wantErr: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := newLuaRouteTestContext()
|
||||
if tc.modify != nil {
|
||||
tc.modify(ctx)
|
||||
}
|
||||
called := false
|
||||
pid, name, path, err := findProcess(ctx, func(network, source string, sourcePort uint16, target string, targetPort uint16) (int, string, string, error) {
|
||||
called = true
|
||||
if network != tc.network || source != "127.0.0.2" || sourcePort != 1234 || target != tc.target || targetPort != tc.targetPort {
|
||||
t.Fatalf("endpoints = %s %s:%d -> %s:%d", network, source, sourcePort, target, targetPort)
|
||||
}
|
||||
return 42, "process", "/path/process", nil
|
||||
})
|
||||
if tc.wantErr {
|
||||
if err == nil || called {
|
||||
t.Fatalf("findProcess = %d, %q, %q, %v", pid, name, path, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || !called || pid != 42 || name != "process" || path != "/path/process" {
|
||||
t.Fatalf("findProcess = %d, %q, %q, %v", pid, name, path, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var _ routing.Context = (*luaRouteTestContext)(nil)
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
type Router struct {
|
||||
domainStrategy Config_DomainStrategy
|
||||
rules atomic.Pointer[[]*Rule]
|
||||
script *scriptEngine
|
||||
balancers atomic.Pointer[map[string]*Balancer]
|
||||
dns dns.Client
|
||||
|
||||
@@ -45,6 +46,14 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
|
||||
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)
|
||||
@@ -52,6 +61,10 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
|
||||
|
||||
// PickRoute implements routing.Router.
|
||||
func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
|
||||
if r.script != nil {
|
||||
return r.script.pickRoute(ctx)
|
||||
}
|
||||
|
||||
originalCtx := ctx
|
||||
rule, ctx, err := r.pickRouteInternal(ctx)
|
||||
if err != nil {
|
||||
@@ -221,6 +234,11 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
|
||||
|
||||
// Start implements common.Runnable.
|
||||
func (r *Router) Start() error {
|
||||
if r.script != nil {
|
||||
if err := r.script.start(r); err != nil {
|
||||
return errors.New("failed to initialize routing script").Base(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -235,6 +253,9 @@ func closeWebhooks(rules []*Rule) {
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (r *Router) Close() error {
|
||||
if r.script != nil {
|
||||
r.script.close()
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
closeWebhooks(*r.rules.Load())
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/app/dns"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const scriptExecutionTimeout = 6 * time.Second
|
||||
|
||||
type scriptEngine struct {
|
||||
program *xlua.Program
|
||||
pool *xlua.Pool
|
||||
}
|
||||
|
||||
func newScriptEngine(script string) (*scriptEngine, error) {
|
||||
program, err := xlua.Compile(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &scriptEngine{program: program}, nil
|
||||
}
|
||||
|
||||
func (e *scriptEngine) start(router *Router) error {
|
||||
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
|
||||
scriptExecutionTimeout*20,
|
||||
256,
|
||||
func(L *lua.LState) {
|
||||
geodata.RegisterLua(L)
|
||||
log.RegisterLua(L)
|
||||
router.RegisterLua(L)
|
||||
dns.RegisterLua(L, router.dns)
|
||||
},
|
||||
func(L *lua.LState) error {
|
||||
if L.GetGlobal("HandleRoute").Type() != lua.LTFunction {
|
||||
return errors.New("routing script must define HandleRoute(...)")
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.pool = pool
|
||||
errors.LogInfo(router.ctx, "routing script initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *scriptEngine) close() {
|
||||
if e.pool != nil {
|
||||
e.pool.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *scriptEngine) pickRoute(ctx routing.Context) (routing.Route, error) {
|
||||
var outboundTag, ruleTag string
|
||||
var routeErr error
|
||||
|
||||
if err := e.pool.WithState(nil, 0, func(L *lua.LState) error {
|
||||
if err := callLuaRoute(L, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
outboundTag, ruleTag, routeErr = readLuaRouteResult(L)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if routeErr != nil {
|
||||
return nil, routeErr
|
||||
}
|
||||
if outboundTag == "" {
|
||||
return nil, common.ErrNoClue
|
||||
}
|
||||
|
||||
return &Route{Context: ctx, outboundTag: outboundTag, ruleTag: ruleTag}, nil
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdnet "net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
wireDNS "github.com/miekg/dns"
|
||||
"github.com/xtls/xray-core/app/dispatcher"
|
||||
appdns "github.com/xtls/xray-core/app/dns"
|
||||
"github.com/xtls/xray-core/app/proxyman"
|
||||
_ "github.com/xtls/xray-core/app/proxyman/outbound"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/core"
|
||||
featureDNS "github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/outbound"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
routing_session "github.com/xtls/xray-core/features/routing/session"
|
||||
"github.com/xtls/xray-core/proxy/blackhole"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
)
|
||||
|
||||
type luaRouteDNSClient struct {
|
||||
featureDNS.Client
|
||||
lookup func(string, featureDNS.IPOption) ([]net.IP, uint32, error)
|
||||
}
|
||||
|
||||
func (d *luaRouteDNSClient) LookupIP(domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
return d.lookup(domain, option)
|
||||
}
|
||||
|
||||
type luaRouteOutboundManager struct{ outbound.Manager }
|
||||
|
||||
func (*luaRouteOutboundManager) Select(selectors []string) []string { return selectors }
|
||||
|
||||
func writeRouteScript(t *testing.T, script string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "route.lua")
|
||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func startLuaRouter(t *testing.T, script string, d featureDNS.Client, config *Config) *Router {
|
||||
t.Helper()
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
config.Script = writeRouteScript(t, script)
|
||||
r := new(Router)
|
||||
if err := r.Init(context.Background(), config, d, &luaRouteOutboundManager{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := r.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func TestRouterScriptStartup(t *testing.T) {
|
||||
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)
|
||||
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
|
||||
wantTag, wantRule string
|
||||
wantErr error
|
||||
wantMessage string
|
||||
wantCalls string
|
||||
}{
|
||||
{name: "route", body: `return "lua-out", "lua-rule"`, wantTag: "lua-out", wantRule: "lua-rule", wantCalls: "2"},
|
||||
{name: "no match", body: `return nil`, wantErr: common.ErrNoClue, wantCalls: "2"},
|
||||
{name: "empty tag", body: `return ""`, wantErr: common.ErrNoClue, wantCalls: "2"},
|
||||
{name: "balancer error", body: `local tag, err = router.PickOutbound("missing"); return tag, nil, err`, wantMessage: "not found", wantCalls: "2"},
|
||||
{name: "string error", body: `return nil, nil, "blocked"`, wantMessage: "blocked", wantCalls: "2"},
|
||||
{name: "invalid tag", body: `return false`, wantMessage: "outboundTag", wantCalls: "2"},
|
||||
{name: "invalid rule", body: `return "lua-out", false`, wantMessage: "ruleTag", wantCalls: "2"},
|
||||
{name: "execution error", body: `error("execution failed")`, wantMessage: "execution failed", wantCalls: "1"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var dnsCalls atomic.Int32
|
||||
d := &luaRouteDNSClient{lookup: func(string, featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
dnsCalls.Add(1)
|
||||
return []net.IP{{1, 2, 3, 4}}, 60, nil
|
||||
}}
|
||||
script := `
|
||||
local router = require("xray.router")
|
||||
local calls = 0
|
||||
function HandleRoute(ctx, inbound)
|
||||
calls = calls + 1
|
||||
if inbound == "count" then return "lua-out", tostring(calls) end
|
||||
` + tc.body + `
|
||||
end
|
||||
`
|
||||
r := startLuaRouter(t, script, d, &Config{
|
||||
DomainStrategy: Config_IpOnDemand,
|
||||
Rule: []*RoutingRule{{
|
||||
TargetTag: &RoutingRule_Tag{Tag: "json-out"},
|
||||
Networks: []net.Network{net.Network_TCP},
|
||||
}},
|
||||
})
|
||||
ctx := newLuaRouteTestContext()
|
||||
ctx.Content.SkipDNSResolve = false
|
||||
route, err := r.PickRoute(ctx)
|
||||
switch {
|
||||
case tc.wantErr != nil:
|
||||
if err != tc.wantErr {
|
||||
t.Fatalf("route error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
case tc.wantMessage != "":
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantMessage) {
|
||||
t.Fatalf("route error = %v, want %q", err, tc.wantMessage)
|
||||
}
|
||||
case err != nil:
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.wantTag == "" {
|
||||
if route != nil {
|
||||
t.Fatalf("route = %v, want nil", route)
|
||||
}
|
||||
} else if route == nil || route.GetOutboundTag() != tc.wantTag || route.GetRuleTag() != tc.wantRule || route.(*Route).Context != ctx {
|
||||
t.Fatalf("route = %v; want %q, %q and original context", route, tc.wantTag, tc.wantRule)
|
||||
}
|
||||
|
||||
ctx.Inbound.Tag = "count"
|
||||
route, err = r.PickRoute(ctx)
|
||||
if err != nil || route == nil || route.GetOutboundTag() != "lua-out" || route.GetRuleTag() != tc.wantCalls {
|
||||
t.Fatalf("next route = %v, %v; want lua-out, calls %s", route, err, tc.wantCalls)
|
||||
}
|
||||
if dnsCalls.Load() != 0 {
|
||||
t.Fatal("script routing implicitly resolved DNS")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterScriptModules(t *testing.T) {
|
||||
ips := []net.IP{{127, 0, 0, 7}}
|
||||
calls := 0
|
||||
d := &luaRouteDNSClient{lookup: func(domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||
calls++
|
||||
if domain != "mixed.example." || !option.IPv4Enable || option.IPv6Enable || !option.FakeEnable {
|
||||
t.Fatalf("dns.Query arguments = %q, %+v", domain, option)
|
||||
}
|
||||
return ips, 17, nil
|
||||
}}
|
||||
r := startLuaRouter(t, `
|
||||
local dns = require("xray.dns")
|
||||
local matcher = require("xray.geodata").BuildIPMatcher("127.0.0.0/8")
|
||||
assert(dns.Servers == nil and type(dns.Query) == "function")
|
||||
assert(type(require("xray.log").Info) == "function")
|
||||
function HandleRoute(ctx, inbound, sourcePort, targetPort, localPort, domain)
|
||||
local ips, ttl, err = dns.Query(domain, true, false, true)
|
||||
assert(not err and ttl == 17)
|
||||
assert(matcher:AnyMatch(ips) and matcher:AnyMatch(ctx:GetTargetIPs()))
|
||||
return "out"
|
||||
end`, d, nil)
|
||||
if _, err := r.PickRoute(newLuaRouteTestContext()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("DNS calls = %d, want 1", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterScriptBalancerReload(t *testing.T) {
|
||||
config := func(tag string) *Config {
|
||||
return &Config{BalancingRule: []*BalancingRule{{
|
||||
Tag: "balance", Strategy: "roundrobin", OutboundSelector: []string{tag},
|
||||
}}}
|
||||
}
|
||||
r := startLuaRouter(t, `
|
||||
local router = require("xray.router")
|
||||
function HandleRoute()
|
||||
local tag, err = router.PickOutbound("balance")
|
||||
return tag, "balanced", err
|
||||
end`, nil, config("old"))
|
||||
pick := func(want string) {
|
||||
t.Helper()
|
||||
route, err := r.PickRoute(&routing_session.Context{})
|
||||
if err != nil || route.GetOutboundTag() != want || route.GetRuleTag() != "balanced" {
|
||||
t.Fatalf("route = %v, %v, want %q", route, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
pick("old")
|
||||
if err := r.SetOverrideTarget("balance", "override"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pick("override")
|
||||
if err := r.SetOverrideTarget("balance", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.ReloadRules(config("new"), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pick("new")
|
||||
}
|
||||
|
||||
func TestRouterScriptConcurrentBalancerReload(t *testing.T) {
|
||||
config := func(tag string) *Config {
|
||||
return &Config{BalancingRule: []*BalancingRule{{
|
||||
Tag: "balance", Strategy: "roundrobin", OutboundSelector: []string{tag},
|
||||
}}}
|
||||
}
|
||||
r := startLuaRouter(t, `
|
||||
local router = require("xray.router")
|
||||
function HandleRoute()
|
||||
local tag, err = router.PickOutbound("balance")
|
||||
return tag, nil, err
|
||||
end`, nil, config("a"))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 4 {
|
||||
wg.Go(func() {
|
||||
for range 20 {
|
||||
route, err := r.PickRoute(&routing_session.Context{})
|
||||
if err != nil {
|
||||
t.Errorf("PickRoute: %v", err)
|
||||
return
|
||||
}
|
||||
if tag := route.GetOutboundTag(); tag != "a" && tag != "b" {
|
||||
t.Errorf("unexpected tag %q", tag)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Go(func() {
|
||||
for range 20 {
|
||||
for _, tag := range []string{"a", "b"} {
|
||||
if err := r.ReloadRules(config(tag), false); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestRouterScriptDNSDispatcherReentry(t *testing.T) {
|
||||
conn, err := stdnet.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port := conn.LocalAddr().(*stdnet.UDPAddr).Port
|
||||
ready, stopped := make(chan struct{}), make(chan error, 1)
|
||||
var queries atomic.Int32
|
||||
server := &wireDNS.Server{
|
||||
PacketConn: conn,
|
||||
NotifyStartedFunc: func() {
|
||||
close(ready)
|
||||
},
|
||||
Handler: wireDNS.HandlerFunc(func(w wireDNS.ResponseWriter, query *wireDNS.Msg) {
|
||||
queries.Add(1)
|
||||
response := new(wireDNS.Msg).SetReply(query)
|
||||
for _, question := range query.Question {
|
||||
if question.Name == "nested.example." && question.Qtype == wireDNS.TypeA {
|
||||
response.Answer = append(response.Answer, &wireDNS.A{
|
||||
Hdr: wireDNS.RR_Header{Name: question.Name, Rrtype: wireDNS.TypeA, Class: wireDNS.ClassINET, Ttl: 60},
|
||||
A: stdnet.IP{127, 0, 0, 7},
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := w.WriteMsg(response); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}),
|
||||
}
|
||||
go func() { stopped <- server.ActivateAndServe() }()
|
||||
defer func() {
|
||||
server.Shutdown()
|
||||
select {
|
||||
case err := <-stopped:
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("DNS server did not stop")
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-ready:
|
||||
case err := <-stopped:
|
||||
t.Fatalf("DNS server startup: %v", err)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("DNS server did not start")
|
||||
}
|
||||
|
||||
dnsScript := writeRouteScript(t, `
|
||||
local server = require("xray.dns").Servers[1]
|
||||
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||
return server:Query(domain, ipv4, ipv6, fake)
|
||||
end`)
|
||||
routerScript := writeRouteScript(t, `
|
||||
local router = require("xray.router")
|
||||
local dns = require("xray.dns")
|
||||
local matcher = require("xray.geodata").BuildIPMatcher("127.0.0.7")
|
||||
local active = false
|
||||
function HandleRoute(ctx, inbound, sourcePort, targetPort, localPort, domain, network,
|
||||
protocol, user, vlessRoute, skipDNSResolve)
|
||||
assert(not active, "borrowed Router VM reentered")
|
||||
if inbound == "dns" then
|
||||
assert(network == router.NetworkUDP and skipDNSResolve == false)
|
||||
return "direct", "dns-route"
|
||||
end
|
||||
active = true
|
||||
local ips, ttl, err = dns.Query("nested.example", true, false, false)
|
||||
assert(not err and matcher:AnyMatch(ips) and active)
|
||||
active = false
|
||||
return "direct", "outer-route"
|
||||
end`)
|
||||
instance, err := core.New(&core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&appdns.Config{
|
||||
Tag: "dns", Script: dnsScript, DisableCache: true,
|
||||
NameServer: []*appdns.NameServer{{
|
||||
Id: "upstream", TimeoutMs: 1000,
|
||||
Address: &net.Endpoint{
|
||||
Network: net.Network_UDP,
|
||||
Address: &net.IPOrDomain{Address: &net.IPOrDomain_Ip{Ip: []byte{127, 0, 0, 1}}},
|
||||
Port: uint32(port),
|
||||
},
|
||||
}},
|
||||
}),
|
||||
serial.ToTypedMessage(&Config{Script: routerScript}),
|
||||
serial.ToTypedMessage(&dispatcher.Config{}),
|
||||
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{Tag: "default", ProxySettings: serial.ToTypedMessage(&blackhole.Config{})},
|
||||
{Tag: "direct", ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
})},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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" {
|
||||
t.Fatalf("nested DNS routing = %v, %v", route, err)
|
||||
}
|
||||
if queries.Load() == 0 {
|
||||
t.Fatal("DNS query did not pass through the dispatcher")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package base64
|
||||
|
||||
const (
|
||||
Prefix = "base64:"
|
||||
GzipPrefix = "base64+gzip:"
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
// Decode decodes Base64 text with an optional [Prefix] or [GzipPrefix].
|
||||
func Decode(value string) ([]byte, error) {
|
||||
value, zip := strings.CutPrefix(value, GzipPrefix)
|
||||
if !zip {
|
||||
value = strings.TrimPrefix(value, Prefix)
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decode base64").Base(err)
|
||||
}
|
||||
if !zip {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decompress gzip").Base(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
data, err = io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decompress gzip").Base(err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeFormats(t *testing.T) {
|
||||
compressed, err := Encode([]byte("x"), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, value, want string
|
||||
}{
|
||||
{"unprefixed", "eA==", "x"},
|
||||
{"base64", Prefix + "eA==", "x"},
|
||||
{"gzip", compressed, "x"},
|
||||
{"empty", Prefix, ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data, err := Decode(tc.value)
|
||||
if err != nil || !bytes.Equal(data, []byte(tc.want)) {
|
||||
t.Fatalf("Decode = %q, %v; want %q", data, err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
// Encode returns prefixed Base64 text, optionally compressed with gzip at the highest level.
|
||||
func Encode(data []byte, zip bool) (string, error) {
|
||||
if !zip {
|
||||
return Prefix + base64.StdEncoding.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to compress gzip").Base(err)
|
||||
}
|
||||
if _, err := writer.Write(data); 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 GzipPrefix + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeFormats(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"empty", nil},
|
||||
{"bytes", []byte("ä˝ ĺĄ˝\r\n\x00\xff")},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, zip := range []bool{false, true} {
|
||||
prefix := Prefix
|
||||
if zip {
|
||||
prefix = GzipPrefix
|
||||
}
|
||||
t.Run(prefix, func(t *testing.T) {
|
||||
value, err := Encode(tc.data, zip)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, ok := strings.CutPrefix(value, prefix)
|
||||
if !ok {
|
||||
t.Fatalf("encoded value does not start with %q", prefix)
|
||||
}
|
||||
// Read the output independently of Decode so matching bugs cannot hide.
|
||||
data, err := base64.StdEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if zip {
|
||||
// gzip's XFL byte identifies the compression level we selected.
|
||||
if len(data) < 10 || data[8] != 2 {
|
||||
t.Fatal("gzip output does not use the highest compression level")
|
||||
}
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
data, err = io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(data, tc.data) {
|
||||
t.Fatal("encoded output does not preserve the input bytes")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,12 @@ import (
|
||||
)
|
||||
|
||||
func checkFile(file, code string) error {
|
||||
r, err := filesystem.OpenAsset(file)
|
||||
r, size, err := filesystem.OpenAsset(file)
|
||||
if err != nil {
|
||||
return errors.New("failed to open ", file).Base(err)
|
||||
}
|
||||
defer r.Close()
|
||||
if _, err := find(r, []byte(code), false); err != nil {
|
||||
if _, err := find(r, []byte(code), false, size); err != nil {
|
||||
return errors.New("failed to check code ", code, " from ", file).Base(err)
|
||||
}
|
||||
return nil
|
||||
@@ -30,12 +30,12 @@ func checkFile(file, code string) error {
|
||||
|
||||
func loadFile(file, code string) ([]byte, error) {
|
||||
runtime.GC() // peak mem
|
||||
r, err := filesystem.OpenAsset(file)
|
||||
r, size, err := filesystem.OpenAsset(file)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to open ", file).Base(err)
|
||||
}
|
||||
defer r.Close()
|
||||
bs, err := find(r, []byte(code), true)
|
||||
bs, err := find(r, []byte(code), true, size)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
|
||||
}
|
||||
@@ -60,13 +60,13 @@ func loadIP(file, code string) ([]*CIDR, error) {
|
||||
// unmarshalling it into a []*Domain, so value is only valid during fn.
|
||||
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
|
||||
runtime.GC() // peak mem
|
||||
r, err := filesystem.OpenAsset(file)
|
||||
r, size, err := filesystem.OpenAsset(file)
|
||||
if err != nil {
|
||||
return errors.New("failed to open ", file).Base(err)
|
||||
}
|
||||
defer r.Close()
|
||||
br := bufio.NewReaderSize(r, 64*1024)
|
||||
n, err := seek(br, []byte(code))
|
||||
n, err := seek(br, []byte(code), size)
|
||||
if err != nil {
|
||||
return errors.New("failed to load code ", code, " from ", file).Base(err)
|
||||
}
|
||||
@@ -114,6 +114,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if shift == 63 && b > 1 {
|
||||
break // Only one value bit fits in the tenth byte.
|
||||
}
|
||||
x |= (uint64(b) & 0x7F) << shift
|
||||
if (b & 0x80) == 0 {
|
||||
return x, nil
|
||||
@@ -123,9 +126,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
|
||||
return 0, errors.New("varint overflow")
|
||||
}
|
||||
|
||||
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
|
||||
func find(r io.Reader, code []byte, readBody bool, size int64) ([]byte, error) {
|
||||
br := bufio.NewReaderSize(r, 64*1024)
|
||||
bodyL, err := seek(br, code)
|
||||
bodyL, err := seek(br, code, size)
|
||||
if err != nil || !readBody {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +140,7 @@ func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
|
||||
}
|
||||
|
||||
// seek advances br to the body of the entry for code and returns the body length.
|
||||
func seek(br *bufio.Reader, code []byte) (int, error) {
|
||||
func seek(br *bufio.Reader, code []byte, size int64) (int, error) {
|
||||
codeL := len(code)
|
||||
if codeL == 0 {
|
||||
return 0, errors.New("empty code")
|
||||
@@ -145,7 +148,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
||||
need := 2 + codeL // TODO: if code too long
|
||||
|
||||
for {
|
||||
if _, err := br.ReadByte(); err != nil {
|
||||
tag, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -153,10 +157,10 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bodyL := int(x)
|
||||
if bodyL <= 0 {
|
||||
return 0, errors.New("invalid body length: ", bodyL)
|
||||
if x == 0 || x > uint64(size) || x > uint64(^uint(0)>>1) {
|
||||
return 0, errors.New("invalid body length: ", x)
|
||||
}
|
||||
bodyL := int(x)
|
||||
|
||||
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single
|
||||
// length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does.
|
||||
@@ -167,7 +171,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if bodyL >= need && len(prefix) >= need && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
|
||||
if tag == 0x0a && bodyL >= need && len(prefix) >= need &&
|
||||
prefix[0] == 0x0a && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
|
||||
return bodyL, nil
|
||||
}
|
||||
if _, err := br.Discard(bodyL); err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -12,6 +13,58 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestFindChecksEntryAndCodeTags(t *testing.T) {
|
||||
for _, payload := range [][]byte{
|
||||
{0x12, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as entry.
|
||||
{0x08, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Wrong entry wire type.
|
||||
{0x0a, 6, 0x1a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as code.
|
||||
{0x0a, 6, 0x08, 4, 'T', 'E', 'S', 'T'}, // Wrong code wire type.
|
||||
} {
|
||||
if _, err := find(bytes.NewReader(payload), []byte("TEST"), true, int64(len(payload))); err == nil {
|
||||
t.Fatalf("matched incorrect entry/code tags in %x", payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileRejectsVarintOverflow(t *testing.T) {
|
||||
payload := []byte{
|
||||
0x0a,
|
||||
0x86, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02, // 2^64 + 6
|
||||
0x0a, 0x04, 'T', 'E', 'S', 'T',
|
||||
}
|
||||
dir := t.TempDir()
|
||||
t.Setenv("xray.location.asset", dir)
|
||||
if err := os.WriteFile(filepath.Join(dir, "overflow.dat"), payload, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := loadFile("overflow.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "varint overflow") {
|
||||
t.Fatalf("want varint overflow, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileRejectsOversizedEntry(t *testing.T) {
|
||||
payload := []byte{
|
||||
0x0a, 0x03, 0x0a, 0x01, 'A', // A valid entry before the oversized one.
|
||||
0x0a, 0x80, 0x80, 0x80, 0x80, 0x04, // Entry length: 1 GiB.
|
||||
0x0a, 0x04, 'T', 'E', 'S', 'T',
|
||||
}
|
||||
dir := t.TempDir()
|
||||
t.Setenv("xray.location.asset", dir)
|
||||
if err := os.WriteFile(filepath.Join(dir, "oversized.dat"), payload, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Check without allocating the body first, so a regression fails safely.
|
||||
if err := checkFile("oversized.dat", "TEST"); err == nil {
|
||||
t.Fatal("accepted an entry length larger than the file")
|
||||
}
|
||||
if _, err := loadFile("oversized.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "invalid body length") {
|
||||
t.Fatalf("want invalid body length, got %v", err)
|
||||
}
|
||||
if err := loadSite("oversized.dat", "TEST", "", func(Domain_Type, []byte) {}); err == nil || !strings.Contains(err.Error(), "invalid body length") {
|
||||
t.Fatalf("want invalid body length, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type siteEntry struct {
|
||||
Type Domain_Type
|
||||
Value string
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// RegisterLua makes xray.geodata available to require in an LState.
|
||||
func RegisterLua(L *lua.LState) {
|
||||
L.PreloadModule("xray.geodata", func(L *lua.LState) int {
|
||||
module := L.CreateTable(0, 2)
|
||||
|
||||
module.RawSetString("BuildDomainMatcher", L.NewFunction(func(L *lua.LState) int {
|
||||
parsed, err := ParseDomainRules(luaRules(L), Domain_Domain)
|
||||
if err != nil {
|
||||
L.RaiseError("%v", err)
|
||||
return 0
|
||||
}
|
||||
matcher, err := DomainReg.BuildDomainMatcher(parsed)
|
||||
if err != nil {
|
||||
L.RaiseError("%v", err)
|
||||
return 0
|
||||
}
|
||||
xlua.PushWithDirectMethods(L, matcher, map[string]xlua.DirectMethod{
|
||||
"Match": newLuaDomainMatch(xlua.NewSlicePusher[uint32](L)),
|
||||
"MatchAny": luaDomainMatchAny,
|
||||
})
|
||||
return 1
|
||||
}))
|
||||
|
||||
module.RawSetString("BuildIPMatcher", L.NewFunction(func(L *lua.LState) int {
|
||||
parsed, err := ParseIPRules(luaRules(L))
|
||||
if err != nil {
|
||||
L.RaiseError("%v", err)
|
||||
return 0
|
||||
}
|
||||
matcher, err := IPReg.BuildIPMatcher(parsed)
|
||||
if err != nil {
|
||||
L.RaiseError("%v", err)
|
||||
return 0
|
||||
}
|
||||
xlua.PushWithDirectMethods(L, matcher, map[string]xlua.DirectMethod{
|
||||
"Match": luaIPMatch,
|
||||
"AnyMatch": luaIPAnyMatch,
|
||||
"Matches": luaIPMatches,
|
||||
"FilterIPs": newLuaIPFilterIPs(xlua.NewReadOnlySlicePusher[net.IP](L)),
|
||||
}, "SetReverse", "ToggleReverse")
|
||||
return 1
|
||||
}))
|
||||
|
||||
L.Push(module)
|
||||
return 1
|
||||
})
|
||||
}
|
||||
|
||||
// Read native Go values by type assertion; slices keep their original storage.
|
||||
func readLuaIPMatcherArgs[T any](L *lua.LState) (IPMatcher, T, bool) {
|
||||
var input T
|
||||
if L.GetTop() != 2 {
|
||||
return nil, input, false
|
||||
}
|
||||
value, ok := L.Get(1).(*lua.LUserData)
|
||||
if !ok {
|
||||
return nil, input, false
|
||||
}
|
||||
matcher, ok := value.Value.(IPMatcher)
|
||||
if !ok {
|
||||
return nil, input, false
|
||||
}
|
||||
if L.Get(2) == lua.LNil {
|
||||
return matcher, input, true
|
||||
}
|
||||
value, ok = L.Get(2).(*lua.LUserData)
|
||||
if !ok {
|
||||
return nil, input, false
|
||||
}
|
||||
input, ok = value.Value.(T)
|
||||
return matcher, input, ok
|
||||
}
|
||||
|
||||
func luaIPMatch(L *lua.LState) (int, bool) {
|
||||
matcher, ip, ok := readLuaIPMatcherArgs[net.IP](L)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
L.Push(lua.LBool(matcher.Match(ip)))
|
||||
return 1, true
|
||||
}
|
||||
|
||||
func luaIPAnyMatch(L *lua.LState) (int, bool) {
|
||||
matcher, ips, ok := readLuaIPMatcherArgs[[]net.IP](L)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
L.Push(lua.LBool(matcher.AnyMatch(ips)))
|
||||
return 1, true
|
||||
}
|
||||
|
||||
func luaIPMatches(L *lua.LState) (int, bool) {
|
||||
matcher, ips, ok := readLuaIPMatcherArgs[[]net.IP](L)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
L.Push(lua.LBool(matcher.Matches(ips)))
|
||||
return 1, true
|
||||
}
|
||||
|
||||
func newLuaIPFilterIPs(pushIPs func(*lua.LState, []net.IP)) xlua.DirectMethod {
|
||||
return func(L *lua.LState) (int, bool) {
|
||||
matcher, ips, ok := readLuaIPMatcherArgs[[]net.IP](L)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
matched, unmatched := matcher.FilterIPs(ips)
|
||||
pushIPs(L, matched)
|
||||
pushIPs(L, unmatched)
|
||||
return 2, true
|
||||
}
|
||||
}
|
||||
|
||||
func newLuaDomainMatch(pushMatches func(*lua.LState, []uint32)) xlua.DirectMethod {
|
||||
return func(L *lua.LState) (int, bool) {
|
||||
if L.GetTop() == 2 {
|
||||
if value, ok := L.Get(1).(*lua.LUserData); ok {
|
||||
matcher, validMatcher := value.Value.(DomainMatcher)
|
||||
domain, validDomain := L.Get(2).(lua.LString)
|
||||
if validMatcher && validDomain {
|
||||
pushMatches(L, matcher.Match(string(domain)))
|
||||
return 1, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func luaDomainMatchAny(L *lua.LState) (int, bool) {
|
||||
if L.GetTop() == 2 {
|
||||
if value, ok := L.Get(1).(*lua.LUserData); ok {
|
||||
matcher, validMatcher := value.Value.(DomainMatcher)
|
||||
domain, validDomain := L.Get(2).(lua.LString)
|
||||
if validMatcher && validDomain {
|
||||
L.Push(lua.LBool(matcher.MatchAny(string(domain))))
|
||||
return 1, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func luaRules(L *lua.LState) []string {
|
||||
rules := make([]string, L.GetTop())
|
||||
for i := range rules {
|
||||
value, ok := L.Get(i + 1).(lua.LString)
|
||||
if !ok {
|
||||
L.RaiseError("geodata rules must be strings")
|
||||
return nil
|
||||
}
|
||||
rules[i] = string(value)
|
||||
}
|
||||
return rules
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestLuaIPMatcher(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
ip := L.NewUserData()
|
||||
ip.Value = net.ParseIP("127.0.0.1")
|
||||
L.SetGlobal("ip", ip)
|
||||
ips := L.NewUserData()
|
||||
ips.Value = []net.IP{ip.Value.(net.IP), net.ParseIP("8.8.8.8")}
|
||||
L.SetGlobal("ips", ips)
|
||||
if err := L.DoString(`
|
||||
local matcher = require("xray.geodata").BuildIPMatcher("127.0.0.0/8", "::1")
|
||||
assert(matcher:Match(ip))
|
||||
assert(matcher:AnyMatch(ips))
|
||||
assert(not matcher:Matches(ips))
|
||||
local matched, unmatched = matcher:FilterIPs(ips)
|
||||
assert(type(matched) == "userdata" and type(unmatched) == "userdata")
|
||||
assert(#matched == 1 and #unmatched == 1)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaDomainMatcher(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
if err := L.DoString(`
|
||||
local matcher = require("xray.geodata").BuildDomainMatcher("example.com", "full:other.com")
|
||||
assert(matcher:MatchAny("example.com"))
|
||||
assert(matcher:MatchAny("www.example.com"))
|
||||
assert(matcher:MatchAny("other.com"))
|
||||
assert(not matcher:MatchAny("www.other.com"))
|
||||
assert(#(matcher:Match("www.example.com")) == 1)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaMatchersRejectInvalidRules(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
script string
|
||||
}{
|
||||
{"IP rule", `require("xray.geodata").BuildIPMatcher("not-an-ip")`},
|
||||
{"non-string domain rule", `require("xray.geodata").BuildDomainMatcher("example.com", true)`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
if err := L.DoString(tc.script); err == nil {
|
||||
t.Fatal("invalid geodata rule was accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaMatcherArgumentsAndAliases(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
ip := L.NewUserData()
|
||||
ip.Value = net.ParseIP("127.0.0.1")
|
||||
L.SetGlobal("ip", ip)
|
||||
if err := L.DoString(`
|
||||
local geodata = require("xray.geodata")
|
||||
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
|
||||
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
|
||||
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
|
||||
assert(matcher.Reload == nil and matcher.reload == nil)
|
||||
assert(matcher:match(ip))
|
||||
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
|
||||
assert(not matcher:AnyMatch(nil))
|
||||
assert(matcher:Matches(nil) == matcher:Matches({}))
|
||||
local matched, unmatched = matcher:FilterIPs({ip})
|
||||
assert(#matched == 1 and matched[1]:Equal(ip))
|
||||
assert(matcher:AnyMatch(matched) and matcher:Matches(matched))
|
||||
local filtered, excluded = matcher:filterIPs(matched)
|
||||
assert(#filtered == 1 and #excluded == 0 and filtered[1]:Equal(ip))
|
||||
local emptyMatched, emptyUnmatched = matcher:FilterIPs(nil)
|
||||
assert(#emptyMatched == 0 and #emptyUnmatched == 0)
|
||||
matcher:SetReverse(true)
|
||||
assert(not matcher:Match(ip) and not matcher:AnyMatch(matched))
|
||||
matcher:ToggleReverse()
|
||||
assert(matcher:Match(ip) and matcher:AnyMatch(matched))
|
||||
assert(matcher.missing == nil)
|
||||
|
||||
local domain = geodata.BuildDomainMatcher("full:example.com")
|
||||
assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny)
|
||||
assert(domain.Reload == nil and domain.reload == nil)
|
||||
for _, value in ipairs({matcher, domain}) do
|
||||
assert(not pcall(function() return -value end))
|
||||
assert(not pcall(function() return value ^ {} end))
|
||||
end
|
||||
assert(domain:matchAny("example.com"))
|
||||
assert(#domain:Match("example.com") == 1)
|
||||
assert(domain:match("example.com")[1] == 0)
|
||||
assert(not pcall(function() matcher:AnyMatch() end))
|
||||
assert(not pcall(function() matcher:AnyMatch(matched, true) end))
|
||||
assert(not pcall(function() matcher.AnyMatch(ip, matched) end))
|
||||
assert(not pcall(function() matcher:Match(true) end))
|
||||
assert(not pcall(function() domain:MatchAny(123) end))
|
||||
assert(not pcall(function() domain:MatchAny("example.com", true) end))
|
||||
assert(not pcall(function() matcher:FilterIPs(true) end))
|
||||
assert(not pcall(function() matcher:FilterIPs(matched, true) end))
|
||||
assert(not pcall(function() domain:Match(123) end))
|
||||
assert(not pcall(function() domain:Match("example.com", true) end))
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLuaMatcherCall measures repeated calls with prebuilt matchers and inputs.
|
||||
func BenchmarkLuaMatcherCall(b *testing.B) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
ip := net.ParseIP("127.0.0.1")
|
||||
for name, value := range map[string]any{"ip": ip, "ips": []net.IP{ip}} {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value
|
||||
L.SetGlobal(name, ud)
|
||||
}
|
||||
if err := L.DoString(`
|
||||
local geodata = require("xray.geodata")
|
||||
ipMatcher = geodata.BuildIPMatcher("127.0.0.0/8")
|
||||
domainMatcher = geodata.BuildDomainMatcher("full:example.com")
|
||||
`); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for _, benchmark := range []struct {
|
||||
name, expression string
|
||||
}{
|
||||
{"ip_match", "ipMatcher:Match(ip)"},
|
||||
{"ip_match_lower", "ipMatcher:match(ip)"},
|
||||
{"ip_any_match", "ipMatcher:AnyMatch(ips)"},
|
||||
{"ip_any_match_lower", "ipMatcher:anyMatch(ips)"},
|
||||
{"ip_matches", "ipMatcher:Matches(ips)"},
|
||||
{"ip_matches_lower", "ipMatcher:matches(ips)"},
|
||||
{"domain_match_any", `domainMatcher:MatchAny("example.com")`},
|
||||
{"domain_match_any_lower", `domainMatcher:matchAny("example.com")`},
|
||||
{"ip_filter", "select(1, ipMatcher:FilterIPs(ips)) ~= nil"},
|
||||
{"ip_filter_lower", "select(1, ipMatcher:filterIPs(ips)) ~= nil"},
|
||||
{"domain_match", `#domainMatcher:Match("example.com") == 1`},
|
||||
{"domain_match_lower", `#domainMatcher:match("example.com") == 1`},
|
||||
{"ip_lua_table", "ipMatcher:AnyMatch({ip})"},
|
||||
{"ip_lua_table_lower", "ipMatcher:anyMatch({ip})"},
|
||||
} {
|
||||
b.Run(benchmark.name, func(b *testing.B) {
|
||||
if err := L.DoString(fmt.Sprintf("function benchmarkMatch() return %s end", benchmark.expression)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
fn := L.GetGlobal("benchmarkMatch")
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 1, Protect: true}); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if L.Get(-1) != lua.LTrue {
|
||||
b.Fatal("matcher returned false")
|
||||
}
|
||||
L.Pop(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// RegisterLua makes xray.log available to require in an LState.
|
||||
func RegisterLua(L *lua.LState) {
|
||||
L.PreloadModule("xray.log", func(L *lua.LState) int {
|
||||
module := L.CreateTable(0, 4)
|
||||
var source, prefix string // cache
|
||||
for name, severity := range map[string]Severity{
|
||||
"Debug": Severity_Debug,
|
||||
"Info": Severity_Info,
|
||||
"Warning": Severity_Warning,
|
||||
"Error": Severity_Error,
|
||||
} {
|
||||
module.RawSetString(name, L.NewFunction(func(L *lua.LState) int {
|
||||
if GetSeverity() < severity {
|
||||
return 0
|
||||
}
|
||||
var content strings.Builder
|
||||
// Prefix with the calling script's filename.
|
||||
if caller, ok := L.GetStack(1); ok {
|
||||
if _, err := L.GetInfo("S", caller, lua.LNil); err == nil && caller.Source != "" {
|
||||
if caller.Source != source {
|
||||
source = caller.Source
|
||||
prefix = filepath.Base(strings.TrimPrefix(source, "@")) + ": "
|
||||
}
|
||||
content.WriteString(prefix)
|
||||
}
|
||||
}
|
||||
for i := 1; i <= L.GetTop(); i++ {
|
||||
content.WriteString(luaLogString(L, L.Get(i)))
|
||||
}
|
||||
Record(&GeneralMessage{
|
||||
Severity: severity,
|
||||
Content: content.String(),
|
||||
})
|
||||
return 0
|
||||
}))
|
||||
}
|
||||
L.Push(module)
|
||||
return 1
|
||||
})
|
||||
}
|
||||
|
||||
func luaLogString(L *lua.LState, value lua.LValue) string {
|
||||
if ud, ok := value.(*lua.LUserData); ok {
|
||||
if err, ok := ud.Value.(error); ok {
|
||||
return err.Error()
|
||||
}
|
||||
}
|
||||
if _, ok := L.GetMetaField(value, "__tostring").(*lua.LFunction); ok {
|
||||
return L.ToStringMeta(value).String()
|
||||
}
|
||||
return value.String()
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type luaLogHandler struct {
|
||||
messages []Message
|
||||
}
|
||||
|
||||
func (h *luaLogHandler) Handle(msg Message) {
|
||||
h.messages = append(h.messages, msg)
|
||||
}
|
||||
|
||||
func TestLuaLog(t *testing.T) {
|
||||
previous := logHandler.Load()
|
||||
t.Cleanup(func() { logHandler.Store(previous) })
|
||||
handler := &luaLogHandler{}
|
||||
RegisterHandler(handler)
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
nativeError := L.NewUserData()
|
||||
nativeError.Value = fmt.Errorf("lookup failed: %w", errors.New("upstream timeout"))
|
||||
L.SetGlobal("nativeError", nativeError)
|
||||
path := filepath.Join(t.TempDir(), "logging.lua")
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
local log = require("xray.log")
|
||||
assert(log == require("xray.log"))
|
||||
log.Debug("query: ", "example.com")
|
||||
log.Info("count=", 42, ", enabled=", true, ", value=", nil)
|
||||
log.Warning(setmetatable({}, {
|
||||
__tostring = function() return "fallback" end
|
||||
}))
|
||||
assert(select("#", log.Error("failed")) == 0)
|
||||
log.Error("DNS failed: ", nativeError)
|
||||
log.Warning(nativeError)
|
||||
local ok, err = pcall(function() error("Lua failure", 0) end)
|
||||
assert(not ok)
|
||||
log.Error(err)
|
||||
local calls = 0
|
||||
local custom = setmetatable({}, {
|
||||
__tostring = function() calls = calls + 1; return "custom" end
|
||||
})
|
||||
log.Info(custom, custom)
|
||||
assert(calls == 2)
|
||||
log.Info("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l")
|
||||
log.Info()
|
||||
function logHook()
|
||||
log.Info("hook")
|
||||
end
|
||||
`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := L.DoFile(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := L.DoString(`
|
||||
logHook()
|
||||
require("xray.log").Info("anonymous")
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other := filepath.Join(t.TempDir(), "other.lua")
|
||||
if err := os.WriteFile(other, []byte(`
|
||||
local log = require("xray.log")
|
||||
log.Info("other")
|
||||
logHook()
|
||||
log.Info("other again")
|
||||
`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := L.DoFile(other); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := []struct {
|
||||
severity Severity
|
||||
message string
|
||||
}{
|
||||
{Severity_Debug, "[Debug] logging.lua: query: example.com"},
|
||||
{Severity_Info, "[Info] logging.lua: count=42, enabled=true, value=nil"},
|
||||
{Severity_Warning, "[Warning] logging.lua: fallback"},
|
||||
{Severity_Error, "[Error] logging.lua: failed"},
|
||||
{Severity_Error, "[Error] logging.lua: DNS failed: lookup failed: upstream timeout"},
|
||||
{Severity_Warning, "[Warning] logging.lua: lookup failed: upstream timeout"},
|
||||
{Severity_Error, "[Error] logging.lua: Lua failure"},
|
||||
{Severity_Info, "[Info] logging.lua: customcustom"},
|
||||
{Severity_Info, "[Info] logging.lua: abcdefghijkl"},
|
||||
{Severity_Info, "[Info] logging.lua: "},
|
||||
{Severity_Info, "[Info] logging.lua: hook"},
|
||||
{Severity_Info, "[Info] <string>: anonymous"},
|
||||
{Severity_Info, "[Info] other.lua: other"},
|
||||
{Severity_Info, "[Info] logging.lua: hook"},
|
||||
{Severity_Info, "[Info] other.lua: other again"},
|
||||
}
|
||||
if len(handler.messages) != len(want) {
|
||||
t.Fatalf("logged %d messages, want %d", len(handler.messages), len(want))
|
||||
}
|
||||
for i, expected := range want {
|
||||
msg, ok := handler.messages[i].(*GeneralMessage)
|
||||
if !ok {
|
||||
t.Fatalf("message %d has type %T, want *GeneralMessage", i, handler.messages[i])
|
||||
}
|
||||
if msg.Severity != expected.severity || msg.String() != expected.message {
|
||||
t.Errorf("message %d = %q with severity %v, want %q with severity %v", i, msg.String(), msg.Severity, expected.message, expected.severity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type luaSeverityLogHandler struct {
|
||||
luaLogHandler
|
||||
level Severity
|
||||
}
|
||||
|
||||
func (h *luaSeverityLogHandler) Severity() Severity { return h.level }
|
||||
|
||||
func TestLuaLogSeverity(t *testing.T) {
|
||||
previous := logHandler.Load()
|
||||
t.Cleanup(func() { logHandler.Store(previous) })
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
for _, level := range []Severity{Severity_Unknown, Severity_Error, Severity_Warning, Severity_Info, Severity_Debug, Severity_Warning} {
|
||||
t.Run(level.String(), func(t *testing.T) {
|
||||
handler := &luaSeverityLogHandler{level: level}
|
||||
RegisterHandler(handler)
|
||||
want := []Severity{}
|
||||
for _, severity := range []Severity{Severity_Error, Severity_Warning, Severity_Info, Severity_Debug} {
|
||||
if severity <= level {
|
||||
want = append(want, severity)
|
||||
}
|
||||
}
|
||||
if err := L.DoString(fmt.Sprintf(`
|
||||
local log = require("xray.log")
|
||||
local calls = 0
|
||||
local value = setmetatable({}, {
|
||||
__tostring = function() calls = calls + 1; return "message" end
|
||||
})
|
||||
for _, write in ipairs({log.Error, log.Warning, log.Info, log.Debug}) do
|
||||
assert(select("#", write(value)) == 0)
|
||||
end
|
||||
assert(calls == %d)
|
||||
`, len(want))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(handler.messages) != len(want) {
|
||||
t.Fatalf("logged %d messages, want %d", len(handler.messages), len(want))
|
||||
}
|
||||
for i, severity := range want {
|
||||
msg := handler.messages[i].(*GeneralMessage)
|
||||
if msg.Severity != severity || msg.Content != "<string>: message" {
|
||||
t.Errorf("message %d = %v, want severity %v and content %q", i, msg, severity, "<string>: message")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type luaDiscardLogHandler struct{ level Severity }
|
||||
|
||||
func (luaDiscardLogHandler) Handle(Message) {}
|
||||
func (h luaDiscardLogHandler) Severity() Severity { return h.level }
|
||||
|
||||
func BenchmarkLuaLog(b *testing.B) {
|
||||
benchmarkLuaLog(b, Severity_Debug)
|
||||
}
|
||||
|
||||
func BenchmarkLuaLogFiltered(b *testing.B) {
|
||||
benchmarkLuaLog(b, Severity_Warning)
|
||||
}
|
||||
|
||||
func benchmarkLuaLog(b *testing.B, level Severity) {
|
||||
previous := logHandler.Load()
|
||||
b.Cleanup(func() { logHandler.Store(previous) })
|
||||
RegisterHandler(luaDiscardLogHandler{level: level})
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
RegisterLua(L)
|
||||
if err := L.DoString(`custom = setmetatable({}, {__tostring = function() return "custom" end})`); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for _, benchmark := range []struct {
|
||||
name, arguments string
|
||||
}{
|
||||
{"strings", `"query: ", "example.com"`},
|
||||
{"mixed", `"count=", 42, ", enabled=", true, ", value=", nil`},
|
||||
{"many_arguments", `"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"`},
|
||||
{"tostring", "custom"},
|
||||
} {
|
||||
b.Run(benchmark.name, func(b *testing.B) {
|
||||
if err := L.DoString(fmt.Sprintf(`local log = require("xray.log")
|
||||
function benchmarkLog() log.Info(%s) end`, benchmark.arguments)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
fn := L.GetGlobal("benchmarkLog")
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 0, Protect: true}); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// DO NOT EDIT. Regression test for app/geodata's luaGuardHeader.
|
||||
|
||||
package lua
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/gopher-lua/parse"
|
||||
)
|
||||
|
||||
func TestGeodataLuaGuard(t *testing.T) {
|
||||
_, err := parse.Parse(strings.NewReader("\x12\x01\x00function HandleRoute() end"), "geodata")
|
||||
var syntaxErr *parse.Error
|
||||
if !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
|
||||
t.Fatalf("want Lua rejection at the first byte, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package lua provides shared GopherLua programs, state management, and value
|
||||
// conversion and validation helpers for Xray scripts.
|
||||
package lua
|
||||
@@ -0,0 +1,156 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
// NewSlicePusher captures luar's slice metatable during state initialization.
|
||||
// The returned function wraps slices without reflection or metatable lookup,
|
||||
// and pushes nil for nil slices. Use it with this state or its coroutines.
|
||||
func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
||||
metatable := luar.MT(L, []T(nil)).LTable
|
||||
return func(L *glua.LState, values []T) {
|
||||
if values == nil {
|
||||
L.Push(glua.LNil)
|
||||
return
|
||||
}
|
||||
userdata := L.NewUserData()
|
||||
userdata.Value = values
|
||||
userdata.Metatable = metatable
|
||||
L.Push(userdata)
|
||||
}
|
||||
}
|
||||
|
||||
// NewReadOnlySlicePusher borrows slices without copying and recursively makes
|
||||
// nested slice types read-only through their shared metatables. IP byte views
|
||||
// and masks are also read-only. Other referenced elements retain their mutability.
|
||||
// Element bindings are initialized on first indexing or iteration.
|
||||
func NewReadOnlySlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
||||
sliceMetatable := luar.MT(L, []T{})
|
||||
if sliceMetatable.RawGetString("__readonly") != glua.LTrue {
|
||||
readOnly := L.NewFunction(func(L *glua.LState) int {
|
||||
L.RaiseError("slice is read-only")
|
||||
return 0
|
||||
})
|
||||
setSliceReadOnly[[]T](L, readOnly)
|
||||
}
|
||||
return NewSlicePusher[T](L)
|
||||
}
|
||||
|
||||
func setSliceReadOnly[S ~[]E, E any](L *glua.LState, readOnly *glua.LFunction) {
|
||||
var visit func(reflect.Type)
|
||||
visit = func(typ reflect.Type) {
|
||||
if typ.Kind() != reflect.Slice {
|
||||
return
|
||||
}
|
||||
metatable := luar.MT(L, reflect.Zero(typ).Interface())
|
||||
if metatable.RawGetString("__readonly") == glua.LTrue {
|
||||
return
|
||||
}
|
||||
// Mark before descending because named slice types can be recursive.
|
||||
metatable.RawSetString("__readonly", glua.LTrue)
|
||||
metatable.RawSetString("__newindex", readOnly)
|
||||
// Reject explicitly so Lua cannot fall back to the other operand.
|
||||
metatable.RawSetString("__add", readOnly)
|
||||
if typ == reflect.TypeFor[net.IP]() {
|
||||
// A nil receiver lets AppendText return its argument as a writable []byte.
|
||||
methods := metatable.RawGetString("methods").(*glua.LTable)
|
||||
appendText := methods.RawGetString("AppendText")
|
||||
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
|
||||
if method == appendText {
|
||||
methods.RawSet(key, glua.LNil)
|
||||
}
|
||||
}
|
||||
visit(reflect.TypeFor[net.IPMask]())
|
||||
}
|
||||
if element := typ.Elem(); element.Kind() == reflect.Slice {
|
||||
index := metatable.RawGetString("__index").(*glua.LFunction)
|
||||
call := metatable.RawGetString("__call").(*glua.LFunction)
|
||||
initialize := func() {
|
||||
visit(element)
|
||||
metatable.RawSetString("__index", index)
|
||||
metatable.RawSetString("__call", call)
|
||||
}
|
||||
metatable.RawSetString("__index", L.NewFunction(func(L *glua.LState) int {
|
||||
initialize()
|
||||
return index.GFunction(L)
|
||||
}))
|
||||
metatable.RawSetString("__call", L.NewFunction(func(L *glua.LState) int {
|
||||
initialize()
|
||||
return call.GFunction(L)
|
||||
}))
|
||||
}
|
||||
}
|
||||
visit(reflect.TypeFor[S]())
|
||||
}
|
||||
|
||||
// DirectMethod handles a Lua call without luar's reflected method invocation.
|
||||
// It returns the result count and whether it handled the arguments. On false,
|
||||
// it must leave the stack unchanged for the original luar wrapper.
|
||||
type DirectMethod func(L *glua.LState) (nresults int, handled bool)
|
||||
|
||||
// PushMethods pushes a luar userdata exposing only the named Go methods and
|
||||
// their aliases through reflected calls. The method list is fixed by the first
|
||||
// binding of that Go type in each LState.
|
||||
func PushMethods(L *glua.LState, value any, methods ...string) {
|
||||
PushWithDirectMethods(L, value, nil, methods...)
|
||||
}
|
||||
|
||||
// PushWithDirectMethods pushes a luar userdata with typed Go method bindings.
|
||||
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
|
||||
// uses the methods table directly instead of luar's reflected __index handler.
|
||||
// Only directMethods and additionalMethods are exposed, including their aliases.
|
||||
// Bindings and their closures are installed once per Go type per LState, outside
|
||||
// the method-call hot path.
|
||||
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod, additionalMethods ...string) {
|
||||
userdata := luar.New(L, value).(*glua.LUserData)
|
||||
metatable := userdata.Metatable.(*glua.LTable)
|
||||
methods := metatable.RawGetString("methods").(*glua.LTable)
|
||||
if metatable.RawGetString("__index") != methods {
|
||||
// Keep all aliases for the explicitly exposed methods.
|
||||
exposed := make(map[glua.LValue]bool, len(directMethods)+len(additionalMethods))
|
||||
for name := range directMethods {
|
||||
exposed[methods.RawGetString(name)] = true
|
||||
}
|
||||
for _, name := range additionalMethods {
|
||||
exposed[methods.RawGetString(name)] = true
|
||||
}
|
||||
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
|
||||
if !exposed[method] {
|
||||
methods.RawSet(key, glua.LNil)
|
||||
}
|
||||
}
|
||||
for name, direct := range directMethods {
|
||||
original := methods.RawGetString(name)
|
||||
fn := L.NewFunction(func(L *glua.LState) int {
|
||||
if nresults, handled := direct(L); handled {
|
||||
return nresults
|
||||
}
|
||||
return callLuarMethod(L, original)
|
||||
})
|
||||
// Keep luar's method aliases on the same direct binding.
|
||||
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
|
||||
if method == original {
|
||||
methods.RawSet(key, fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pointer operations would bypass the method-only API.
|
||||
metatable.RawSetString("__newindex", glua.LNil)
|
||||
metatable.RawSetString("__pow", glua.LNil)
|
||||
metatable.RawSetString("__unm", glua.LNil)
|
||||
metatable.RawSetString("__index", methods)
|
||||
}
|
||||
L.Push(userdata)
|
||||
}
|
||||
|
||||
func callLuarMethod(L *glua.LState, method glua.LValue) int {
|
||||
nargs := L.GetTop()
|
||||
L.Insert(method, 1)
|
||||
L.Call(nargs, glua.MultRet)
|
||||
return L.GetTop()
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
func TestSlicePusher(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
push := NewSlicePusher[int](L)
|
||||
values := []int{3, 5}
|
||||
L.SetGlobal("getValues", L.NewFunction(func(L *glua.LState) int {
|
||||
push(L, values)
|
||||
return 1
|
||||
}))
|
||||
if err := L.DoString(`
|
||||
local values = getValues()
|
||||
assert(#values == 2 and values[1] == 3 and values[2] == 5)
|
||||
values[2] = 7
|
||||
local co = coroutine.create(function()
|
||||
local values = getValues()
|
||||
assert(#values == 2 and values[1] == 3 and values[2] == 7)
|
||||
return true
|
||||
end)
|
||||
local ok, result = coroutine.resume(co)
|
||||
assert(ok and result == true)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values[1] != 7 {
|
||||
t.Fatal("slice storage was copied")
|
||||
}
|
||||
push(L, nil)
|
||||
if L.Get(-1) != glua.LNil {
|
||||
t.Fatal("nil slice must push Lua nil")
|
||||
}
|
||||
L.Pop(1)
|
||||
push(L, []int{})
|
||||
L.SetGlobal("empty", L.Get(-1))
|
||||
L.Pop(1)
|
||||
if err := L.DoString(`assert(type(empty) == "userdata" and #empty == 0)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlySlicePusher(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
values := []int{3, 5}
|
||||
NewReadOnlySlicePusher[int](L)(L, values)
|
||||
L.SetGlobal("values", L.Get(-1))
|
||||
L.Pop(1)
|
||||
if err := L.DoString(`
|
||||
assert(#values == 2 and values[1] == 3 and values[2] == 5)
|
||||
assert(not pcall(function() values[1] = 7 end))
|
||||
assert(not pcall(function() return values + 7 end))
|
||||
local sum = 0
|
||||
for _, value in values() do sum = sum + value end
|
||||
assert(sum == 8)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := L.GetGlobal("values").(*glua.LUserData).Value.([]int)
|
||||
if &got[0] != &values[0] || values[0] != 3 || values[1] != 5 {
|
||||
t.Fatal("read-only binding copied or modified the slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyIPSlicePusher(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
ips := []net.IP{{192, 0, 2, 1}, net.ParseIP("2001:db8::1")}
|
||||
NewReadOnlySlicePusher[net.IP](L)(L, ips)
|
||||
L.SetGlobal("ips", L.Get(-1))
|
||||
L.Pop(1)
|
||||
readOnly := luar.MT(L, ips).RawGetString("__newindex")
|
||||
NewReadOnlySlicePusher[net.IP](L)
|
||||
if luar.MT(L, ips).RawGetString("__newindex") != readOnly {
|
||||
t.Fatal("IP metatables were initialized more than once")
|
||||
}
|
||||
if err := L.DoString(`
|
||||
local function readOnly(value)
|
||||
assert(not pcall(function() value[1] = value[1] end))
|
||||
assert(not pcall(function() return value + value[1] end))
|
||||
end
|
||||
assert(#ips == 2 and ips[1]:String() == "192.0.2.1")
|
||||
readOnly(ips)
|
||||
for _, ip in ips() do
|
||||
readOnly(ip)
|
||||
readOnly(ip:To16())
|
||||
if ip:To4() then
|
||||
readOnly(ip:To4())
|
||||
readOnly(ip:DefaultMask())
|
||||
end
|
||||
assert(ip:Equal(ip:To16()) and type(ip:String()) == "string")
|
||||
assert(ip.AppendText == nil and ip.appendText == nil)
|
||||
local text, err = ip:MarshalText()
|
||||
assert(not err)
|
||||
text[1] = 88 -- Independently owned byte buffers remain writable.
|
||||
assert(not pcall(function() return ips + text end))
|
||||
end
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := L.GetGlobal("ips").(*glua.LUserData).Value.([]net.IP)
|
||||
if &got[0] != &ips[0] || &got[0][0] != &ips[0][0] || &got[1][0] != &ips[1][0] {
|
||||
t.Fatal("read-only binding copied the IP data")
|
||||
}
|
||||
for i, want := range []string{"192.0.2.1", "2001:db8::1"} {
|
||||
if got[i].String() != want {
|
||||
t.Fatalf("IP %d changed to %s; want %s", i, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyIPSlicePusherLazy(t *testing.T) {
|
||||
for _, tc := range []struct{ name, access string }{
|
||||
{"index", `local ip = ips[1]`},
|
||||
{"iterate", `local _, ip = ips()()`},
|
||||
{"coroutine", `local co = coroutine.create(function() return ips[1] end); local ok, ip = coroutine.resume(co); assert(ok)`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
methods := 0
|
||||
luar.GetConfig(L).MethodNames = func(_ reflect.Type, method reflect.Method) []string {
|
||||
methods++
|
||||
return []string{method.Name}
|
||||
}
|
||||
NewReadOnlySlicePusher[net.IP](L)(L, []net.IP{{192, 0, 2, 1}})
|
||||
L.SetGlobal("ips", L.Get(-1))
|
||||
L.Pop(1)
|
||||
if err := L.DoString(`assert(#ips == 1); assert(not pcall(function() ips[1] = nil end))`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if methods != 0 {
|
||||
t.Fatal("IP methods initialized without reading an element")
|
||||
}
|
||||
source := tc.access + `
|
||||
assert(ip:String() == "192.0.2.1" and ip.AppendText == nil)
|
||||
assert(not pcall(function() ip[1] = 0 end))
|
||||
assert(not pcall(function() ip:To4()[1] = 0 end))
|
||||
assert(not pcall(function() ip:To16()[1] = 0 end))
|
||||
assert(not pcall(function() ip:DefaultMask()[1] = 0 end))
|
||||
`
|
||||
if err := L.DoString(source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
initialized := methods
|
||||
if initialized == 0 {
|
||||
t.Fatal("IP access did not initialize its methods")
|
||||
}
|
||||
if err := L.DoString(source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if methods != initialized {
|
||||
t.Fatal("IP methods initialized more than once")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyNestedSlicePusher(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
values := [][][]byte{{{3, 5}}}
|
||||
NewReadOnlySlicePusher[[][]byte](L)(L, values)
|
||||
L.SetGlobal("values", L.Get(-1))
|
||||
L.Pop(1)
|
||||
ips := [][]net.IP{{{192, 0, 2, 1}}}
|
||||
NewReadOnlySlicePusher[[]net.IP](L)(L, ips)
|
||||
L.SetGlobal("ips", L.Get(-1))
|
||||
L.Pop(1)
|
||||
if err := L.DoString(`
|
||||
local function readOnly(value)
|
||||
assert(not pcall(function() value[1] = value[1] end))
|
||||
assert(not pcall(function() return value + value[1] end))
|
||||
end
|
||||
assert(values[1][1][1] == 3 and values[1][1][2] == 5)
|
||||
readOnly(values)
|
||||
for _, group in values() do
|
||||
readOnly(group)
|
||||
for _, bytes in group() do readOnly(bytes) end
|
||||
end
|
||||
readOnly(ips)
|
||||
readOnly(ips[1])
|
||||
local ip = ips[1][1]
|
||||
assert(ip:String() == "192.0.2.1")
|
||||
readOnly(ip)
|
||||
readOnly(ip:To4())
|
||||
readOnly(ip:To16())
|
||||
readOnly(ip:DefaultMask())
|
||||
assert(ip.AppendText == nil and ip.appendText == nil)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
readOnly := luar.MT(L, ips[0]).RawGetString("__newindex")
|
||||
NewReadOnlySlicePusher[net.IP](L)
|
||||
if luar.MT(L, ips[0]).RawGetString("__newindex") != readOnly {
|
||||
t.Fatal("nested slice metatable was initialized again")
|
||||
}
|
||||
got := L.GetGlobal("values").(*glua.LUserData).Value.([][][]byte)
|
||||
if &got[0][0][0] != &values[0][0][0] || values[0][0][0] != 3 || values[0][0][1] != 5 {
|
||||
t.Fatal("read-only binding copied or modified the nested slice")
|
||||
}
|
||||
}
|
||||
|
||||
type recursiveReadOnlySlice []recursiveReadOnlySlice
|
||||
|
||||
func TestReadOnlyRecursiveSlicePusher(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
values := make(recursiveReadOnlySlice, 1)
|
||||
values[0] = values
|
||||
NewReadOnlySlicePusher[recursiveReadOnlySlice](L)(L, values)
|
||||
L.SetGlobal("values", L.Get(-1))
|
||||
L.Pop(1)
|
||||
if err := L.DoString(`
|
||||
local nested = values[1][1][1]
|
||||
assert(#nested == 1)
|
||||
assert(not pcall(function() nested[1] = nested end))
|
||||
assert(not pcall(function() return nested + nested end))
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlicePusherMetatablePerState(t *testing.T) {
|
||||
first := glua.NewState()
|
||||
defer first.Close()
|
||||
second := glua.NewState()
|
||||
defer second.Close()
|
||||
NewSlicePusher[int](first)(first, []int{1})
|
||||
NewSlicePusher[int](second)(second, []int{1})
|
||||
if first.Get(-1).(*glua.LUserData).Metatable == second.Get(-1).(*glua.LUserData).Metatable {
|
||||
t.Fatal("independent states share a slice metatable")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSlicePusher(b *testing.B) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
ips := []net.IP{net.ParseIP("127.0.0.1")}
|
||||
pushIPs := NewSlicePusher[net.IP](L)
|
||||
for _, benchmark := range []struct {
|
||||
name string
|
||||
push func(*glua.LState, []net.IP)
|
||||
}{
|
||||
{"bare", func(L *glua.LState, ips []net.IP) { PushUserData(L, ips) }},
|
||||
{"luar", func(L *glua.LState, ips []net.IP) { L.Push(luar.New(L, ips)) }},
|
||||
{"cached", pushIPs},
|
||||
} {
|
||||
b.Run(benchmark.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmark.push(L, ips)
|
||||
L.Pop(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const maxIdleStates = 16
|
||||
|
||||
// Pool lends each state to one caller at a time. It grows on contention and
|
||||
// keeps up to maxIdleStates idle states until Close. Acquire/Release callers
|
||||
// decide reusability; WithState uses its callback's error.
|
||||
type Pool struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
timeout time.Duration
|
||||
|
||||
factory LStateFactory
|
||||
idle []*glua.LState
|
||||
top int
|
||||
|
||||
mu sync.Mutex
|
||||
active sync.WaitGroup
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewPool tests the factory by creating one state during initialization.
|
||||
func NewPool(ctx context.Context, timeout time.Duration, factory LStateFactory) (*Pool, error) {
|
||||
if timeout <= 0 {
|
||||
return nil, errors.New("Lua pool timeout must be positive")
|
||||
}
|
||||
|
||||
poolCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
state, err := factory(poolCtx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Pool{ctx: poolCtx, cancel: cancel, timeout: timeout, factory: factory, idle: []*glua.LState{state}, top: state.GetTop()}, nil
|
||||
}
|
||||
|
||||
// Acquire returns an initialized exclusive state, growing the pool if necessary.
|
||||
// ctx is passed to the factory for state creation; nil uses the pool context.
|
||||
func (p *Pool) Acquire(ctx context.Context) (*glua.LState, error) {
|
||||
p.mu.Lock()
|
||||
if p.closed {
|
||||
p.mu.Unlock()
|
||||
return nil, errors.New("Lua pool is closed")
|
||||
}
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
p.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = p.ctx
|
||||
} else if err := ctx.Err(); err != nil {
|
||||
p.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.active.Add(1)
|
||||
|
||||
n := len(p.idle)
|
||||
if n != 0 {
|
||||
state := p.idle[n-1]
|
||||
p.idle[n-1] = nil
|
||||
p.idle = p.idle[:n-1]
|
||||
p.mu.Unlock()
|
||||
return state, nil
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
// TODO: Limit the total number of states. When the limit is reached, wait
|
||||
// for a Release instead of creating another state; allow the wait to be
|
||||
// cancelled by the caller or by Close.
|
||||
state, err := p.factory(ctx)
|
||||
if err != nil {
|
||||
p.active.Done()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// WithState runs work on an exclusive state and releases it afterward.
|
||||
// Nil ctx and zero timeout use pool defaults. The timeout starts after acquisition.
|
||||
func (p *Pool) WithState(ctx context.Context, timeout time.Duration, work func(*glua.LState) error) error {
|
||||
state, err := p.Acquire(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = p.ctx
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = p.timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
state.SetContext(ctx)
|
||||
reusable := false
|
||||
defer func() {
|
||||
cancel()
|
||||
p.Release(state, reusable)
|
||||
}()
|
||||
err = work(state)
|
||||
reusable = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Release resets a state for reuse or closes it.
|
||||
func (p *Pool) Release(state *glua.LState, reusable bool) {
|
||||
if reusable {
|
||||
state.RemoveContext()
|
||||
state.SetTop(p.top)
|
||||
p.mu.Lock()
|
||||
if !p.closed && p.ctx.Err() == nil && len(p.idle) < maxIdleStates {
|
||||
p.idle = append(p.idle, state)
|
||||
} else {
|
||||
reusable = false
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
if !reusable {
|
||||
state.Close()
|
||||
}
|
||||
|
||||
p.active.Done()
|
||||
}
|
||||
|
||||
// Close cancels the pool context, closes idle states, and waits for borrowed states.
|
||||
func (p *Pool) Close() {
|
||||
p.mu.Lock()
|
||||
if !p.closed {
|
||||
p.closed = true
|
||||
p.cancel()
|
||||
for _, state := range p.idle {
|
||||
state.Close()
|
||||
}
|
||||
p.idle = nil
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
p.active.Wait()
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func newTestPool(t testing.TB, ctx context.Context, timeout time.Duration, factory LStateFactory) *Pool {
|
||||
t.Helper()
|
||||
pool, err := NewPool(ctx, timeout, factory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
return pool
|
||||
}
|
||||
|
||||
func assertPoolCloseBlocked(t *testing.T, done <-chan struct{}) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatal("Close returned while work was still active")
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolTimeoutValidation(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
timeout time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
{"zero", 0, true},
|
||||
{"negative", -time.Nanosecond, true},
|
||||
{"positive", time.Nanosecond, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
called := false
|
||||
pool, err := NewPool(context.Background(), tc.timeout, func(context.Context) (*glua.LState, error) {
|
||||
called = true
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
if pool != nil {
|
||||
t.Cleanup(pool.Close)
|
||||
}
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("NewPool error = %v, want error %t", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr && (pool != nil || called) {
|
||||
t.Fatal("invalid timeout created a pool or called the factory")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolFactoryFailure(t *testing.T) {
|
||||
failure := errors.New("factory failed")
|
||||
_, err := NewPool(context.Background(), time.Second, func(context.Context) (*glua.LState, error) {
|
||||
return nil, failure
|
||||
})
|
||||
if !errors.Is(err, failure) {
|
||||
t.Fatalf("NewPool error = %v, want original factory error", err)
|
||||
}
|
||||
|
||||
calls := 0
|
||||
pool := newTestPool(t, context.Background(), time.Second, func(context.Context) (*glua.LState, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return glua.NewState(), nil
|
||||
}
|
||||
return nil, failure
|
||||
})
|
||||
state, err := pool.Acquire(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Release(state, true)
|
||||
err = pool.WithState(nil, 0, func(*glua.LState) error {
|
||||
t.Error("work ran after factory failure")
|
||||
return nil
|
||||
})
|
||||
if !errors.Is(err, failure) {
|
||||
t.Fatalf("WithState error = %v, want original factory error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolReusesStatesAndLimitsIdle(t *testing.T) {
|
||||
created := 0
|
||||
pool := newTestPool(t, context.Background(), time.Second, func(context.Context) (*glua.LState, error) {
|
||||
created++
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
var borrowed []*glua.LState
|
||||
defer func() {
|
||||
for _, state := range borrowed {
|
||||
pool.Release(state, false)
|
||||
}
|
||||
}()
|
||||
for range maxIdleStates + 3 {
|
||||
state, err := pool.Acquire(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
borrowed = append(borrowed, state)
|
||||
state.SetContext(context.Background())
|
||||
}
|
||||
states := borrowed
|
||||
for _, state := range states {
|
||||
pool.Release(state, true)
|
||||
}
|
||||
borrowed = nil
|
||||
open := 0
|
||||
for _, state := range states {
|
||||
if !state.IsClosed() {
|
||||
if state.Context() != nil {
|
||||
t.Fatal("Release left a context on a reusable state")
|
||||
}
|
||||
open++
|
||||
}
|
||||
}
|
||||
if open != maxIdleStates {
|
||||
t.Fatalf("retained %d states, want %d", open, maxIdleStates)
|
||||
}
|
||||
if err := pool.WithState(nil, 0, func(*glua.LState) error { return nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if created != len(states) {
|
||||
t.Fatalf("created %d states, want %d", created, len(states))
|
||||
}
|
||||
pool.Close()
|
||||
for _, state := range states {
|
||||
if !state.IsClosed() {
|
||||
t.Fatal("Close left an idle state open")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolWithStateOptions(t *testing.T) {
|
||||
key := struct{}{}
|
||||
parent := context.WithValue(context.Background(), key, "pool")
|
||||
caller := context.WithValue(context.Background(), key, "caller")
|
||||
pool := newTestPool(t, parent, time.Second, func(context.Context) (*glua.LState, error) {
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
wantValue string
|
||||
wantTimeout time.Duration
|
||||
}{
|
||||
{"defaults", nil, 0, "pool", time.Second},
|
||||
{"context", caller, 0, "caller", time.Second},
|
||||
{"timeout", nil, 2 * time.Second, "pool", 2 * time.Second},
|
||||
{"both", caller, 2 * time.Second, "caller", 2 * time.Second},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
started := time.Now()
|
||||
err := pool.WithState(tc.ctx, tc.timeout, func(L *glua.LState) error {
|
||||
ctx := L.Context()
|
||||
if ctx.Value(key) != tc.wantValue {
|
||||
t.Errorf("context value = %v, want %q", ctx.Value(key), tc.wantValue)
|
||||
}
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok || deadline.Before(started.Add(tc.wantTimeout)) || deadline.After(time.Now().Add(tc.wantTimeout)) {
|
||||
t.Errorf("deadline = %v, want timeout %v", deadline, tc.wantTimeout)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolFactoryContext(t *testing.T) {
|
||||
caller, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
}{
|
||||
{"default", nil},
|
||||
{"caller", caller},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var contexts []context.Context
|
||||
pool := newTestPool(t, context.Background(), time.Second, func(ctx context.Context) (*glua.LState, error) {
|
||||
contexts = append(contexts, ctx)
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
state, err := pool.Acquire(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Release(state, true)
|
||||
if err := pool.WithState(tc.ctx, 2*time.Second, func(*glua.LState) error { return nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := tc.ctx
|
||||
if want == nil {
|
||||
want = pool.ctx
|
||||
}
|
||||
if len(contexts) != 2 || contexts[0] != pool.ctx || contexts[1] != want {
|
||||
t.Fatal("factory did not receive the initialization and acquisition contexts unchanged")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolWithStateLifecycle(t *testing.T) {
|
||||
failure := errors.New("work failed")
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
work func(*glua.LState, context.CancelFunc) error
|
||||
reusable bool
|
||||
wantPanic bool
|
||||
wantErr error
|
||||
}{
|
||||
{"success", func(*glua.LState, context.CancelFunc) error { return nil }, true, false, nil},
|
||||
{"canceled success", func(_ *glua.LState, cancel context.CancelFunc) error {
|
||||
cancel()
|
||||
return nil
|
||||
}, true, false, nil},
|
||||
{"error", func(*glua.LState, context.CancelFunc) error { return failure }, false, false, failure},
|
||||
{"timeout", func(L *glua.LState, _ context.CancelFunc) error { return L.DoString("while true do end") }, false, false, nil},
|
||||
{"panic", func(*glua.LState, context.CancelFunc) error { panic(failure) }, false, true, nil},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
pool := newTestPool(t, context.Background(), 10*time.Millisecond, func(context.Context) (*glua.LState, error) {
|
||||
state := glua.NewState()
|
||||
state.Push(glua.LTrue)
|
||||
return state, nil
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var state *glua.LState
|
||||
var workCtx context.Context
|
||||
var recovered any
|
||||
err := func() (err error) {
|
||||
defer func() { recovered = recover() }()
|
||||
return pool.WithState(ctx, 0, func(L *glua.LState) error {
|
||||
state, workCtx = L, L.Context()
|
||||
L.Push(glua.LFalse)
|
||||
return tc.work(L, cancel)
|
||||
})
|
||||
}()
|
||||
if tc.wantPanic {
|
||||
if recovered != failure {
|
||||
t.Fatalf("panic = %v, want original panic", recovered)
|
||||
}
|
||||
} else {
|
||||
if recovered != nil || (err == nil) != tc.reusable {
|
||||
t.Fatalf("WithState error = %v, panic = %v", err, recovered)
|
||||
}
|
||||
if tc.wantErr != nil && !errors.Is(err, tc.wantErr) {
|
||||
t.Fatalf("WithState error = %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
}
|
||||
if workCtx.Err() == nil {
|
||||
t.Fatal("WithState did not cancel the execution context")
|
||||
}
|
||||
if closed := state.IsClosed(); closed == tc.reusable {
|
||||
t.Fatalf("state closed = %t, want %t", closed, !tc.reusable)
|
||||
}
|
||||
if tc.reusable && (state.Context() != nil || state.GetTop() != 1 || state.Get(1) != glua.LTrue) {
|
||||
t.Fatal("WithState did not reset the state for reuse")
|
||||
}
|
||||
if err := pool.WithState(nil, 0, func(L *glua.LState) error {
|
||||
if (L == state) != tc.reusable {
|
||||
t.Error("unexpected state reuse")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolClose(t *testing.T) {
|
||||
pool := newTestPool(t, context.Background(), time.Second, func(context.Context) (*glua.LState, error) {
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
finishCtx, finish := context.WithCancel(context.Background())
|
||||
t.Cleanup(finish)
|
||||
started, done := make(chan *glua.LState, 1), make(chan error, 1)
|
||||
var workCtx context.Context
|
||||
go func() {
|
||||
done <- pool.WithState(nil, 0, func(L *glua.LState) error {
|
||||
workCtx = L.Context()
|
||||
started <- L
|
||||
<-finishCtx.Done()
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
var state *glua.LState
|
||||
select {
|
||||
case state = <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("WithState did not start")
|
||||
}
|
||||
closed := make(chan struct{})
|
||||
go func() {
|
||||
pool.Close()
|
||||
close(closed)
|
||||
}()
|
||||
select {
|
||||
case <-workCtx.Done():
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Close did not cancel work using the pool context")
|
||||
}
|
||||
if !errors.Is(workCtx.Err(), context.Canceled) {
|
||||
t.Fatalf("work context error = %v, want context.Canceled", workCtx.Err())
|
||||
}
|
||||
assertPoolCloseBlocked(t, closed)
|
||||
finish()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("successful work returned an error: %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("WithState did not finish")
|
||||
}
|
||||
select {
|
||||
case <-closed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Close did not finish after WithState")
|
||||
}
|
||||
if !state.IsClosed() {
|
||||
t.Fatal("Release returned a state to a closed pool")
|
||||
}
|
||||
if state, err := pool.Acquire(nil); state != nil || err == nil || errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Acquire after Close = %v, %v; want closed pool error", state, err)
|
||||
}
|
||||
pool.Close()
|
||||
}
|
||||
|
||||
func TestPoolCloseWaitsForFactory(t *testing.T) {
|
||||
finishCtx, finish := context.WithCancel(context.Background())
|
||||
started, canceled := make(chan struct{}), make(chan struct{})
|
||||
first := true
|
||||
pool := newTestPool(t, context.Background(), time.Second, func(ctx context.Context) (*glua.LState, error) {
|
||||
if first {
|
||||
first = false
|
||||
return glua.NewState(), nil
|
||||
}
|
||||
close(started)
|
||||
<-ctx.Done()
|
||||
close(canceled)
|
||||
<-finishCtx.Done()
|
||||
return nil, ctx.Err()
|
||||
})
|
||||
t.Cleanup(finish)
|
||||
state, err := pool.Acquire(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pool.Release(state, false)
|
||||
acquireDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := pool.Acquire(nil)
|
||||
acquireDone <- err
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("state creation did not start")
|
||||
}
|
||||
closed := make(chan struct{})
|
||||
go func() {
|
||||
pool.Close()
|
||||
close(closed)
|
||||
}()
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Close did not cancel state creation")
|
||||
}
|
||||
assertPoolCloseBlocked(t, closed)
|
||||
finish()
|
||||
select {
|
||||
case err := <-acquireDone:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Acquire error = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("state creation did not finish")
|
||||
}
|
||||
select {
|
||||
case <-closed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Close did not finish after state creation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolCloseWaitsForCallerContext(t *testing.T) {
|
||||
pool := newTestPool(t, context.Background(), time.Minute, func(context.Context) (*glua.LState, error) {
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
started, done := make(chan context.Context, 1), make(chan error, 1)
|
||||
go func() {
|
||||
done <- pool.WithState(ctx, 0, func(L *glua.LState) error {
|
||||
started <- L.Context()
|
||||
<-L.Context().Done()
|
||||
return L.Context().Err()
|
||||
})
|
||||
}()
|
||||
var workCtx context.Context
|
||||
select {
|
||||
case workCtx = <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("WithState did not start")
|
||||
}
|
||||
closed := make(chan struct{})
|
||||
go func() {
|
||||
pool.Close()
|
||||
close(closed)
|
||||
}()
|
||||
select {
|
||||
case <-pool.ctx.Done():
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Close did not cancel the pool context")
|
||||
}
|
||||
assertPoolCloseBlocked(t, closed)
|
||||
if workCtx.Err() != nil || ctx.Err() != nil {
|
||||
t.Fatal("Close canceled the caller's execution context")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("WithState error = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("WithState did not stop after caller cancellation")
|
||||
}
|
||||
select {
|
||||
case <-closed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Close did not finish after WithState")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPoolAcquireRelease(b *testing.B) {
|
||||
pool := newTestPool(b, context.Background(), time.Second, func(context.Context) (*glua.LState, error) {
|
||||
return glua.NewState(), nil
|
||||
})
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
state, err := pool.Acquire(nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
pool.Release(state, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
var privilegedKey struct {
|
||||
sync.Once
|
||||
publicKey ed25519.PublicKey
|
||||
}
|
||||
|
||||
// SetPrivilegedPublicKey configures the trusted Ed25519 public key once at
|
||||
// startup, before compiling scripts. An empty value disables authorization.
|
||||
// Nonempty values must be unpadded Base64URL encodings of exactly 32 bytes.
|
||||
func SetPrivilegedPublicKey(value string) error {
|
||||
var publicKey ed25519.PublicKey
|
||||
if value != "" {
|
||||
decoded, err := decodePrivilegedValue(value, ed25519.PublicKeySize)
|
||||
if err != nil {
|
||||
return errors.New("invalid Lua privileged public key").Base(err)
|
||||
}
|
||||
publicKey = ed25519.PublicKey(decoded)
|
||||
}
|
||||
privilegedKey.Do(func() {
|
||||
privilegedKey.publicKey = publicKey
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodePrivilegedValue(value string, size int) ([]byte, error) {
|
||||
encoding := base64.RawURLEncoding.Strict()
|
||||
if len(value) != encoding.EncodedLen(size) {
|
||||
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
|
||||
}
|
||||
decoded, err := encoding.DecodeString(value)
|
||||
if err != nil || len(decoded) != size || encoding.EncodeToString(decoded) != value {
|
||||
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func verifyPrivilegedSignature(code []byte) (bool, error) {
|
||||
_, end, value, found := privilegedSignatureLine(code)
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
publicKey := privilegedKey.publicKey
|
||||
if len(publicKey) == 0 {
|
||||
return false, errors.New("Lua privileged signature requires --lua-privileged-pubkey")
|
||||
}
|
||||
signature, err := decodePrivilegedValue(string(bytes.TrimSpace(value)), ed25519.SignatureSize)
|
||||
if err != nil {
|
||||
return false, errors.New("invalid Lua privileged signature").Base(err)
|
||||
}
|
||||
digest := sha256.Sum256(bytes.TrimSpace(code[end:]))
|
||||
if !ed25519.Verify(publicKey, digest[:], signature) {
|
||||
return false, errors.New("Lua privileged signature verification failed")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SignPrivilegedScript signs the script body, then prepends or replaces the signature line.
|
||||
func SignPrivilegedScript(code []byte, seed string) ([]byte, error) {
|
||||
decoded, err := decodePrivilegedValue(seed, ed25519.SeedSize)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid Ed25519 private key seed").Base(err)
|
||||
}
|
||||
|
||||
start, end, _, found := privilegedSignatureLine(code)
|
||||
body := code
|
||||
if found {
|
||||
body = code[end:]
|
||||
}
|
||||
|
||||
digest := sha256.Sum256(bytes.TrimSpace(body))
|
||||
signature := ed25519.Sign(ed25519.NewKeyFromSeed(decoded), digest[:])
|
||||
header := []byte("-- @xray:privileged-signature=" + base64.RawURLEncoding.EncodeToString(signature))
|
||||
|
||||
if !found {
|
||||
if newline := bytes.IndexByte(code, '\n'); newline > 0 && code[newline-1] == '\r' {
|
||||
header = append(header, '\r')
|
||||
}
|
||||
header = append(header, '\n')
|
||||
return append(header, code...), nil
|
||||
}
|
||||
signed := make([]byte, 0, start+len(header)+len(code)-end)
|
||||
signed = append(signed, code[:start]...)
|
||||
signed = append(signed, header...)
|
||||
return append(signed, code[end:]...), nil
|
||||
}
|
||||
|
||||
func privilegedSignatureLine(code []byte) (start, end int, value []byte, found bool) {
|
||||
for rest := code; len(rest) != 0; {
|
||||
start = len(code) - len(rest)
|
||||
var line []byte
|
||||
line, rest, _ = bytes.Cut(rest, []byte{'\n'})
|
||||
line = bytes.TrimSuffix(line, []byte{'\r'})
|
||||
if len(bytes.TrimSpace(line)) == 0 {
|
||||
continue
|
||||
}
|
||||
end = start + len(line)
|
||||
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
|
||||
if !found {
|
||||
return 0, 0, nil, false
|
||||
}
|
||||
value, found = bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
|
||||
return start, end, value, found
|
||||
}
|
||||
return 0, 0, nil, false
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package lua
|
||||
|
||||
// TODO: Re-enable these tests and their helper when Compile supports privileged signature authorization again.
|
||||
/*
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func privilegedSeed(t *testing.T) string {
|
||||
t.Helper()
|
||||
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(seed)
|
||||
}
|
||||
|
||||
func TestSignPrivilegedScriptPreservesSource(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
seed := privilegedSeed(t)
|
||||
for _, tc := range []struct {
|
||||
name, prefix, header, body, newline string
|
||||
}{
|
||||
{name: "unsigned", body: privilegedFixtureBody, newline: "\n"},
|
||||
{name: "unsigned boundary whitespace", body: " \t\n\n" + privilegedFixtureBody + " \t\n", newline: "\n"},
|
||||
{name: "unsigned CRLF", body: strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n"), newline: "\r\n"},
|
||||
{name: "replace invalid header", header: privilegedSignatureHeader + "%", body: "\n" + privilegedFixtureBody, newline: "\n"},
|
||||
{name: "replace indented header", prefix: " \t\r\n\n", header: " \t--\t @xray:privileged-signature=% \t", body: "\r\n \t\r\n" + privilegedFixtureBody + "\r\n", newline: "\r\n"},
|
||||
{name: "replace valid header", header: privilegedSignatureHeader + privilegedFixtureSignature, body: "\n" + privilegedFixtureBody, newline: "\n"},
|
||||
{name: "empty script", newline: "\n"},
|
||||
{name: "header without newline", header: privilegedSignatureHeader + "%"},
|
||||
{name: "ordinary comment before marker", body: "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", newline: "\n"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
code := []byte(tc.prefix + tc.header + tc.body)
|
||||
original := bytes.Clone(code)
|
||||
signed, err := SignPrivilegedScript(code, seed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(code, original) {
|
||||
t.Fatal("signing modified the input buffer")
|
||||
}
|
||||
body := tc.body
|
||||
if tc.header != "" {
|
||||
body = strings.TrimPrefix(body, tc.newline)
|
||||
}
|
||||
header := privilegedSignatureHeader + testSignature(t, body)
|
||||
want := header + tc.newline + tc.body
|
||||
if tc.header != "" {
|
||||
want = tc.prefix + header + tc.body
|
||||
}
|
||||
if string(signed) != want {
|
||||
t.Fatalf("signed source = %q, want %q", signed, want)
|
||||
}
|
||||
program, err := Compile(writeTestScript(t, string(signed)))
|
||||
if err != nil || program == nil || !program.privileged {
|
||||
t.Fatalf("signed source Compile = %v, %v; want privileged program", program, err)
|
||||
}
|
||||
again, err := SignPrivilegedScript(signed, seed)
|
||||
if err != nil || !bytes.Equal(again, signed) {
|
||||
t.Fatalf("resigning changed the source: %q, %v", again, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPrivilegedScriptRejectsInvalidSeed(t *testing.T) {
|
||||
seed := privilegedSeed(t)
|
||||
for _, value := range []string{
|
||||
"", "%", seed + "=",
|
||||
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SeedSize-1)),
|
||||
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PrivateKeySize)),
|
||||
} {
|
||||
if signed, err := SignPrivilegedScript([]byte(privilegedFixtureBody), value); signed != nil || err == nil {
|
||||
t.Fatalf("SignPrivilegedScript with invalid seed = %q, %v; want rejection", signed, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,260 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/base64"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Program holds immutable bytecode that can be run by independent LStates.
|
||||
type Program struct {
|
||||
proto *glua.FunctionProto
|
||||
privileged bool
|
||||
}
|
||||
|
||||
// LStateFactory returns a fully initialized state or nil and an error.
|
||||
// Implementations must close partial states on failure; callers own successful states.
|
||||
type LStateFactory func(context.Context) (*glua.LState, error)
|
||||
|
||||
// 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]"
|
||||
privileged := false
|
||||
if strings.HasPrefix(script, base64.GzipPrefix) || strings.HasPrefix(script, base64.Prefix) {
|
||||
// TODO: Restore inline script support and re-enable the related tests.
|
||||
return nil, errors.New("currently unavailable")
|
||||
code, err = base64.Decode(script)
|
||||
} else if name, err = platform.ResolveLuaFile(script); err == nil {
|
||||
code, err = os.ReadFile(name)
|
||||
// TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries.
|
||||
// if err == nil {
|
||||
// privileged, err = verifyPrivilegedSignature(code)
|
||||
// }
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunk, err := parse.Parse(bytes.NewReader(code), name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proto, err := glua.Compile(chunk, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Program{proto: proto, privileged: privileged}, 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.
|
||||
// Choose registrySize for the script type's expected stack usage, balancing
|
||||
// per-state memory usage against the number of states retained in a pool.
|
||||
func (p *Program) NewState(ctx context.Context, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
|
||||
options := glua.Options{
|
||||
RegistrySize: registrySize,
|
||||
RegistryMaxSize: glua.RegistrySize,
|
||||
RegistryGrowStep: registrySize,
|
||||
}
|
||||
var L *glua.LState
|
||||
if p.privileged {
|
||||
L = glua.NewState(options)
|
||||
} else {
|
||||
L = newSandboxedState(options)
|
||||
}
|
||||
valid := false
|
||||
defer func() {
|
||||
if !valid {
|
||||
L.Close()
|
||||
}
|
||||
}()
|
||||
L.SetContext(ctx)
|
||||
defer L.RemoveContext()
|
||||
if register != nil {
|
||||
register(L)
|
||||
}
|
||||
if !p.privileged {
|
||||
// Hide package internals to prevent data snooping and sandbox escape.
|
||||
L.SetGlobal(glua.LoadLibName, glua.LNil)
|
||||
}
|
||||
L.Push(L.NewFunctionFromProto(p.proto))
|
||||
// Execute the Lua script's top level.
|
||||
if err := L.PCall(0, 0, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if validate != nil {
|
||||
if err := validate(L); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
valid = true
|
||||
return L, nil
|
||||
}
|
||||
|
||||
func newSandboxedState(options glua.Options) *glua.LState {
|
||||
options.SkipOpenLibs = true
|
||||
L := glua.NewState(options)
|
||||
|
||||
for _, lib := range []struct {
|
||||
name string
|
||||
open glua.LGFunction
|
||||
}{
|
||||
{glua.LoadLibName, glua.OpenPackage},
|
||||
{glua.BaseLibName, glua.OpenBase},
|
||||
{glua.TabLibName, glua.OpenTable},
|
||||
{glua.StringLibName, glua.OpenString},
|
||||
{glua.MathLibName, glua.OpenMath},
|
||||
{glua.OsLibName, glua.OpenOs},
|
||||
} {
|
||||
L.Push(L.NewFunction(lib.open))
|
||||
L.Push(glua.LString(lib.name))
|
||||
L.Call(1, 0)
|
||||
}
|
||||
|
||||
for _, name := range []string{
|
||||
"dofile", "loadfile", "load", "loadstring", "getfenv", "setfenv",
|
||||
"getmetatable", "setmetatable", "rawget", "rawset",
|
||||
"collectgarbage", "module", "newproxy", "print", "_printregs",
|
||||
} {
|
||||
L.SetGlobal(name, glua.LNil)
|
||||
}
|
||||
|
||||
// Prevent printing memory addresses that reveal the memory layout.
|
||||
toString := func(value glua.LValue) glua.LString {
|
||||
result := "<opaque>"
|
||||
switch value.Type() {
|
||||
case glua.LTNil, glua.LTBool, glua.LTNumber, glua.LTString:
|
||||
result = value.String()
|
||||
case glua.LTUserData:
|
||||
switch native := value.(*glua.LUserData).Value.(type) {
|
||||
case error: // Errors can also implement fmt.Stringer; redact them first.
|
||||
result = redactMemoryAddresses(native.Error())
|
||||
case fmt.Stringer:
|
||||
result = native.String()
|
||||
}
|
||||
}
|
||||
return glua.LString(result)
|
||||
}
|
||||
L.SetGlobal("tostring", L.NewFunction(func(L *glua.LState) int {
|
||||
L.Push(toString(L.CheckAny(1)))
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Prevent printing memory addresses that reveal the memory layout.
|
||||
stringLib := L.GetGlobal(glua.StringLibName).(*glua.LTable)
|
||||
stringLib.RawSetString("dump", glua.LNil)
|
||||
format := stringLib.RawGetString("format")
|
||||
stringLib.RawSetString("format", L.NewFunction(func(L *glua.LState) int {
|
||||
nargs := L.GetTop()
|
||||
for i := 2; i <= nargs; i++ {
|
||||
value := L.Get(i)
|
||||
switch value.Type() {
|
||||
case glua.LTBool, glua.LTNumber, glua.LTString:
|
||||
continue
|
||||
}
|
||||
L.Replace(i, toString(value))
|
||||
}
|
||||
L.Insert(format, 1)
|
||||
L.Call(nargs, 1)
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Redact addresses before caught errors become readable by the script.
|
||||
redactError := func(value glua.LValue) glua.LValue {
|
||||
if message, ok := value.(glua.LString); ok {
|
||||
if redacted := redactMemoryAddresses(string(message)); redacted != string(message) {
|
||||
return glua.LString(redacted)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
for _, name := range []string{"pcall", "xpcall"} {
|
||||
call := L.GetGlobal(name)
|
||||
L.SetGlobal(name, L.NewFunction(func(L *glua.LState) int {
|
||||
nargs := L.GetTop()
|
||||
if name == "xpcall" {
|
||||
L.CheckFunction(1)
|
||||
handler := L.CheckFunction(2)
|
||||
L.Replace(2, L.NewFunction(func(L *glua.LState) int {
|
||||
L.Replace(1, redactError(L.Get(1)))
|
||||
L.Insert(handler, 1)
|
||||
L.Call(1, 1)
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
L.Insert(call, 1)
|
||||
L.Call(nargs, glua.MultRet)
|
||||
if L.Get(1) == glua.LFalse {
|
||||
L.Replace(2, redactError(L.Get(2)))
|
||||
}
|
||||
return L.GetTop()
|
||||
}))
|
||||
}
|
||||
|
||||
// Prevent seed output from compromising cryptographic security.
|
||||
L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil)
|
||||
|
||||
// Keep only time functions, including when os is accessed through require.
|
||||
osLib := L.GetGlobal(glua.OsLibName).(*glua.LTable)
|
||||
for key, _ := osLib.Next(glua.LNil); key != glua.LNil; key, _ = osLib.Next(key) {
|
||||
switch key {
|
||||
case glua.LString("clock"), glua.LString("date"), glua.LString("difftime"), glua.LString("time"):
|
||||
default:
|
||||
osLib.RawSet(key, glua.LNil)
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent module injection through the global package table.
|
||||
module := L.GetGlobal(glua.LoadLibName).(*glua.LTable)
|
||||
preload := module.RawGetString("preload").(*glua.LTable)
|
||||
loaders := L.CreateTable(1, 0)
|
||||
loaders.RawSetInt(1, L.NewFunction(func(L *glua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
loader := preload.RawGetString(name)
|
||||
if loader == glua.LNil {
|
||||
loader = glua.LString("no preloaded module: " + name)
|
||||
}
|
||||
L.Push(loader)
|
||||
return 1
|
||||
}))
|
||||
module.RawSetString("loaders", loaders)
|
||||
L.SetField(L.Get(glua.RegistryIndex), "_LOADERS", loaders)
|
||||
for _, name := range []string{"loadlib", "seeall", "path", "cpath", "config"} {
|
||||
module.RawSetString(name, glua.LNil)
|
||||
}
|
||||
|
||||
return L
|
||||
}
|
||||
|
||||
var memoryAddressPattern = regexp.MustCompile(`0[xX][0-9a-fA-F]+`)
|
||||
|
||||
func redactMemoryAddresses(message string) string {
|
||||
if !strings.Contains(message, "0x") && !strings.Contains(message, "0X") {
|
||||
return message
|
||||
}
|
||||
return memoryAddressPattern.ReplaceAllString(message, "<address>")
|
||||
}
|
||||
|
||||
// NewStateFactory returns a factory that gives each state an initialization timeout.
|
||||
// Choose registrySize for the script type's expected stack usage, balancing
|
||||
// per-state memory usage against the number of states retained in a pool.
|
||||
func (p *Program) NewStateFactory(initTimeout time.Duration, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
|
||||
return func(ctx context.Context) (*glua.LState, error) {
|
||||
initCtx, cancel := context.WithTimeout(ctx, initTimeout)
|
||||
defer cancel()
|
||||
return p.NewState(initCtx, registrySize, register, validate)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type number interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// PushNumber converts a Go number to a Lua number and pushes it.
|
||||
func PushNumber[T number](L *glua.LState, value T) {
|
||||
L.Push(glua.LNumber(value))
|
||||
}
|
||||
|
||||
// PushString converts a Go string to a Lua string and pushes it.
|
||||
func PushString(L *glua.LState, value string) {
|
||||
L.Push(glua.LString(value))
|
||||
}
|
||||
|
||||
// PushNil pushes Lua nil.
|
||||
func PushNil(L *glua.LState) {
|
||||
L.Push(glua.LNil)
|
||||
}
|
||||
|
||||
// PushUserData pushes a native Go value without copying it.
|
||||
func PushUserData(L *glua.LState, value any) {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value
|
||||
L.Push(ud)
|
||||
}
|
||||
|
||||
// PushError pushes nil or the original Go error as userdata.
|
||||
func PushError(L *glua.LState, err error) {
|
||||
if err == nil {
|
||||
L.Push(glua.LNil)
|
||||
return
|
||||
}
|
||||
PushUserData(L, err)
|
||||
}
|
||||
|
||||
// ReadUserData reads a native Go value of type T without copying it.
|
||||
// Other Lua values or userdata containing a different type return invalidMessage.
|
||||
func ReadUserData[T any](value glua.LValue, invalidMessage string) (T, error) {
|
||||
if ud, ok := value.(*glua.LUserData); ok {
|
||||
if result, ok := ud.Value.(T); ok {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
var zero T
|
||||
return zero, errors.New(invalidMessage)
|
||||
}
|
||||
|
||||
// ReadError accepts nil, a native Go error, or a Lua string.
|
||||
// Native errors retain their identity; other values return invalidMessage.
|
||||
func ReadError(value glua.LValue, invalidMessage string) error {
|
||||
if value == glua.LNil {
|
||||
return nil
|
||||
}
|
||||
if ud, ok := value.(*glua.LUserData); ok {
|
||||
if err, ok := ud.Value.(error); ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if message, ok := value.(glua.LString); ok {
|
||||
return errors.New(string(message))
|
||||
}
|
||||
return errors.New(invalidMessage)
|
||||
}
|
||||
|
||||
// ReadUint32 accepts only integral Lua numbers in the uint32 range.
|
||||
func ReadUint32(value glua.LValue, invalidMessage string) (uint32, error) {
|
||||
number, ok := value.(glua.LNumber)
|
||||
if !ok || number < 0 || number > math.MaxUint32 || math.Trunc(float64(number)) != float64(number) {
|
||||
return 0, errors.New(invalidMessage)
|
||||
}
|
||||
return uint32(number), nil
|
||||
}
|
||||
|
||||
// ReadOptionalString accepts a Lua string or nil, which becomes an empty string.
|
||||
// It does not coerce other values to strings.
|
||||
func ReadOptionalString(value glua.LValue, invalidMessage string) (string, error) {
|
||||
if value == glua.LNil {
|
||||
return "", nil
|
||||
}
|
||||
if result, ok := value.(glua.LString); ok {
|
||||
return string(result), nil
|
||||
}
|
||||
return "", errors.New(invalidMessage)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestReadUint32(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value glua.LValue
|
||||
want uint32
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "zero", value: glua.LNumber(0)},
|
||||
{name: "integer", value: glua.LNumber(45), want: 45},
|
||||
{name: "maximum", value: glua.LNumber(math.MaxUint32), want: math.MaxUint32},
|
||||
{name: "fraction", value: glua.LNumber(1.5), wantErr: true},
|
||||
{name: "negative", value: glua.LNumber(-1), wantErr: true},
|
||||
{name: "overflow", value: glua.LNumber(math.MaxUint32 + 1), wantErr: true},
|
||||
{name: "NaN", value: glua.LNumber(math.NaN()), wantErr: true},
|
||||
{name: "positive infinity", value: glua.LNumber(math.Inf(1)), wantErr: true},
|
||||
{name: "negative infinity", value: glua.LNumber(math.Inf(-1)), wantErr: true},
|
||||
{name: "nil", value: glua.LNil, wantErr: true},
|
||||
{name: "numeric string", value: glua.LString("45"), wantErr: true},
|
||||
{name: "boolean", value: glua.LTrue, wantErr: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ReadUint32(tc.value, "invalid number")
|
||||
if got != tc.want || (err != nil) != tc.wantErr {
|
||||
t.Fatalf("ReadUint32() = %d, %v; want %d, error %t", got, err, tc.want, tc.wantErr)
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "invalid number") {
|
||||
t.Fatalf("error = %v, want invalid number", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOptionalString(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value glua.LValue
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "nil", value: glua.LNil},
|
||||
{name: "empty", value: glua.LString("")},
|
||||
{name: "string", value: glua.LString("out"), want: "out"},
|
||||
{name: "number", value: glua.LNumber(1), wantErr: true},
|
||||
{name: "boolean", value: glua.LFalse, wantErr: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ReadOptionalString(tc.value, "invalid string")
|
||||
if got != tc.want || (err != nil) != tc.wantErr {
|
||||
t.Fatalf("ReadOptionalString() = %q, %v; want %q, error %t", got, err, tc.want, tc.wantErr)
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "invalid string") {
|
||||
t.Fatalf("error = %v, want invalid string", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserDataRoundTrip(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
want := []int{1, 2}
|
||||
PushUserData(L, want)
|
||||
if L.GetTop() != 1 {
|
||||
t.Fatalf("stack top = %d, want 1", L.GetTop())
|
||||
}
|
||||
got, err := ReadUserData[[]int](L.Get(-1), "invalid userdata")
|
||||
if err != nil || len(got) != len(want) || &got[0] != &want[0] {
|
||||
t.Fatalf("userdata = %v, %v; want original slice", got, err)
|
||||
}
|
||||
PushUserData(L, []int(nil))
|
||||
if got, err := ReadUserData[[]int](L.Get(-1), "invalid userdata"); err != nil || got != nil {
|
||||
t.Fatalf("nil slice userdata = %v, %v", got, err)
|
||||
}
|
||||
for _, value := range []glua.LValue{glua.LNil, glua.LString("1"), L.NewTable(), L.Get(1)} {
|
||||
if got, err := ReadUserData[int](value, "invalid userdata"); got != 0 || err == nil || !strings.Contains(err.Error(), "invalid userdata") {
|
||||
t.Fatalf("ReadUserData(%v) = %d, %v; want invalid userdata", value, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorRoundTrip(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
want := errors.New("upstream failed")
|
||||
for _, err := range []error{nil, want} {
|
||||
PushError(L, err)
|
||||
if L.GetTop() != 1 {
|
||||
t.Fatalf("stack top = %d, want 1", L.GetTop())
|
||||
}
|
||||
if err == nil && L.Get(-1) != glua.LNil {
|
||||
t.Fatalf("nil error pushed as %v", L.Get(-1))
|
||||
}
|
||||
if got := ReadError(L.Get(-1), "invalid error"); got != err {
|
||||
t.Fatalf("ReadError() = %v, want original error %v", got, err)
|
||||
}
|
||||
L.Pop(1)
|
||||
}
|
||||
for _, message := range []string{"script failed", ""} {
|
||||
if err := ReadError(glua.LString(message), "invalid error"); err == nil || !strings.Contains(err.Error(), message) {
|
||||
t.Fatalf("string error = %v, want %q", err, message)
|
||||
}
|
||||
}
|
||||
wrong := L.NewUserData()
|
||||
wrong.Value = "not a native error"
|
||||
for _, value := range []glua.LValue{glua.LTrue, glua.LNumber(1), L.NewTable(), wrong, L.NewUserData()} {
|
||||
if err := ReadError(value, "invalid error"); err == nil || !strings.Contains(err.Error(), "invalid error") {
|
||||
t.Fatalf("ReadError(%v) = %v, want invalid error", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/mux"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/testing/mocks"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
@@ -114,3 +116,48 @@ func TestClientWorkerClose(t *testing.T) {
|
||||
|
||||
common.Must(w2.Close())
|
||||
}
|
||||
|
||||
func TestClientWorkerUDPSource(t *testing.T) {
|
||||
downR, downW := pipe.New(pipe.WithoutSizeLimit())
|
||||
upR, upW := pipe.New(pipe.WithoutSizeLimit())
|
||||
worker, err := mux.NewClientWorker(transport.Link{Reader: downR, Writer: upW}, mux.ClientStrategy{})
|
||||
common.Must(err)
|
||||
|
||||
inR, inW := pipe.New(pipe.WithoutSizeLimit())
|
||||
outR, outW := pipe.New(pipe.WithoutSizeLimit())
|
||||
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{
|
||||
Target: net.UDPDestination(net.ParseAddress("8.8.8.8"), 53),
|
||||
}})
|
||||
if !worker.Dispatch(ctx, &transport.Link{Reader: inR, Writer: outW}) {
|
||||
t.Fatal("failed to dispatch")
|
||||
}
|
||||
b := buf.New()
|
||||
b.WriteString("query")
|
||||
common.Must(inW.WriteMultiBuffer(buf.MultiBuffer{b}))
|
||||
mb, err := upR.ReadMultiBuffer() // New frame, the session is UDP from now on
|
||||
common.Must(err)
|
||||
buf.ReleaseMulti(mb)
|
||||
|
||||
srcs := []net.Destination{
|
||||
net.UDPDestination(net.ParseAddress("1.1.1.1"), 1111),
|
||||
net.UDPDestination(net.DomainAddress("example.com"), 2222),
|
||||
net.UDPDestination(net.ParseAddress("3.3.3.3"), 3333),
|
||||
}
|
||||
w := mux.NewResponseWriter(1, downW, protocol.TransferTypePacket)
|
||||
var got buf.MultiBuffer
|
||||
for i := range srcs {
|
||||
b := buf.New()
|
||||
b.WriteString("reply")
|
||||
b.UDP = &srcs[i]
|
||||
common.Must(w.WriteMultiBuffer(buf.MultiBuffer{b}))
|
||||
// keep earlier replies around while the next frame is parsed
|
||||
mb, err := outR.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
got = append(got, mb...)
|
||||
}
|
||||
for i, b := range got {
|
||||
if b.UDP == nil || *b.UDP != srcs[i] {
|
||||
t.Errorf("reply %d: source = %v, want %v", i, b.UDP, srcs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,16 @@ import (
|
||||
type PacketReader struct {
|
||||
reader io.Reader
|
||||
eof bool
|
||||
dest *net.Destination
|
||||
dest net.Destination
|
||||
}
|
||||
|
||||
// NewPacketReader creates a new PacketReader.
|
||||
// dest is copied because the caller reuses it for the next frame.
|
||||
func NewPacketReader(reader io.Reader, dest *net.Destination) *PacketReader {
|
||||
return &PacketReader{
|
||||
reader: reader,
|
||||
eof: false,
|
||||
dest: dest,
|
||||
dest: *dest,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +48,8 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
return nil, err
|
||||
}
|
||||
r.eof = true
|
||||
if r.dest != nil && r.dest.Network == net.Network_UDP {
|
||||
b.UDP = r.dest
|
||||
if r.dest.Network == net.Network_UDP {
|
||||
b.UDP = &r.dest // only one packet is read, so b owns r.dest
|
||||
}
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@ func ReadAsset(file string) ([]byte, error) {
|
||||
return ReadFile(path)
|
||||
}
|
||||
|
||||
func OpenAsset(file string) (io.ReadCloser, error) {
|
||||
path, _, err := getAssetFileLocation(file)
|
||||
func OpenAsset(file string) (io.ReadCloser, int64, error) {
|
||||
path, info, err := getAssetFileLocation(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
return NewFileReader(path)
|
||||
r, err := NewFileReader(path)
|
||||
return r, info.Size(), err
|
||||
}
|
||||
|
||||
func StatAsset(file string) (os.FileInfo, error) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package platform // import "github.com/xtls/xray-core/common/platform"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -90,3 +92,64 @@ func GetConfDirPath() string {
|
||||
configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" })
|
||||
return configPath
|
||||
}
|
||||
|
||||
// ResolveLuaFile finds a local .lua script and returns its resolved absolute path.
|
||||
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
|
||||
func ResolveLuaFile(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", errors.New("Lua file path is empty")
|
||||
}
|
||||
paths := []string{path}
|
||||
if !filepath.IsAbs(path) {
|
||||
paths = nil
|
||||
for _, dir := range []string{
|
||||
GetConfDirPath(),
|
||||
NewEnvFlag(ConfigLocation).GetValue(func() string { return "" }),
|
||||
".",
|
||||
getExecutableDir(),
|
||||
} {
|
||||
if dir != "" {
|
||||
paths = append(paths, filepath.Join(dir, path))
|
||||
}
|
||||
}
|
||||
}
|
||||
path, err := resolveFile(paths)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolveLuaTarget(path)
|
||||
}
|
||||
|
||||
func resolveLuaTarget(path string) (string, error) {
|
||||
path, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve Lua file symlinks: %w", err)
|
||||
}
|
||||
if filepath.Ext(path) != ".lua" {
|
||||
return "", errors.New("Lua file extension must be .lua")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func resolveFile(paths []string) (string, error) {
|
||||
var tried []string
|
||||
for _, path := range paths {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve file path: %w", err)
|
||||
}
|
||||
tried = append(tried, path)
|
||||
info, err := os.Stat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to inspect file %q: %w", path, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", fmt.Errorf("file is not a regular file: %s", path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package platform_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -64,3 +66,89 @@ func TestGetAssetLocation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLuaFile(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
t.Chdir(workingDir)
|
||||
executable, err := os.Executable()
|
||||
common.Must(err)
|
||||
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
|
||||
common.Must(err)
|
||||
common.Must(file.Close())
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
name := filepath.Base(file.Name())
|
||||
paths := []string{
|
||||
filepath.Join(t.TempDir(), name),
|
||||
filepath.Join(t.TempDir(), name),
|
||||
filepath.Join(workingDir, name),
|
||||
file.Name(),
|
||||
}
|
||||
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
|
||||
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
|
||||
for _, path := range paths[:3] {
|
||||
common.Must(os.WriteFile(path, nil, 0o600))
|
||||
}
|
||||
for i, path := range paths {
|
||||
paths[i], err = filepath.EvalSymlinks(path)
|
||||
common.Must(err)
|
||||
}
|
||||
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
|
||||
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
|
||||
}
|
||||
for i, want := range paths {
|
||||
if i == 2 {
|
||||
t.Setenv(ConfdirLocation, "")
|
||||
t.Setenv(ConfigLocation, "")
|
||||
}
|
||||
if got, err := ResolveLuaFile(name); err != nil || got != want {
|
||||
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
|
||||
}
|
||||
common.Must(os.Remove(want))
|
||||
}
|
||||
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("missing file error = %v", err)
|
||||
}
|
||||
|
||||
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
|
||||
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
|
||||
common.Must(os.Mkdir(paths[0], 0o700))
|
||||
common.Must(os.WriteFile(paths[1], nil, 0o600))
|
||||
for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} {
|
||||
if _, err := ResolveLuaFile(path); err == nil {
|
||||
t.Fatalf("accepted invalid path %q", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLuaFileSymlink(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
link, target string
|
||||
valid bool
|
||||
}{
|
||||
{"alias.lua", "data.dat", false},
|
||||
{"alias.dat", "script.lua", true},
|
||||
} {
|
||||
t.Run(tc.link, func(t *testing.T) {
|
||||
dir, err := filepath.EvalSymlinks(t.TempDir())
|
||||
common.Must(err)
|
||||
target := filepath.Join(dir, tc.target)
|
||||
common.Must(os.WriteFile(target, nil, 0o600))
|
||||
link := filepath.Join(dir, tc.link)
|
||||
if err := os.Symlink(tc.target, link); err != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skipf("cannot create symlinks: %v", err)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := ResolveLuaFile(link)
|
||||
if tc.valid {
|
||||
if err != nil || got != target {
|
||||
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target)
|
||||
}
|
||||
} else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
|
||||
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -175,9 +175,13 @@ func init() {
|
||||
switch v := input.(type) {
|
||||
case cmdarg.Arg:
|
||||
r, err := confloader.LoadConfig(v[0])
|
||||
common.Must(err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := buf.ReadAllToBytes(r)
|
||||
common.Must(err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadProtobufConfig(data)
|
||||
case io.Reader:
|
||||
data, err := buf.ReadAllToBytes(v)
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 9
|
||||
Version_z byte = 30
|
||||
Version_y byte = 10
|
||||
Version_z byte = 10
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+3
-1
@@ -92,6 +92,8 @@ type Instance struct {
|
||||
|
||||
// Instance state
|
||||
func (server *Instance) IsRunning() bool {
|
||||
server.statusLock.Lock()
|
||||
defer server.statusLock.Unlock()
|
||||
return server.running
|
||||
}
|
||||
|
||||
@@ -320,7 +322,7 @@ func (s *Instance) RequireFeatures(callback interface{}, optional bool) error {
|
||||
|
||||
// AddFeature registers a feature into current Instance.
|
||||
func (s *Instance) AddFeature(feature features.Feature) error {
|
||||
if s.running {
|
||||
if s.IsRunning() {
|
||||
if err := feature.Start(); err != nil {
|
||||
errors.LogInfoInner(s.ctx, err, "failed to start feature")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ go 1.27
|
||||
|
||||
require (
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
|
||||
github.com/cloudflare/circl v1.6.5
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
@@ -20,32 +19,33 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
|
||||
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/crypto v0.58.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.1.1
|
||||
google.golang.org/grpc v1.84.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
layeh.com/gopher-luar v1.0.11
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
mvdan.cc/gofumpt v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.3 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||
@@ -55,10 +55,9 @@ require (
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
golang.org/x/tools v0.51.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
|
||||
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
@@ -30,8 +31,8 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||
@@ -78,9 +79,14 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
||||
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
|
||||
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
@@ -89,8 +95,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
@@ -101,12 +107,13 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -114,22 +121,22 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -157,6 +164,8 @@ gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TI
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
||||
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
|
||||
layeh.com/gopher-luar v1.0.11 h1:8zJudpKI6HWkoh9eyyNFaTM79PY6CAPcIr6X/KTiliw=
|
||||
layeh.com/gopher-luar v1.0.11/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
)
|
||||
|
||||
type NameServerConfig struct {
|
||||
ID string `json:"id"`
|
||||
Address *Address `json:"address"`
|
||||
ClientIP *Address `json:"clientIp"`
|
||||
Port uint16 `json:"port"`
|
||||
@@ -43,6 +44,7 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
var advanced struct {
|
||||
ID string `json:"id"`
|
||||
Address *Address `json:"address"`
|
||||
ClientIP *Address `json:"clientIp"`
|
||||
Port uint16 `json:"port"`
|
||||
@@ -60,6 +62,7 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
|
||||
UnexpectedIPs StringList `json:"unexpectedIPs"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &advanced); err == nil {
|
||||
c.ID = advanced.ID
|
||||
c.Address = advanced.Address
|
||||
c.ClientIP = advanced.ClientIP
|
||||
c.Port = advanced.Port
|
||||
@@ -134,6 +137,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
|
||||
}
|
||||
|
||||
return &dns.NameServer{
|
||||
Id: c.ID,
|
||||
Address: &net.Endpoint{
|
||||
Network: net.Network_UDP,
|
||||
Address: c.Address.Build(),
|
||||
@@ -159,6 +163,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
|
||||
// DNSConfig is a JSON serializable object for dns.Config
|
||||
type DNSConfig struct {
|
||||
Servers []*NameServerConfig `json:"servers"`
|
||||
Script string `json:"script"`
|
||||
Hosts *HostsWrapper `json:"hosts"`
|
||||
ClientIP *Address `json:"clientIp"`
|
||||
Tag string `json:"tag"`
|
||||
@@ -267,6 +272,17 @@ func (m *HostsWrapper) Build() ([]*dns.Config_HostMapping, error) {
|
||||
|
||||
// Build implements Buildable
|
||||
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{
|
||||
Tag: c.Tag,
|
||||
DisableCache: c.DisableCache,
|
||||
@@ -276,6 +292,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
DisableFallbackIfMatch: c.DisableFallbackIfMatch,
|
||||
EnableParallelQuery: c.EnableParallelQuery,
|
||||
QueryStrategy: resolveQueryStrategy(c.QueryStrategy),
|
||||
Script: c.Script,
|
||||
}
|
||||
|
||||
if c.ClientIP != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -122,3 +123,57 @@ func TestDNSConfigParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
built, err := parsed.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(built.NameServer) != 1 || built.NameServer[0].Id != "primary" {
|
||||
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":"script.lua"}`},
|
||||
{name: "compatible fields", input: `{"script":"script.lua","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":"script.lua","disableFallback":false,"disableFallbackIfMatch":false,"enableParallelQuery":false,"servers":[{"address":"1.1.1.1","domains":[],"skipFallback":false,"finalQuery":false}]}`},
|
||||
{name: "domains", input: `{"script":"script.lua","servers":[{"address":"1.1.1.1","domains":["domain:example.com"]}]}`, wantError: "domains"},
|
||||
{name: "skip fallback", input: `{"script":"script.lua","servers":[{"address":"1.1.1.1","skipFallback":true}]}`, wantError: "skipFallback"},
|
||||
{name: "final query", input: `{"script":"script.lua","servers":[{"address":"1.1.1.1","finalQuery":true}]}`, wantError: "finalQuery"},
|
||||
{name: "later server", input: `{"script":"script.lua","servers":["1.1.1.1",{"address":"8.8.8.8","finalQuery":true}]}`, wantError: "server 1"},
|
||||
{name: "disable fallback", input: `{"script":"script.lua","disableFallback":true}`, wantError: "disableFallback"},
|
||||
{name: "disable fallback if match", input: `{"script":"script.lua","disableFallbackIfMatch":true}`, wantError: "disableFallbackIfMatch"},
|
||||
{name: "parallel query", input: `{"script":"script.lua","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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+68
-11
@@ -16,8 +16,61 @@ import (
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
masqueproxy "github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
)
|
||||
|
||||
func defaultMasqueXmux() *splithttp.XmuxConfig {
|
||||
return &splithttp.XmuxConfig{
|
||||
MaxConcurrency: &splithttp.RangeConfig{},
|
||||
MaxConnections: &splithttp.RangeConfig{From: 1, To: 1},
|
||||
CMaxReuseTimes: &splithttp.RangeConfig{},
|
||||
HMaxRequestTimes: &splithttp.RangeConfig{},
|
||||
HMaxReusableSecs: &splithttp.RangeConfig{From: 1800, To: 3000},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasqueXmuxConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(MasqueConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{"xmux": {"maxConnections": "2-4", "hMaxReusableSecs": 60, "hKeepAlivePeriod": -1}}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Path: "/.well-known/masque/ip/*/*/",
|
||||
Xmux: &splithttp.XmuxConfig{
|
||||
MaxConcurrency: &splithttp.RangeConfig{},
|
||||
MaxConnections: &splithttp.RangeConfig{From: 2, To: 4},
|
||||
CMaxReuseTimes: &splithttp.RangeConfig{},
|
||||
HMaxRequestTimes: &splithttp.RangeConfig{},
|
||||
HMaxReusableSecs: &splithttp.RangeConfig{From: 60, To: 60},
|
||||
HKeepAlivePeriod: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{"xmux": {"maxConcurrency": 8}}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{
|
||||
Path: "/.well-known/masque/ip/*/*/",
|
||||
Xmux: &splithttp.XmuxConfig{
|
||||
MaxConcurrency: &splithttp.RangeConfig{From: 8, To: 8},
|
||||
MaxConnections: &splithttp.RangeConfig{},
|
||||
CMaxReuseTimes: &splithttp.RangeConfig{},
|
||||
HMaxRequestTimes: &splithttp.RangeConfig{},
|
||||
HMaxReusableSecs: &splithttp.RangeConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if _, err := loadJSON(creator)(`{"xmux": {"maxConnections": 2, "maxConcurrency": 8}}`); err == nil {
|
||||
t.Error("expected an error for maxConnections with maxConcurrency")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasqueConfig(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(MasqueConfig)
|
||||
@@ -27,7 +80,7 @@ func TestMasqueConfig(t *testing.T) {
|
||||
{
|
||||
Input: `{}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/"},
|
||||
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/", Xmux: defaultMasqueXmux()},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
@@ -40,12 +93,13 @@ func TestMasqueConfig(t *testing.T) {
|
||||
Host: "example.com:8443",
|
||||
Path: "/.well-known/masque/ip/*/*/",
|
||||
Headers: map[string]string{"Authorization": "Basic dTpw"},
|
||||
Xmux: defaultMasqueXmux(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{"path": "/masque/ip{?target,ipproto}"}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*"},
|
||||
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*", Xmux: defaultMasqueXmux()},
|
||||
},
|
||||
{
|
||||
Input: `{"user": "u", "pass": "p:q", "headers": {"X-Token": "a"}}`,
|
||||
@@ -53,6 +107,7 @@ func TestMasqueConfig(t *testing.T) {
|
||||
Output: &masque.Config{
|
||||
Path: "/.well-known/masque/ip/*/*/",
|
||||
Headers: map[string]string{"Authorization": "Basic dTpwOnE=", "X-Token": "a"},
|
||||
Xmux: defaultMasqueXmux(),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -122,6 +177,7 @@ func TestMasqueWarpConfig(t *testing.T) {
|
||||
Host: "cloudflareaccess.com",
|
||||
Path: "/",
|
||||
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: address},
|
||||
Xmux: defaultMasqueXmux(),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -134,6 +190,7 @@ func TestMasqueWarpConfig(t *testing.T) {
|
||||
Host: "example.com",
|
||||
Path: "/warp",
|
||||
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: address},
|
||||
Xmux: defaultMasqueXmux(),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -164,6 +221,7 @@ func TestMasqueWarpConfig(t *testing.T) {
|
||||
Host: "cloudflareaccess.com",
|
||||
Path: "/",
|
||||
Warp: &masque.Warp{PrivateKey: pkcs8, PublicKey: publicKey, Address: []string{"172.16.0.2/32"}},
|
||||
Xmux: defaultMasqueXmux(),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -199,18 +257,17 @@ func TestMasqueOutboundConfig(t *testing.T) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := build(`{
|
||||
"protocol": "masque",
|
||||
"settings": {"address": "example.com", "port": 443},
|
||||
"streamSettings": {"network": "masque", "security": "tls"},
|
||||
"mux": {"enabled": false, "concurrency": -1}
|
||||
}`); err != nil {
|
||||
t.Error(err)
|
||||
for _, input := range []string{
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": false, "concurrency": -1}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1, "xudpConcurrency": 8}}`,
|
||||
} {
|
||||
if err := build(input); err != nil {
|
||||
t.Errorf("%s: %v", input, err)
|
||||
}
|
||||
}
|
||||
for _, input := range []string{
|
||||
`{"protocol": "masque", "settings": {"address": "example.com"}, "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
|
||||
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1}}`,
|
||||
`{"protocol": "freedom", "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||
} {
|
||||
if err := build(input); err == nil {
|
||||
|
||||
@@ -72,6 +72,7 @@ type RouterConfig struct {
|
||||
RuleList []json.RawMessage `json:"rules"`
|
||||
DomainStrategy *string `json:"domainStrategy"`
|
||||
Balancers []*BalancingRule `json:"balancers"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
|
||||
@@ -92,6 +93,12 @@ func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
|
||||
|
||||
func (c *RouterConfig) Build() (*router.Config, error) {
|
||||
config := new(router.Config)
|
||||
|
||||
if c.Script != "" && (c.DomainStrategy != nil || len(c.RuleList) > 0) {
|
||||
return nil, errors.New("routing script cannot be used with domainStrategy or rules")
|
||||
}
|
||||
config.Script = c.Script
|
||||
|
||||
config.DomainStrategy = c.getDomainStrategy()
|
||||
|
||||
var rawRuleList []json.RawMessage
|
||||
|
||||
@@ -2,6 +2,7 @@ package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
@@ -236,3 +237,40 @@ func TestRouterConfig(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouterScriptConfig(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
input string
|
||||
wantError bool
|
||||
}{
|
||||
{name: "script only", input: `{"script":"missing.lua"}`},
|
||||
{name: "empty rules", input: `{"script":"missing.lua","rules":[]}`},
|
||||
{name: "balancers", input: `{"script":"missing.lua","balancers":[{"tag":"b1","selector":["direct"]}]}`},
|
||||
{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(test.name, func(t *testing.T) {
|
||||
var config RouterConfig
|
||||
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(), "routing script cannot be used with domainStrategy or rules") {
|
||||
t.Fatalf("Build() = %v, %v; want nil config and script conflict error", built, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if built.Script != config.Script || len(built.Rule) != len(config.RuleList) || len(built.BalancingRule) != len(config.Balancers) {
|
||||
t.Fatalf("Build() did not preserve script, rules or balancers: %v", built)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||
@@ -28,6 +31,7 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -304,11 +308,26 @@ type NoiseItem struct {
|
||||
}
|
||||
|
||||
type NoiseMask struct {
|
||||
Reset Int32Range `json:"reset"`
|
||||
Noise []NoiseItem `json:"noise"`
|
||||
Reset Int32Range `json:"reset"`
|
||||
Noise []NoiseItem `json:"noise"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
func (c *NoiseMask) Build() (proto.Message, error) {
|
||||
if c.Script != "" {
|
||||
if len(c.Noise) > 0 {
|
||||
return nil, errors.New(`"script" and "noise" can't be used together`)
|
||||
}
|
||||
if _, err := xlua.Compile(c.Script); err != nil {
|
||||
return nil, errors.New("failed to compile the noise script").Base(err)
|
||||
}
|
||||
return &noise.Config{
|
||||
ResetMin: int64(c.Reset.From),
|
||||
ResetMax: int64(c.Reset.To),
|
||||
Script: c.Script,
|
||||
}, nil
|
||||
}
|
||||
|
||||
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
|
||||
for _, item := range c.Noise {
|
||||
if len(item.Packet) > 0 && item.Rand.To > 0 {
|
||||
@@ -799,7 +818,10 @@ type XDNSDomain struct {
|
||||
}
|
||||
|
||||
type XDNSResolver struct {
|
||||
Addrs []string `json:"addrs"`
|
||||
Addrs []string `json:"addrs"`
|
||||
Worker int32 `json:"worker"`
|
||||
TlsConfig *TLSConfig `json:"tlsConfig"`
|
||||
QuicConfig *QuicParamsConfig `json:"quicConfig"`
|
||||
}
|
||||
|
||||
type XDNS struct {
|
||||
@@ -834,6 +856,26 @@ func (c *XDNS) Build() (proto.Message, error) {
|
||||
}
|
||||
}
|
||||
for i := range c.Resolvers {
|
||||
worker := c.Resolvers[i].Worker
|
||||
if worker == 0 {
|
||||
worker = 32
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
var quicConfig *internet.QuicParams
|
||||
if c.Resolvers[i].TlsConfig != nil {
|
||||
proto, err := c.Resolvers[i].TlsConfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = proto.(*tls.Config)
|
||||
}
|
||||
if c.Resolvers[i].QuicConfig != nil {
|
||||
proto, err := c.Resolvers[i].QuicConfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConfig = proto.(*internet.QuicParams)
|
||||
}
|
||||
for j := range c.Resolvers[i].Addrs {
|
||||
var u *url.URL
|
||||
var e error
|
||||
@@ -845,18 +887,21 @@ func (c *XDNS) Build() (proto.Message, error) {
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
var addr string
|
||||
switch u.Scheme {
|
||||
case "tcp", "udp":
|
||||
host := u.Hostname()
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = "53"
|
||||
}
|
||||
addr = net.JoinHostPort(host, port)
|
||||
case "https":
|
||||
addr = u.String()
|
||||
default:
|
||||
return nil, errors.New("invalid protocol")
|
||||
}
|
||||
var host, port string
|
||||
host = u.Hostname()
|
||||
port = u.Port()
|
||||
if port == "" {
|
||||
port = "53"
|
||||
}
|
||||
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: net.JoinHostPort(host, port)})
|
||||
resolvers = append(resolvers, &xdns.ResolverProto{Type: u.Scheme, Addr: addr, Worker: worker, TlsConfig: tlsConfig, QuicConfig: quicConfig})
|
||||
}
|
||||
}
|
||||
if c.ExtraPoll < 0 || c.ExtraPoll > 3 {
|
||||
@@ -1149,6 +1194,91 @@ type QuicParamsConfig struct {
|
||||
DisableStatelessReset bool `json:"disableStatelessReset"`
|
||||
}
|
||||
|
||||
func (config *QuicParamsConfig) Build() (proto.Message, error) {
|
||||
profile := strings.ToLower(config.BbrProfile)
|
||||
switch profile {
|
||||
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
|
||||
if profile == "" {
|
||||
profile = string(bbr.ProfileStandard)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown bbr profile")
|
||||
}
|
||||
|
||||
up, err := config.BrutalUp.Bps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
down, err := config.BrutalDown.Bps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if up > 0 && up < 65536 {
|
||||
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
|
||||
}
|
||||
if down > 0 && down < 65536 {
|
||||
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
|
||||
}
|
||||
|
||||
config.Congestion = strings.ToLower(config.Congestion)
|
||||
switch config.Congestion {
|
||||
case "", "brutal", "reno", "bbr":
|
||||
case "force-brutal":
|
||||
if up == 0 {
|
||||
return nil, errors.New("force-brutal requires up")
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown congestion control: ", config.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
|
||||
}
|
||||
|
||||
if config.InitStreamReceiveWindow > 0 && config.InitStreamReceiveWindow < 16384 {
|
||||
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
|
||||
}
|
||||
if config.MaxStreamReceiveWindow > 0 && config.MaxStreamReceiveWindow < 16384 {
|
||||
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
|
||||
}
|
||||
if config.InitConnectionReceiveWindow > 0 && config.InitConnectionReceiveWindow < 16384 {
|
||||
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
|
||||
}
|
||||
if config.MaxConnectionReceiveWindow > 0 && config.MaxConnectionReceiveWindow < 16384 {
|
||||
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
|
||||
}
|
||||
if config.MaxIdleTimeout != 0 && (config.MaxIdleTimeout < 4 || config.MaxIdleTimeout > 120) {
|
||||
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
|
||||
}
|
||||
if config.KeepAlivePeriod != 0 && (config.KeepAlivePeriod < 2 || config.KeepAlivePeriod > 60) {
|
||||
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
|
||||
}
|
||||
if config.MaxIncomingStreams != 0 && config.MaxIncomingStreams < 8 {
|
||||
return nil, errors.New("MaxIncomingStreams must be at least 8")
|
||||
}
|
||||
|
||||
if config.Debug {
|
||||
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
|
||||
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
|
||||
}
|
||||
|
||||
return &internet.QuicParams{
|
||||
Congestion: config.Congestion,
|
||||
BbrProfile: profile,
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
BrutalDisableLossCompensation: config.BrutalDisableLossCompensation,
|
||||
InitStreamReceiveWindow: config.InitStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: config.MaxStreamReceiveWindow,
|
||||
InitConnReceiveWindow: config.InitConnectionReceiveWindow,
|
||||
MaxConnReceiveWindow: config.MaxConnectionReceiveWindow,
|
||||
MaxIdleTimeout: config.MaxIdleTimeout,
|
||||
KeepAlivePeriod: config.KeepAlivePeriod,
|
||||
DisablePathMtuDiscovery: config.DisablePathMTUDiscovery,
|
||||
DisableChromeParrot: config.DisableChromeParrot,
|
||||
DisableGSO: config.DisableGSO,
|
||||
MaxIncomingStreams: config.MaxIncomingStreams,
|
||||
DisableStatelessReset: config.DisableStatelessReset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type FinalMask struct {
|
||||
Tcp []Mask `json:"tcp"`
|
||||
Udp []Mask `json:"udp"`
|
||||
|
||||
@@ -2,6 +2,9 @@ package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
|
||||
@@ -134,3 +137,68 @@ func TestNoiseExpFromJSON(t *testing.T) {
|
||||
t.Errorf("item 3 = %+v", items[3])
|
||||
}
|
||||
}
|
||||
|
||||
const noiseScript = `function HandleNoise(plan, address) plan:Add("n") end
|
||||
`
|
||||
|
||||
func writeNoiseScript(t *testing.T, script string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "noise.lua")
|
||||
if err := os.WriteFile(path, []byte(script), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func buildNoiseMask(t *testing.T, mask *NoiseMask) *noise.Config {
|
||||
t.Helper()
|
||||
msg, err := mask.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return msg.(*noise.Config)
|
||||
}
|
||||
|
||||
func TestNoiseScriptFromFile(t *testing.T) {
|
||||
path := writeNoiseScript(t, noiseScript)
|
||||
cfg := buildNoiseMask(t, &NoiseMask{Script: path, Reset: Int32Range{From: 30, To: 60}})
|
||||
|
||||
if cfg.Script != path {
|
||||
t.Errorf("got script %q, want the path", cfg.Script)
|
||||
}
|
||||
if len(cfg.Items) != 0 {
|
||||
t.Errorf("got %d items, want none", len(cfg.Items))
|
||||
}
|
||||
if cfg.ResetMin != 30 || cfg.ResetMax != 60 {
|
||||
t.Errorf("got reset %d-%d, want 30-60", cfg.ResetMin, cfg.ResetMax)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoiseScriptFromJSON(t *testing.T) {
|
||||
path := writeNoiseScript(t, noiseScript)
|
||||
input := `{"script": ` + strconv.Quote(path) + `, "reset": "30-60"}`
|
||||
var mask NoiseMask
|
||||
if err := json.Unmarshal([]byte(input), &mask); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg := buildNoiseMask(t, &mask); cfg.Script != path {
|
||||
t.Errorf("got script %q, want the path", cfg.Script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoiseScriptRejected(t *testing.T) {
|
||||
path := writeNoiseScript(t, noiseScript)
|
||||
invalid := writeNoiseScript(t, "function HandleNoise(")
|
||||
|
||||
for name, mask := range map[string]*NoiseMask{
|
||||
"script with noise": {Script: path, Noise: []NoiseItem{{Rand: Int32Range{From: 10, To: 20}}}},
|
||||
"missing file": {Script: filepath.Join(t.TempDir(), "absent.lua")},
|
||||
"not lua": {Script: invalid},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := mask.Build(); err == nil {
|
||||
t.Error("expected an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
)
|
||||
|
||||
type TransportProtocol string
|
||||
@@ -228,102 +226,25 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
|
||||
if c.FinalMask != nil {
|
||||
for _, mask := range c.FinalMask.Tcp {
|
||||
u, err := mask.Build(true)
|
||||
proto, err := mask.Build(true)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
|
||||
}
|
||||
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u))
|
||||
config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(proto))
|
||||
}
|
||||
for _, mask := range c.FinalMask.Udp {
|
||||
u, err := mask.Build(false)
|
||||
proto, err := mask.Build(false)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build mask with type ", mask.Type).Base(err)
|
||||
}
|
||||
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u))
|
||||
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(proto))
|
||||
}
|
||||
if c.FinalMask.QuicParams != nil {
|
||||
profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile)
|
||||
switch profile {
|
||||
case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive):
|
||||
if profile == "" {
|
||||
profile = string(bbr.ProfileStandard)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown bbr profile")
|
||||
}
|
||||
|
||||
up, err := c.FinalMask.QuicParams.BrutalUp.Bps()
|
||||
proto, err := c.FinalMask.QuicParams.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
down, err := c.FinalMask.QuicParams.BrutalDown.Bps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if up > 0 && up < 65536 {
|
||||
return nil, errors.New("BrutalUp must be at least 65536 bytes per second")
|
||||
}
|
||||
if down > 0 && down < 65536 {
|
||||
return nil, errors.New("BrutalDown must be at least 65536 bytes per second")
|
||||
}
|
||||
|
||||
c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion)
|
||||
switch c.FinalMask.QuicParams.Congestion {
|
||||
case "", "brutal", "reno", "bbr":
|
||||
case "force-brutal":
|
||||
if up == 0 {
|
||||
return nil, errors.New("force-brutal requires up")
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
|
||||
}
|
||||
|
||||
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
|
||||
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
|
||||
}
|
||||
if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 {
|
||||
return nil, errors.New("MaxStreamReceiveWindow must be at least 16384")
|
||||
}
|
||||
if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 {
|
||||
return nil, errors.New("InitConnectionReceiveWindow must be at least 16384")
|
||||
}
|
||||
if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 {
|
||||
return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384")
|
||||
}
|
||||
if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) {
|
||||
return nil, errors.New("MaxIdleTimeout must be between 4 and 120")
|
||||
}
|
||||
if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) {
|
||||
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
|
||||
}
|
||||
if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 {
|
||||
return nil, errors.New("MaxIncomingStreams must be at least 8")
|
||||
}
|
||||
|
||||
if c.FinalMask.QuicParams.Debug {
|
||||
os.Setenv("HYSTERIA_BBR_DEBUG", "true")
|
||||
os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true")
|
||||
}
|
||||
|
||||
config.QuicParams = &internet.QuicParams{
|
||||
Congestion: c.FinalMask.QuicParams.Congestion,
|
||||
BbrProfile: profile,
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
|
||||
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
|
||||
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
|
||||
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
|
||||
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
|
||||
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
|
||||
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
|
||||
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
|
||||
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
|
||||
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
|
||||
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
|
||||
}
|
||||
config.QuicParams = proto.(*internet.QuicParams)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -808,6 +808,7 @@ type MasqueConfig struct {
|
||||
Pass string `json:"pass"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Warp *MasqueWarpConfig `json:"warp"`
|
||||
Xmux XmuxConfig `json:"xmux"`
|
||||
}
|
||||
|
||||
func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
@@ -878,11 +879,28 @@ func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.User+":"+c.Pass))
|
||||
}
|
||||
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
|
||||
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
|
||||
}
|
||||
if c.Xmux == (XmuxConfig{}) {
|
||||
c.Xmux.MaxConnections.From = 1
|
||||
c.Xmux.MaxConnections.To = 1
|
||||
c.Xmux.HMaxReusableSecs.From = 1800
|
||||
c.Xmux.HMaxReusableSecs.To = 3000
|
||||
}
|
||||
return &masque.Config{
|
||||
Host: host,
|
||||
Path: path,
|
||||
Headers: headers,
|
||||
Warp: warp,
|
||||
Xmux: &splithttp.XmuxConfig{
|
||||
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
|
||||
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
|
||||
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
|
||||
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
|
||||
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
|
||||
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
if slices.Contains(config.AutoSystemWfpBlockLeak, "dns") && len(v.DNS) == 0 {
|
||||
return nil, errors.New(`autoSystemWfpBlockLeak "dns" needs dns to be set`)
|
||||
}
|
||||
case "linux":
|
||||
case "linux", "freebsd":
|
||||
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
|
||||
return nil, errors.New("autoSystemDnsToGateway needs gateway to be set")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package conf_test
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
@@ -43,18 +44,18 @@ func TestTunConfigAutoSystem(t *testing.T) {
|
||||
func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
input string
|
||||
goos string // where it is rejected
|
||||
goos []string // where it is rejected
|
||||
}{
|
||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, "windows"},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, ""},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, "windows"},
|
||||
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, "linux"},
|
||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, nil},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
|
||||
} {
|
||||
config := new(TunConfig)
|
||||
if err := json.Unmarshal([]byte(c.input), config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := config.Build(); (err != nil) != (runtime.GOOS == c.goos) {
|
||||
if _, err := config.Build(); (err != nil) != slices.Contains(c.goos, runtime.GOOS) {
|
||||
t.Errorf("%s on %s: error = %v", c.input, runtime.GOOS, err)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -344,11 +344,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := ts.(*masque.ClientConfig); ok {
|
||||
if ms := senderSettings.MultiplexSettings; ms != nil && ms.Enabled {
|
||||
return nil, errors.New(`masque outbound does not support "mux"`)
|
||||
}
|
||||
} else if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
|
||||
if _, ok := ts.(*masque.ClientConfig); !ok && senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
|
||||
return nil, errors.New("the masque transport can only be used by the masque outbound")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package base64
|
||||
|
||||
import "github.com/xtls/xray-core/main/commands/base"
|
||||
|
||||
// CmdBase64 holds all Base64 subcommands.
|
||||
var CmdBase64 = &base.Command{
|
||||
UsageLine: "{{.Exec}} base64",
|
||||
Short: "Encode and decode Base64",
|
||||
Long: "{{.Exec}} {{.LongName}} provides tools to encode and decode arbitrary bytes.",
|
||||
Commands: []*base.Command{
|
||||
cmdEncode,
|
||||
cmdDecode,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
xbase64 "github.com/xtls/xray-core/common/base64"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdDecode = &base.Command{
|
||||
UsageLine: "{{.Exec}} base64 decode [-gzip] <value | - | stdin:> [output-file]",
|
||||
Short: "Decode Base64",
|
||||
Long: fmt.Sprintf(`
|
||||
Decode a Base64 value, or pass "-" or "stdin:" to read stdin.
|
||||
Write to output-file, or stdout if omitted.
|
||||
Accepts %s, %s, or no prefix; gzip is enabled by %s or -gzip.
|
||||
|
||||
Examples:
|
||||
|
||||
{{.Exec}} base64 decode eA== out.bin
|
||||
{{.Exec}} base64 decode -gzip H4sIAAAAAAAC/6sAAIMW3IwBAAAA out.bin
|
||||
`, xbase64.Prefix, xbase64.GzipPrefix, xbase64.GzipPrefix),
|
||||
}
|
||||
|
||||
var decodeGzipEnabled = cmdDecode.Flag.Bool("gzip", false, "Decompress the decoded bytes with gzip.")
|
||||
|
||||
func init() {
|
||||
cmdDecode.Run = executeDecode
|
||||
}
|
||||
|
||||
func executeDecode(cmd *base.Command, args []string) {
|
||||
if len(args) == 0 || len(args) > 2 {
|
||||
cmd.Usage()
|
||||
}
|
||||
|
||||
value := args[0]
|
||||
if value == "-" || value == "stdin:" {
|
||||
input, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to read input: %s", err)
|
||||
}
|
||||
value = string(input)
|
||||
}
|
||||
|
||||
value = strings.TrimSpace(value)
|
||||
if *decodeGzipEnabled && !strings.HasPrefix(value, xbase64.GzipPrefix) {
|
||||
value = xbase64.GzipPrefix + strings.TrimPrefix(value, xbase64.Prefix)
|
||||
}
|
||||
|
||||
data, err := xbase64.Decode(value)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to decode input: %s", err)
|
||||
}
|
||||
|
||||
if len(args) == 2 && args[1] != "-" {
|
||||
err = os.WriteFile(args[1], data, 0o666)
|
||||
} else {
|
||||
_, err = os.Stdout.Write(data)
|
||||
}
|
||||
if err != nil {
|
||||
base.Fatalf("failed to write decoded output: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
xbase64 "github.com/xtls/xray-core/common/base64"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdEncode = &base.Command{
|
||||
UsageLine: "{{.Exec}} base64 encode [-gzip] <input-file | - | stdin:>",
|
||||
Short: "Encode as Base64",
|
||||
Long: fmt.Sprintf(`
|
||||
Encode input-file as Base64 with the %s prefix.
|
||||
Pass "-" or "stdin:" to read stdin.
|
||||
Use -gzip to compress and output %s.
|
||||
|
||||
Examples:
|
||||
|
||||
{{.Exec}} base64 encode input.bin
|
||||
{{.Exec}} base64 encode -gzip input.bin
|
||||
`, xbase64.Prefix, xbase64.GzipPrefix),
|
||||
}
|
||||
|
||||
var gzipEnabled = cmdEncode.Flag.Bool("gzip", false, "Compress the input with gzip.")
|
||||
|
||||
func init() {
|
||||
cmdEncode.Run = executeEncode
|
||||
}
|
||||
|
||||
func executeEncode(cmd *base.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if args[0] == "-" || args[0] == "stdin:" {
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = os.ReadFile(args[0])
|
||||
}
|
||||
if err != nil {
|
||||
base.Fatalf("failed to read input: %s", err)
|
||||
}
|
||||
|
||||
value, err := xbase64.Encode(data, *gzipEnabled)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to encode input: %s", err)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(os.Stdout, value); err != nil {
|
||||
base.Fatalf("failed to write encoded output: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package all
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/main/commands/all/api"
|
||||
"github.com/xtls/xray-core/main/commands/all/base64"
|
||||
"github.com/xtls/xray-core/main/commands/all/convert"
|
||||
// TODO: Restore this import and lua.CmdLua when Lua privileged signature authorization is re-enabled.
|
||||
// "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/base"
|
||||
)
|
||||
@@ -11,10 +14,13 @@ func init() {
|
||||
base.RootCommand.Commands = append(
|
||||
base.RootCommand.Commands,
|
||||
api.CmdAPI,
|
||||
base64.CmdBase64,
|
||||
convert.CmdConvert,
|
||||
// lua.CmdLua,
|
||||
tls.CmdTLS,
|
||||
cmdUUID,
|
||||
cmdX25519,
|
||||
// cmdEd25519,
|
||||
cmdWG,
|
||||
cmdMLDSA65,
|
||||
cmdMLKEM768,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package all
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdEd25519 = &base.Command{
|
||||
UsageLine: `{{.Exec}} ed25519 [-i "private key seed (Base64URL)"]`,
|
||||
Short: "Generate an Ed25519 key pair",
|
||||
Long: `
|
||||
Generate an Ed25519 key pair, or restore it from a 32-byte private key seed
|
||||
with -i. PrivateKey (seed) and PublicKey use unpadded Base64URL.
|
||||
`,
|
||||
}
|
||||
|
||||
var inputEd25519 = cmdEd25519.Flag.String("i", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||
|
||||
func init() {
|
||||
cmdEd25519.Run = executeEd25519
|
||||
}
|
||||
|
||||
func executeEd25519(cmd *base.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
}
|
||||
|
||||
var privateKey ed25519.PrivateKey
|
||||
if *inputEd25519 == "" {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to generate Ed25519 key: %s", err)
|
||||
}
|
||||
privateKey = key
|
||||
} else {
|
||||
encoding := base64.RawURLEncoding.Strict()
|
||||
seed, err := encoding.DecodeString(*inputEd25519)
|
||||
if err != nil || len(seed) != ed25519.SeedSize || encoding.EncodeToString(seed) != *inputEd25519 {
|
||||
base.Fatalf("invalid Ed25519 private key seed: expected 32 bytes in unpadded Base64URL")
|
||||
}
|
||||
privateKey = ed25519.NewKeyFromSeed(seed)
|
||||
}
|
||||
|
||||
if _, err := fmt.Printf("PrivateKey: %s\nPublicKey: %s\n",
|
||||
base64.RawURLEncoding.EncodeToString(privateKey.Seed()),
|
||||
base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))); err != nil {
|
||||
base.Fatalf("failed to write Ed25519 keys: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package lua
|
||||
|
||||
import "github.com/xtls/xray-core/main/commands/base"
|
||||
|
||||
// CmdLua holds the Lua script commands.
|
||||
var CmdLua = &base.Command{
|
||||
UsageLine: "{{.Exec}} lua",
|
||||
Short: "Manage Lua scripts",
|
||||
Long: "{{.Exec}} {{.LongName}} provides tools for local Lua scripts.",
|
||||
Commands: []*base.Command{
|
||||
cmdSign,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdSign = &base.Command{
|
||||
UsageLine: "{{.Exec}} lua sign [-key seed] [-o output-file] <input-file>",
|
||||
Short: "Sign a local Lua script for privileged execution",
|
||||
Long: `
|
||||
Sign a local Lua file using a 32-byte Ed25519 private key seed in unpadded
|
||||
Base64URL. Use {{.Exec}} ed25519 to generate a key pair.
|
||||
Write back to the input file, or use -o to write another file (- for stdout).
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
signKey = cmdSign.Flag.String("key", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||
signOutput = cmdSign.Flag.String("o", "", "Output file, or - for stdout. Default: input file.")
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdSign.Run = executeSign
|
||||
}
|
||||
|
||||
func executeSign(cmd *base.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
}
|
||||
|
||||
info, err := os.Stat(args[0])
|
||||
if err != nil {
|
||||
base.Fatalf("failed to inspect Lua file: %s", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
base.Fatalf("Lua input is not a regular file: %s", args[0])
|
||||
}
|
||||
|
||||
code, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
base.Fatalf("failed to read Lua file: %s", err)
|
||||
}
|
||||
|
||||
signed, err := xlua.SignPrivilegedScript(code, *signKey)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to sign Lua file: %s", err)
|
||||
}
|
||||
|
||||
if *signOutput == "-" {
|
||||
if _, err := os.Stdout.Write(signed); err != nil {
|
||||
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
output := *signOutput
|
||||
if output == "" {
|
||||
output = args[0]
|
||||
}
|
||||
if err := writeSignedFile(output, signed, info.Mode().Perm()); err != nil {
|
||||
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSignedFile(path string, code []byte, mode os.FileMode) error {
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
if !info.Mode().IsRegular() {
|
||||
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
|
||||
}
|
||||
mode = info.Mode().Perm()
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp(filepath.Dir(path), ".xray-lua-sign-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer file.Close()
|
||||
if _, err := file.Write(code); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Chmod(mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(file.Name(), path)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package all
|
||||
|
||||
import (
|
||||
"crypto/mldsa"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
@@ -39,8 +39,8 @@ func executeMLDSA65(cmd *base.Command, args []string) {
|
||||
} else {
|
||||
rand.Read(seed[:])
|
||||
}
|
||||
pub, _ := mldsa65.NewKeyFromSeed(&seed)
|
||||
key, _ := mldsa.NewPrivateKey(mldsa.MLDSA65(), seed[:])
|
||||
fmt.Printf("Seed: %v\nVerify: %v\n",
|
||||
base64.RawURLEncoding.EncodeToString(seed[:]),
|
||||
base64.RawURLEncoding.EncodeToString(pub.Bytes()))
|
||||
base64.RawURLEncoding.EncodeToString(key.PublicKey().Bytes()))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ package base
|
||||
type CommandEnvHolder struct {
|
||||
// Executable name of current binary
|
||||
Exec string
|
||||
// Full name of the command being executed
|
||||
CommandName string
|
||||
// commands column width of current command
|
||||
CommandsWidth int
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ BigCmdLoop:
|
||||
}
|
||||
|
||||
buildCommandText(cmd)
|
||||
CommandEnv.CommandName = cmdName
|
||||
cmd.Run(cmd, args)
|
||||
Exit()
|
||||
return
|
||||
|
||||
Vendored
+35
-1
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
"github.com/xtls/xray-core/main/confloader"
|
||||
)
|
||||
|
||||
@@ -31,7 +33,12 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
|
||||
default:
|
||||
data, err = os.ReadFile(arg)
|
||||
file, openErr := os.Open(arg)
|
||||
if openErr != nil {
|
||||
return nil, openErr
|
||||
}
|
||||
defer file.Close()
|
||||
data, err = readConfigFile(file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -41,6 +48,33 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func readConfigFile(file *os.File) ([]byte, error) {
|
||||
if base.CommandEnv.CommandName != "run" {
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
path, err := filepath.Abs(file.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf":
|
||||
default:
|
||||
return nil, errors.New("unsupported config file extension: ", path)
|
||||
}
|
||||
}
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL
|
||||
// or a Unix socket HTTP endpoint.
|
||||
//
|
||||
|
||||
+10
-4
@@ -36,10 +36,6 @@ The -confdir=dir flag sets a dir with multiple json config
|
||||
The -format=json flag sets the format of config files.
|
||||
Default "auto".
|
||||
|
||||
The config root env object sets process environment variables after all config
|
||||
files are parsed. Variables needed to locate or parse config files must be set
|
||||
in the process environment before Xray starts.
|
||||
|
||||
The -test flag tells Xray to test config files only,
|
||||
without launching the server.
|
||||
|
||||
@@ -47,6 +43,9 @@ The -dump flag tells Xray to print the merged config.
|
||||
`,
|
||||
}
|
||||
|
||||
// The --lua-privileged-pubkey=key flag sets the trusted Ed25519 public key
|
||||
// for privileged local Lua scripts, encoded as unpadded Base64URL.
|
||||
|
||||
func init() {
|
||||
cmdRun.Run = executeRun // break init loop
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
|
||||
@@ -58,6 +57,8 @@ var (
|
||||
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
|
||||
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
|
||||
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
|
||||
// TODO: Restore this flag and its help text when Lua privileged signature authorization is re-enabled.
|
||||
// luaPrivilegedPublicKey = cmdRun.Flag.String("lua-privileged-pubkey", "", "Unpadded Base64URL Ed25519 public key for privileged local Lua scripts.")
|
||||
|
||||
/* We have to do this here because Golang's Test will also need to parse flag, before
|
||||
* main func in this file is run.
|
||||
@@ -214,6 +215,11 @@ func getConfigFormat() string {
|
||||
}
|
||||
|
||||
func startXray() (core.Server, error) {
|
||||
// TODO: Restore public key initialization when Lua privileged signature authorization is re-enabled.
|
||||
// if err := lua.SetPrivilegedPublicKey(*luaPrivilegedPublicKey); err != nil {
|
||||
// return nil, errors.New("failed to configure Lua privileged authorization").Base(err)
|
||||
// }
|
||||
|
||||
configFiles := getConfigFilePath(true)
|
||||
|
||||
c, err := core.LoadConfig(getConfigFormat(), configFiles)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRunConfigCommands(t *testing.T) {
|
||||
if os.Getenv("XRAY_TEST_CONFIG_COMMAND") == "1" {
|
||||
os.Args = append([]string{os.Args[0]}, flag.Args()...)
|
||||
main()
|
||||
return
|
||||
}
|
||||
dir := t.TempDir()
|
||||
config := filepath.Join(dir, "config.yaml")
|
||||
script := filepath.Join(dir, "config.lua")
|
||||
binary := filepath.Join(dir, "config.pb")
|
||||
message := filepath.Join(dir, "user.tmsg")
|
||||
for path, body := range map[string]string{
|
||||
config: "{}",
|
||||
script: "{}",
|
||||
binary: "",
|
||||
message: `{"type":"xray.app.dispatcher.Config","value":""}`,
|
||||
} {
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
file, err := os.Open(script)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
code int
|
||||
input io.Reader
|
||||
}{
|
||||
{"format override", []string{"run", "-test", "-format", "json", "-c", config}, 0, nil},
|
||||
{"invalid extension", []string{"run", "-test", "-format", "json", "-c", script}, 23, nil},
|
||||
{"default command", []string{"-test", "-format", "json", "-c", script}, 23, nil},
|
||||
{"protobuf config", []string{"run", "-test", "-c", binary}, 0, nil},
|
||||
{"protobuf error", []string{"run", "-test", "-format", "protobuf", "-c", script}, 23, nil},
|
||||
{"merged source", []string{"run", "-test", "-format", "json", "-c", config, "-c", script}, 23, nil},
|
||||
{"dump", []string{"run", "-dump", "-c", config}, 0, nil},
|
||||
{"stdin pipe", []string{"run", "-test", "-c", "stdin:"}, 0, nil},
|
||||
{"stdin file", []string{"run", "-test", "-c", "stdin:"}, 0, file},
|
||||
{"convert json", []string{"convert", "json", message}, 0, nil},
|
||||
{"convert pb", []string{"convert", "pb", "-debug", config}, 0, nil},
|
||||
{"convert pb stdin", []string{"convert", "pb", "-debug", "stdin:"}, 0, file},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
args := append([]string{"-test.run=^TestRunConfigCommands$", "--"}, tc.args...)
|
||||
cmd := exec.CommandContext(ctx, os.Args[0], args...)
|
||||
cmd.Env = append(os.Environ(), "XRAY_TEST_CONFIG_COMMAND=1")
|
||||
cmd.Stdin = strings.NewReader("{}")
|
||||
if tc.input != nil {
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Stdin = tc.input
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
if cmd.ProcessState == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code := cmd.ProcessState.ExitCode(); code != tc.code {
|
||||
t.Fatalf("exit code = %d, want %d: %v\n%s", code, tc.code, err, output)
|
||||
}
|
||||
if tc.code == 23 && !strings.Contains(string(output), "failed to load config files") {
|
||||
t.Fatalf("missing config error:\n%s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+104
-35
@@ -7,7 +7,6 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -26,6 +25,7 @@ import (
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/masque"
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
@@ -43,11 +43,15 @@ type Client struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
tunnel atomic.Pointer[tunnel]
|
||||
|
||||
mu sync.Mutex
|
||||
xmux *splithttp.XmuxManager
|
||||
dialCtx context.Context
|
||||
dialer internet.Dialer
|
||||
lastErr error
|
||||
lastErrAt time.Time
|
||||
|
||||
tunnelsMu sync.Mutex
|
||||
tunnels map[*tunnel]struct{}
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
|
||||
@@ -55,7 +59,8 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
|
||||
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
|
||||
|
||||
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
|
||||
if _, ok := streamSettings.ProtocolSettings.(*masque.Config); !ok {
|
||||
transportConfig, ok := streamSettings.ProtocolSettings.(*masque.Config)
|
||||
if !ok {
|
||||
return nil, errors.New("not masque transport")
|
||||
}
|
||||
if tls.ConfigFromStreamSettings(streamSettings) == nil {
|
||||
@@ -86,7 +91,13 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
|
||||
server: server,
|
||||
policyManager: p,
|
||||
remoteDNS: remoteDNS,
|
||||
tunnels: make(map[*tunnel]struct{}),
|
||||
}
|
||||
xmuxConfig := transportConfig.Xmux
|
||||
if xmuxConfig == nil {
|
||||
xmuxConfig = &splithttp.XmuxConfig{}
|
||||
}
|
||||
c.xmux = splithttp.NewXmuxManager(*xmuxConfig, c.newXmuxConn)
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
return c, nil
|
||||
}
|
||||
@@ -100,10 +111,11 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
ob.Name = "masque"
|
||||
ob.CanSpliceCopy = 3
|
||||
|
||||
t, err := c.getTunnel(ctx, dialer)
|
||||
xmuxClient, t, err := c.getTunnel(ctx, dialer)
|
||||
if err != nil {
|
||||
return errors.New("failed to establish CONNECT-IP tunnel").Base(err)
|
||||
}
|
||||
defer xmuxClient.DoneRunning()
|
||||
|
||||
var newCtx context.Context
|
||||
var newCancel context.CancelFunc
|
||||
@@ -180,40 +192,63 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getTunnel(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
|
||||
func (c *Client) getTunnel(ctx context.Context, dialer internet.Dialer) (*splithttp.XmuxClient, *tunnel, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.ctx.Err() != nil {
|
||||
return nil, errors.New("closed")
|
||||
}
|
||||
if t := c.tunnel.Load(); t != nil {
|
||||
select {
|
||||
case <-t.done:
|
||||
default:
|
||||
return t, nil
|
||||
}
|
||||
return nil, nil, errors.New("closed")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
var xmuxClient *splithttp.XmuxClient
|
||||
if c.lastErr != nil && time.Since(c.lastErrAt) < retryInterval {
|
||||
return nil, c.lastErr
|
||||
if xmuxClient = c.xmux.GetExistingXmuxClient(ctx); xmuxClient == nil {
|
||||
return nil, nil, c.lastErr
|
||||
}
|
||||
} else {
|
||||
c.dialCtx, c.dialer = ctx, dialer
|
||||
xmuxClient = c.xmux.GetXmuxClient(ctx)
|
||||
c.dialCtx, c.dialer = nil, nil
|
||||
if err := xmuxClient.XmuxConn.(*tunnel).err; err != nil {
|
||||
if xmuxClient = c.xmux.GetExistingXmuxClient(ctx); xmuxClient == nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
t := xmuxClient.XmuxConn.(*tunnel)
|
||||
if c.ctx.Err() != nil {
|
||||
return nil, nil, errors.New("closed")
|
||||
}
|
||||
xmuxClient.AddRunning()
|
||||
return xmuxClient, t, nil
|
||||
}
|
||||
|
||||
t, err := c.establish(ctx, dialer)
|
||||
func (c *Client) newXmuxConn() splithttp.XmuxConn {
|
||||
t, err := c.establish(c.dialCtx, c.dialer)
|
||||
if err != nil {
|
||||
c.lastErr, c.lastErrAt = err, time.Now()
|
||||
return nil, err
|
||||
return failedTunnel(err)
|
||||
}
|
||||
c.lastErr = nil
|
||||
c.tunnel.Store(t)
|
||||
if c.ctx.Err() != nil {
|
||||
if c.tunnel.CompareAndSwap(t, nil) {
|
||||
t.close()
|
||||
}
|
||||
return nil, errors.New("closed")
|
||||
c.tunnelsMu.Lock()
|
||||
if c.tunnels == nil {
|
||||
c.tunnelsMu.Unlock()
|
||||
t.close()
|
||||
return failedTunnel(errors.New("closed"))
|
||||
}
|
||||
return t, nil
|
||||
c.tunnels[t] = struct{}{}
|
||||
c.tunnelsMu.Unlock()
|
||||
if t.IsClosed() {
|
||||
c.removeTunnel(t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *Client) removeTunnel(t *tunnel) {
|
||||
c.tunnelsMu.Lock()
|
||||
delete(c.tunnels, t)
|
||||
c.tunnelsMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
|
||||
@@ -229,7 +264,7 @@ func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel
|
||||
conn.Close()
|
||||
return nil, errors.New("not a CONNECT-IP connection")
|
||||
}
|
||||
t, err := newTunnel(conn, mconn.LocalAddrs(), c.remoteDNS)
|
||||
t, err := newTunnel(conn, mconn.LocalAddrs(), c.remoteDNS, c.removeTunnel)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
@@ -240,7 +275,11 @@ func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.cancel()
|
||||
if t := c.tunnel.Swap(nil); t != nil {
|
||||
c.tunnelsMu.Lock()
|
||||
tunnels := c.tunnels
|
||||
c.tunnels = nil
|
||||
c.tunnelsMu.Unlock()
|
||||
for t := range tunnels {
|
||||
t.close()
|
||||
}
|
||||
return nil
|
||||
@@ -252,9 +291,31 @@ type tunnel struct {
|
||||
tnet *wireguard.Net
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
onClose func(*tunnel)
|
||||
err error
|
||||
}
|
||||
|
||||
func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr) (*tunnel, error) {
|
||||
func failedTunnel(err error) *tunnel {
|
||||
t := &tunnel{err: err, done: make(chan struct{})}
|
||||
t.close()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *tunnel) IsClosed() bool {
|
||||
select {
|
||||
case <-t.done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tunnel) Close() error {
|
||||
t.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr, onClose func(*tunnel)) (*tunnel, error) {
|
||||
var dns []netip.Addr
|
||||
for _, addr := range remoteDNS {
|
||||
if slices.ContainsFunc(local, func(l netip.Addr) bool { return l.Is4() == addr.Is4() }) {
|
||||
@@ -266,15 +327,16 @@ func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr)
|
||||
dns = remoteDNS
|
||||
}
|
||||
|
||||
dev, tnet, _, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, true)
|
||||
dev, tnet, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &tunnel{
|
||||
conn: conn,
|
||||
dev: dev,
|
||||
tnet: tnet,
|
||||
done: make(chan struct{}),
|
||||
conn: conn,
|
||||
dev: dev,
|
||||
tnet: tnet,
|
||||
done: make(chan struct{}),
|
||||
onClose: onClose,
|
||||
}
|
||||
go t.readFromTunnel()
|
||||
go t.writeToTunnel()
|
||||
@@ -316,8 +378,15 @@ func (t *tunnel) writeToTunnel() {
|
||||
func (t *tunnel) close() {
|
||||
t.closeOnce.Do(func() {
|
||||
close(t.done)
|
||||
t.conn.Close()
|
||||
t.dev.Close()
|
||||
if t.conn != nil {
|
||||
t.conn.Close()
|
||||
}
|
||||
if t.dev != nil {
|
||||
t.dev.Close()
|
||||
}
|
||||
if t.onClose != nil {
|
||||
t.onClose(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
)
|
||||
|
||||
func newTestClient(newConn func(c *Client) splithttp.XmuxConn) *Client {
|
||||
c := &Client{ctx: context.Background()}
|
||||
c.xmux = splithttp.NewXmuxManager(splithttp.XmuxConfig{
|
||||
MaxConnections: &splithttp.RangeConfig{From: 3, To: 3},
|
||||
}, func() splithttp.XmuxConn {
|
||||
return newConn(c)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
func TestGetTunnelFallsBack(t *testing.T) {
|
||||
refused := errors.New("refused")
|
||||
healthy := &tunnel{done: make(chan struct{})}
|
||||
dials := 0
|
||||
c := newTestClient(func(c *Client) splithttp.XmuxConn {
|
||||
if dials++; dials == 1 {
|
||||
return healthy
|
||||
}
|
||||
c.lastErr, c.lastErrAt = refused, time.Now()
|
||||
return failedTunnel(refused)
|
||||
})
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, got, err := c.getTunnel(context.Background(), nil); err != nil || got != healthy {
|
||||
t.Fatalf("flow %d: got %p, %v instead of the healthy tunnel", i, got, err)
|
||||
}
|
||||
}
|
||||
if dials != 2 {
|
||||
t.Errorf("dialed %d times, a failed tunnel should not be retried within %v", dials, retryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTunnelFailsWithoutTunnels(t *testing.T) {
|
||||
refused := errors.New("refused")
|
||||
c := newTestClient(func(c *Client) splithttp.XmuxConn {
|
||||
c.lastErr, c.lastErrAt = refused, time.Now()
|
||||
return failedTunnel(refused)
|
||||
})
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, _, err := c.getTunnel(context.Background(), nil); err != refused {
|
||||
t.Fatalf("flow %d: got %v instead of %v", i, err, refused)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-9
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
xtun "github.com/xtls/xray-core/proxy/tun"
|
||||
"github.com/xtls/xray-core/proxy/wireguard"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
@@ -166,17 +167,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
||||
if mtu == 0 {
|
||||
mtu = masque.MinPacketSize
|
||||
}
|
||||
dev, _, gstack, err := wireguard.CreateNetTUN(local, nil, mtu, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
validator: users,
|
||||
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
|
||||
ctx: core.ToBackgroundDetachedContext(ctx),
|
||||
mtu: mtu,
|
||||
dev: dev,
|
||||
pools: pools,
|
||||
local: local,
|
||||
tunnels: make(map[netip.Addr]*serverTunnel),
|
||||
@@ -187,7 +182,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
||||
if content := session.ContentFromContext(ctx); content != nil {
|
||||
s.sniffing = content.SniffingRequest
|
||||
}
|
||||
wireguard.CreateForwarder(gstack, s.handleConnection)
|
||||
dev, _, err := wireguard.CreateNetTUN(local, nil, mtu, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.dev = dev
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -500,7 +499,8 @@ func packetDestination(packet []byte) (netip.Addr, bool) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
|
||||
// HandleConnection implements tun.ConnectionHandler
|
||||
func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
|
||||
defer conn.Close()
|
||||
source := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
addr, _ := netip.AddrFromSlice(source.Address.IP())
|
||||
@@ -534,9 +534,13 @@ func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
|
||||
})
|
||||
errors.LogInfo(ctx, "processing from ", source, " to ", dest)
|
||||
|
||||
writer := buf.NewWriter(conn)
|
||||
if dest.Network == net.Network_TCP {
|
||||
writer = &xtun.TCPWriter{Writer: writer}
|
||||
}
|
||||
link := &transport.Link{
|
||||
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
|
||||
Writer: buf.NewWriter(conn),
|
||||
Writer: writer,
|
||||
}
|
||||
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
|
||||
errors.LogError(ctx, errors.New("connection closed").Base(err))
|
||||
|
||||
@@ -7,13 +7,17 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
@@ -326,3 +330,56 @@ func TestPacketDestination(t *testing.T) {
|
||||
require.False(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// stackConn is a TCP connection as the stack hands it over: it is its own buf.Writer.
|
||||
type stackConn struct {
|
||||
net.Conn
|
||||
closed atomic.Int32
|
||||
}
|
||||
|
||||
func (c *stackConn) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
|
||||
func (c *stackConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
buf.ReleaseMulti(mb)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stackConn) Close() error {
|
||||
c.closed.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stackConn) RemoteAddr() net.Addr {
|
||||
return &net.TCPAddr{IP: net.IP{10, 13, 0, 2}, Port: 1234}
|
||||
}
|
||||
|
||||
// closingDispatcher does what an outbound does when the other side has no more to send.
|
||||
type closingDispatcher struct {
|
||||
routing.Dispatcher
|
||||
conn *stackConn
|
||||
closed int32
|
||||
}
|
||||
|
||||
func (d *closingDispatcher) DispatchLink(_ context.Context, _ net.Destination, link *transport.Link) error {
|
||||
common.Close(link.Writer)
|
||||
d.closed = d.conn.closed.Load()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestServerKeepsConnectionOpenWhenWriterCloses(t *testing.T) {
|
||||
conn := &stackConn{}
|
||||
dispatcher := &closingDispatcher{conn: conn}
|
||||
s := &Server{
|
||||
ctx: context.Background(),
|
||||
dispatcher: dispatcher,
|
||||
tunnels: map[netip.Addr]*serverTunnel{netip.MustParseAddr("10.13.0.2"): newServerTunnel(nil, &protocol.MemoryUser{})},
|
||||
}
|
||||
s.HandleConnection(conn, net.TCPDestination(net.ParseAddress("192.0.2.1"), 443))
|
||||
|
||||
if dispatcher.closed != 0 {
|
||||
t.Fatal("the connection was closed with its writer, while the client may still be sending")
|
||||
}
|
||||
if got := conn.closed.Load(); got != 1 {
|
||||
t.Fatalf("connection closed %d times, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -180,14 +180,14 @@ type VisionReader struct {
|
||||
isUplink bool
|
||||
conn net.Conn
|
||||
input *bytes.Reader
|
||||
rawInput *bytes.Buffer
|
||||
rawInput **bytes.Buffer
|
||||
ob *session.Outbound
|
||||
|
||||
// internal
|
||||
directReadCounter stats.Counter
|
||||
}
|
||||
|
||||
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, ob *session.Outbound) *VisionReader {
|
||||
func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput **bytes.Buffer, ob *session.Outbound) *VisionReader {
|
||||
return &VisionReader{
|
||||
Reader: reader,
|
||||
trafficState: trafficState,
|
||||
@@ -261,12 +261,15 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() {
|
||||
buffer, _ = buf.MergeMulti(buffer, inputBuffer)
|
||||
}
|
||||
if rawInputBuffer, err := buf.ReadFrom(w.rawInput); err == nil && !rawInputBuffer.IsEmpty() {
|
||||
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
|
||||
}
|
||||
*w.input = bytes.Reader{} // release memory
|
||||
w.input = nil
|
||||
*w.rawInput = bytes.Buffer{} // release memory
|
||||
|
||||
if raw := *w.rawInput; raw != nil {
|
||||
if rawInputBuffer, err := buf.ReadFrom(raw); err == nil && !rawInputBuffer.IsEmpty() {
|
||||
buffer, _ = buf.MergeMulti(buffer, rawInputBuffer)
|
||||
}
|
||||
*raw = bytes.Buffer{} // release memory
|
||||
}
|
||||
w.rawInput = nil
|
||||
|
||||
if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil {
|
||||
|
||||
@@ -169,11 +169,17 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
|
||||
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
link := sessionItem.link.Load()
|
||||
if link != nil {
|
||||
sessionItem.UpdateConn(conn)
|
||||
} else {
|
||||
var err error
|
||||
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
|
||||
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
payloadBuf := buf.New()
|
||||
|
||||
@@ -336,11 +336,17 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
|
||||
link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
|
||||
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
link := sessionItem.link.Load()
|
||||
if link != nil {
|
||||
sessionItem.UpdateConn(conn)
|
||||
} else {
|
||||
var err error
|
||||
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
|
||||
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pBuf := buf.New()
|
||||
|
||||
@@ -253,10 +253,16 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
link, err := sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
|
||||
if err != nil {
|
||||
b.Release()
|
||||
continue
|
||||
link := sessionItem.link.Load()
|
||||
if link != nil {
|
||||
sessionItem.UpdateConn(conn)
|
||||
} else {
|
||||
var err error
|
||||
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
|
||||
if err != nil {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
||||
|
||||
@@ -169,8 +169,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
|
||||
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
|
||||
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
@@ -212,8 +212,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
|
||||
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDonePost); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,8 @@ import (
|
||||
)
|
||||
|
||||
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
|
||||
if s.currentConn.Load() == nil {
|
||||
s.currentConn.Store(conn)
|
||||
}
|
||||
if s.timer != nil {
|
||||
s.timer.Update()
|
||||
}
|
||||
s.currentConn.Store(conn)
|
||||
s.timer.Update()
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) WriteToClient(b []byte) error {
|
||||
@@ -57,19 +53,16 @@ func (s *ServerUDPSession) EnsureLink(
|
||||
policyManager policy.Manager,
|
||||
responseEncoder func(dest net.Destination, payload []byte) ([]byte, error),
|
||||
) (*transport.Link, error) {
|
||||
s.UpdateConn(conn)
|
||||
|
||||
if link := s.link.Load(); link != nil {
|
||||
return link, nil
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if link := s.link.Load(); link != nil {
|
||||
s.UpdateConn(conn)
|
||||
return link, nil
|
||||
}
|
||||
|
||||
s.currentConn.Store(conn)
|
||||
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
inbound := session.InboundFromContext(sessCtx)
|
||||
if inbound != nil && s.User != nil {
|
||||
@@ -94,7 +87,6 @@ func (s *ServerUDPSession) EnsureLink(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.link.Store(link)
|
||||
sessionPolicy := policyManager.ForLevel(level)
|
||||
s.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
if s.manager != nil {
|
||||
@@ -103,6 +95,7 @@ func (s *ServerUDPSession) EnsureLink(
|
||||
s.Close()
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
s.link.Store(link)
|
||||
|
||||
go handleUDPResponse(s, link, dest, responseEncoder)
|
||||
return link, nil
|
||||
@@ -118,19 +111,13 @@ func ResetTCPConn(conn net.Conn) {
|
||||
}
|
||||
|
||||
func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) {
|
||||
defer func() {
|
||||
if s.timer != nil {
|
||||
s.timer.SetTimeout(0)
|
||||
}
|
||||
}()
|
||||
defer s.timer.SetTimeout(0)
|
||||
for {
|
||||
resMb, err := link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if s.timer != nil {
|
||||
s.timer.Update()
|
||||
}
|
||||
s.timer.Update()
|
||||
for i, rb := range resMb {
|
||||
b := rb.Bytes()
|
||||
if encode != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/antireplay"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -629,6 +630,7 @@ func InitServerStream(conn net.Conn, method *CipherMethod, psk, saltSlice []byte
|
||||
func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.Reader, writer buf.Writer, link *transport.Link) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
defer timer.SetTimeout(0)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
@@ -644,6 +646,12 @@ func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.
|
||||
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil {
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -170,9 +170,17 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche
|
||||
return errors.New("UDP associate with listen port failed")
|
||||
}
|
||||
tempUDPConn.SetTimeout(plcy.Timeouts.ConnectionIdle)
|
||||
var udpConn stat.Connection = tempUDPConn
|
||||
if counters, ok := conn.(*stat.CounterConnection); ok {
|
||||
udpConn = &stat.CounterConnection{
|
||||
Connection: tempUDPConn,
|
||||
ReadCounter: counters.ReadCounter,
|
||||
WriteCounter: counters.WriteCounter,
|
||||
}
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- s.handleUDPPayload(ctx, tempUDPConn, dispatcher)
|
||||
errCh <- s.handleUDPPayload(ctx, udpConn, dispatcher)
|
||||
}()
|
||||
// Associated TCP keeps the UDP alive
|
||||
// Close UDP if TCP connection is closed
|
||||
|
||||
+31
-9
@@ -15,17 +15,17 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
|
||||
## DETAILS
|
||||
|
||||
By default, enabling the feature will only bring the tun interface up. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS and FreeBSD use the first IPv4 prefix from `gateway` for the point-to-point address. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` for the point-to-point address and FreeBSD for the interface address, the one after the gateway. \
|
||||
Without `gateway`, the systems differ: Xray assigns no address on Linux, Windows gives the interface link-local addresses itself (an IPv6 one at once, an IPv4 one from `169.254.0.0/16` after a few seconds), and macOS and FreeBSD use `169.254.10.1/30`. \
|
||||
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
|
||||
macOS does not configure system DNS from the `dns` field, and neither does Linux by default; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
Windows, Linux, macOS and FreeBSD can also apply system routes from `autoSystemRoutingTable`.
|
||||
macOS does not configure system DNS from the `dns` field, and neither do Linux and FreeBSD by default; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
|
||||
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
|
||||
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
|
||||
|
||||
### SYSTEM DNS ON LINUX (`autoSystemDnsToGateway`)
|
||||
### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`)
|
||||
|
||||
On Linux, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it is Linux-only.
|
||||
On Linux and FreeBSD, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it exists only on those two systems. This section describes Linux; the FreeBSD differences follow in [SYSTEM DNS ON FREEBSD](#system-dns-on-freebsd).
|
||||
|
||||
It uses `resolvectl`, which means it only works when all of these hold. Where Xray can tell that one does not, it does not start:
|
||||
|
||||
@@ -66,6 +66,24 @@ Where it cannot apply, Xray does not start, rather than run with the leak descri
|
||||
|
||||
On `Close()` the setting is reverted. It is **not** reverted if the process is killed with `SIGKILL`, since a process cannot handle that signal; run `resolvectl revert <iface>` to clean up by hand. An application that brings its own DNS endpoint is unaffected either way — this only covers the system resolver.
|
||||
|
||||
### SYSTEM DNS ON FREEBSD
|
||||
|
||||
On FreeBSD the same option goes through `resolvconf(8)`, the openresolv that ships in the base system, instead of `resolvectl`. The checks on Xray's own `dns` and routing configuration above apply unchanged, as does the loop through a `dns` upstream that bootstraps over the system resolver.
|
||||
|
||||
The address handed over is the gateway itself, not the next address: on FreeBSD the interface gets the address after the first IPv4 `gateway` (as on macOS), and the gateway, which nothing owns, is routed into the TUN, so a query to it is what Xray answers (e.g. `169.254.10.1/30` -> resolver `169.254.10.1`, local address `169.254.10.2`). Without `gateway` the config is rejected, as on Linux.
|
||||
|
||||
The entry is added with `resolvconf -a <iface> -x`, which makes the TUN's nameserver the only one in use, the equivalent of `domain ~.` plus `default-route true` above; without `-x` the nameservers DHCP supplied stay listed alongside it, and the system falls back to them. `resolvconf` exits successfully, and still records the entry, when `resolvconf=NO` is set in `resolvconf.conf`, but then runs none of the subscribers that write the resolver files, so Xray confirms the takeover in those files and does not start when the nameserver reached none of them: `/etc/resolv.conf` first, where it also has to be the only nameserver besides loopback ones (`inclusive_interfaces` in `resolvconf.conf` makes `resolvconf` ignore `-x` and keep the DHCP nameservers listed, which would be the leak again), and then, when `resolvconf.conf` names an `unbound_conf` file, that file. The latter is the `local_unbound` layout, where `local-unbound-setup` turns the `libc` subscriber off and has `resolvconf` write unbound's forwarders instead, with `resolv.conf` left pointed at loopback; the system then resolves through unbound, which forwards into the TUN. That only works with unbound's DNSSEC validation turned off, which `local_unbound` has on by default: the extra queries validation makes get no answer through the `dns` outbound, and resolution times out. A drop-in such as `/var/unbound/conf.d/xray.conf` with `server:` and `module-config: "iterator"`, followed by `service local_unbound restart`, turns it off (verified on FreeBSD 15.1).
|
||||
|
||||
| Environment | Behaviour |
|
||||
|---|---|
|
||||
| default FreeBSD, `resolv.conf` managed by `resolvconf` (DHCP or static) | applies |
|
||||
| `local_unbound` enabled | applies, through unbound's forwarders; needs validation off (see above) |
|
||||
| `resolvconf=NO` in `resolvconf.conf` | no resolver file changes, does not start |
|
||||
| `inclusive_interfaces` covering the TUN in `resolvconf.conf` | DHCP nameservers stay listed, does not start |
|
||||
| another resolver in front of `resolv.conf` that `resolvconf` is not configured to feed | nameserver reaches no resolver file, does not start |
|
||||
|
||||
On `Close()` the entry is removed with `resolvconf -d <iface>`. It is **not** removed if the process is killed with `SIGKILL`; run `resolvconf -d <iface>` to clean up by hand. The next start with the same interface name replaces the stale entry on its own.
|
||||
|
||||
Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \
|
||||
Here is simple Xray config snippet to enable the inbound:
|
||||
```
|
||||
@@ -245,10 +263,10 @@ So everything applicable for ipv4 above also works for ipv6, you only need to gi
|
||||
FreeBSD support of the same functionality is implemented through tun(4).
|
||||
|
||||
Interface name in the configuration must comply to the scheme "tunN", where N is some number. \
|
||||
It's necessary to set an IP address to the interface, ex.:
|
||||
```
|
||||
ifconfig tun0 inet 169.254.10.1/30
|
||||
```
|
||||
FreeBSD requires the tun interface to have an IPv4 address before IPv4 routes can use it. \
|
||||
By default Xray uses `169.254.10.1/30` as the gateway and assigns the next address in the prefix to the tun interface; the gateway itself belongs to nothing and is routed into the TUN. \
|
||||
You can override this by setting `gateway`; FreeBSD uses the first IPv4 prefix in the list, and IPv6 `gateway` entries are not used for addressing.
|
||||
|
||||
To attach routing to the interface, route command like following can be executed:
|
||||
```
|
||||
route add -net 1.1.1.0/24 -iface tun10
|
||||
@@ -257,6 +275,10 @@ route add -net 1.1.1.0/24 -iface tun10
|
||||
route add -inet6 -host 2606:4700:4700::1111 -iface tun10
|
||||
route add -inet6 -host 2606:4700:4700::1001 -iface tun10
|
||||
```
|
||||
Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running. \
|
||||
With `autoSystemRoutingTable`, Xray's own connections are kept out of the TUN through `autoOutboundsInterface` (the default), which on FreeBSD switches them to a second routing table (FIB 1) that mirrors the physical default route; this needs the boot tunable `net.fibs` set to at least 2, and Xray does not start otherwise. \
|
||||
`autoSystemDnsToGateway` points the system resolver at the TUN through `resolvconf(8)`, see [SYSTEM DNS ON FREEBSD](#system-dns-on-freebsd).
|
||||
|
||||
Important to remember that everything written above about Linux routing concept, also apply to FreeBSD. If you simply route default route through tun interface, that will result network loop and immediate network failure.
|
||||
|
||||
## MAC OS X SUPPORT
|
||||
|
||||
+27
-22
@@ -56,6 +56,25 @@ func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TCPWriter is the writing side of a TCP connection of the stack for the dispatcher. It has no Close: the outbound
|
||||
// closes its writer when the other side has no more to send, and the system may still be sending
|
||||
type TCPWriter struct {
|
||||
Writer buf.Writer
|
||||
// Counter counts what is written, if there is one
|
||||
Counter stats.Counter
|
||||
}
|
||||
|
||||
func (w *TCPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
size := mb.Len()
|
||||
if err := w.Writer.WriteMultiBuffer(mb); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Counter != nil {
|
||||
w.Counter.Add(int64(size))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectionHandler interface with the only method that stack is going to push new connections to
|
||||
type ConnectionHandler interface {
|
||||
HandleConnection(conn net.Conn, destination net.Destination)
|
||||
@@ -143,6 +162,7 @@ func (t *Handler) Start() error {
|
||||
|
||||
tunStackOptions := StackOptions{
|
||||
Tun: tunInterface,
|
||||
MTU: t.config.MTU,
|
||||
IdleTimeout: t.policyManager.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle,
|
||||
}
|
||||
tunStack, err := NewStack(t.ctx, tunStackOptions, t)
|
||||
@@ -194,22 +214,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
|
||||
defer cancel()
|
||||
ctx = c.ContextWithID(ctx, session.NewID())
|
||||
|
||||
// if the connection is already closed, conn.RemoteAddr() will be nil
|
||||
// due to gvisor weird behavior
|
||||
remote := conn.RemoteAddr()
|
||||
if remote == nil {
|
||||
errors.LogInfo(t.ctx, "dropped quickly closed connection")
|
||||
return
|
||||
}
|
||||
source := net.DestinationFromAddr(remote)
|
||||
isUDP := destination.Network == net.Network_UDP
|
||||
if !isUDP && (t.uplinkCounter != nil || t.downlinkCounter != nil) {
|
||||
conn = &stat.CounterConnection{
|
||||
Connection: conn,
|
||||
ReadCounter: t.uplinkCounter,
|
||||
WriteCounter: t.downlinkCounter,
|
||||
}
|
||||
}
|
||||
source := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
|
||||
inbound := session.Inbound{
|
||||
Name: "tun",
|
||||
@@ -235,13 +240,13 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
|
||||
})
|
||||
errors.LogInfo(ctx, "processing from ", source, " to ", destination)
|
||||
|
||||
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}
|
||||
// both kinds of connection are buf.Reader and buf.Writer, what they carry is counted on the way
|
||||
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn), Counter: t.uplinkCounter}
|
||||
writer := buf.NewWriter(conn)
|
||||
if isUDP {
|
||||
reader.Counter = t.uplinkCounter
|
||||
if t.downlinkCounter != nil {
|
||||
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
|
||||
}
|
||||
if destination.Network != net.Network_UDP {
|
||||
writer = &TCPWriter{Writer: writer, Counter: t.downlinkCounter}
|
||||
} else if t.downlinkCounter != nil {
|
||||
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
|
||||
}
|
||||
|
||||
link := &transport.Link{
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
xnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
@@ -103,6 +104,53 @@ func (d *testDispatcher) DispatchLink(ctx context.Context, dest xnet.Destination
|
||||
return link.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(d.writePayload)})
|
||||
}
|
||||
|
||||
// closableConn is a connection that is its own writer, as the ones of the stack are
|
||||
type closableConn struct {
|
||||
*testConn
|
||||
closed int32
|
||||
}
|
||||
|
||||
func (c *closableConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
buf.ReleaseMulti(mb)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *closableConn) Close() error {
|
||||
atomic.AddInt32(&c.closed, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// closingDispatcher does what an outbound does when the other side has no more to send
|
||||
type closingDispatcher struct {
|
||||
testDispatcher
|
||||
conn *closableConn
|
||||
closed int32
|
||||
}
|
||||
|
||||
func (d *closingDispatcher) DispatchLink(_ context.Context, _ xnet.Destination, link *transport.Link) error {
|
||||
common.Close(link.Writer)
|
||||
d.closed = atomic.LoadInt32(&d.conn.closed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHandlerClosesTunConnectionWhenBothSidesAreDone(t *testing.T) {
|
||||
conn := &closableConn{testConn: newTestConn(nil)}
|
||||
dispatcher := &closingDispatcher{conn: conn}
|
||||
handler := &Handler{
|
||||
ctx: context.Background(),
|
||||
config: &Config{},
|
||||
dispatcher: dispatcher,
|
||||
}
|
||||
handler.HandleConnection(conn, xnet.TCPDestination(xnet.LocalHostIP, 443))
|
||||
|
||||
if dispatcher.closed != 0 {
|
||||
t.Fatal("the connection was closed with its writer, while the system may still be sending")
|
||||
}
|
||||
if got := atomic.LoadInt32(&conn.closed); got != 1 {
|
||||
t.Fatalf("connection closed %d times, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerCountsTunConnectionTraffic(t *testing.T) {
|
||||
uplinkCounter := new(testCounter)
|
||||
downlinkCounter := new(testCounter)
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package icmp
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
)
|
||||
|
||||
func ProtocolLabel(netProto tcpip.NetworkProtocolNumber) string {
|
||||
switch netProto {
|
||||
case header.IPv4ProtocolNumber:
|
||||
return "ipv4"
|
||||
case header.IPv6ProtocolNumber:
|
||||
return "ipv6"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func ParseEchoRequest(netProto tcpip.NetworkProtocolNumber, message []byte) (uint16, uint16, bool) {
|
||||
switch netProto {
|
||||
case header.IPv4ProtocolNumber:
|
||||
if len(message) < header.ICMPv4MinimumSize {
|
||||
return 0, 0, false
|
||||
}
|
||||
icmpHdr := header.ICMPv4(message)
|
||||
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
|
||||
return 0, 0, false
|
||||
}
|
||||
return icmpHdr.Ident(), icmpHdr.Sequence(), true
|
||||
case header.IPv6ProtocolNumber:
|
||||
if len(message) < header.ICMPv6MinimumSize {
|
||||
return 0, 0, false
|
||||
}
|
||||
icmpHdr := header.ICMPv6(message)
|
||||
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
|
||||
return 0, 0, false
|
||||
}
|
||||
return icmpHdr.Ident(), icmpHdr.Sequence(), true
|
||||
default:
|
||||
return 0, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func RewriteChecksum(netProto tcpip.NetworkProtocolNumber, message []byte, srcIP, dstIP tcpip.Address) error {
|
||||
switch netProto {
|
||||
case header.IPv4ProtocolNumber:
|
||||
if len(message) < header.ICMPv4MinimumSize {
|
||||
return errors.New("invalid icmpv4 packet")
|
||||
}
|
||||
icmpHdr := header.ICMPv4(message)
|
||||
icmpHdr.SetChecksum(0)
|
||||
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0)))
|
||||
return nil
|
||||
case header.IPv6ProtocolNumber:
|
||||
if len(message) < header.ICMPv6MinimumSize {
|
||||
return errors.New("invalid icmpv6 packet")
|
||||
}
|
||||
icmpHdr := header.ICMPv6(message)
|
||||
icmpHdr.SetChecksum(0)
|
||||
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
|
||||
Header: icmpHdr[:header.ICMPv6MinimumSize],
|
||||
Src: srcIP,
|
||||
Dst: dstIP,
|
||||
PayloadCsum: checksum.Checksum(icmpHdr.Payload(), 0),
|
||||
PayloadLen: len(icmpHdr.Payload()),
|
||||
}))
|
||||
return nil
|
||||
default:
|
||||
return errors.New("unsupported icmp network protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func BuildLocalEchoReply(netProto tcpip.NetworkProtocolNumber, request []byte, srcIP, dstIP tcpip.Address) ([]byte, error) {
|
||||
reply := append([]byte(nil), request...)
|
||||
|
||||
switch netProto {
|
||||
case header.IPv4ProtocolNumber:
|
||||
if len(reply) < header.ICMPv4MinimumSize {
|
||||
return nil, errors.New("invalid icmpv4 echo packet")
|
||||
}
|
||||
icmpHdr := header.ICMPv4(reply)
|
||||
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != header.ICMPv4UnusedCode {
|
||||
return nil, errors.New("not an icmpv4 echo request")
|
||||
}
|
||||
reply[0] = byte(header.ICMPv4EchoReply)
|
||||
case header.IPv6ProtocolNumber:
|
||||
if len(reply) < header.ICMPv6MinimumSize {
|
||||
return nil, errors.New("invalid icmpv6 echo packet")
|
||||
}
|
||||
icmpHdr := header.ICMPv6(reply)
|
||||
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != header.ICMPv6UnusedCode {
|
||||
return nil, errors.New("not an icmpv6 echo request")
|
||||
}
|
||||
reply[0] = byte(header.ICMPv6EchoReply)
|
||||
default:
|
||||
return nil, errors.New("unsupported icmp network protocol")
|
||||
}
|
||||
|
||||
if err := RewriteChecksum(netProto, reply, srcIP, dstIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package icmp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
)
|
||||
|
||||
func TestParseEchoRequest(t *testing.T) {
|
||||
t.Run("ipv4 echo", func(t *testing.T) {
|
||||
var zero tcpip.Address
|
||||
packet := []byte{
|
||||
byte(header.ICMPv4Echo), 0,
|
||||
0, 0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb,
|
||||
}
|
||||
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ident, sequence, ok := ParseEchoRequest(header.IPv4ProtocolNumber, packet)
|
||||
if !ok {
|
||||
t.Fatal("expected ipv4 echo request to parse")
|
||||
}
|
||||
if ident != 0x1234 || sequence != 0x5678 {
|
||||
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ipv6 echo", func(t *testing.T) {
|
||||
packet := []byte{
|
||||
byte(header.ICMPv6EchoRequest), 0,
|
||||
0, 0,
|
||||
0xab, 0xcd,
|
||||
0xef, 0x01,
|
||||
0xaa, 0xbb,
|
||||
}
|
||||
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
|
||||
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ident, sequence, ok := ParseEchoRequest(header.IPv6ProtocolNumber, packet)
|
||||
if !ok {
|
||||
t.Fatal("expected ipv6 echo request to parse")
|
||||
}
|
||||
if ident != 0xabcd || sequence != 0xef01 {
|
||||
t.Fatalf("unexpected ident/sequence: %x/%x", ident, sequence)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRewriteChecksum(t *testing.T) {
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
var zero tcpip.Address
|
||||
packet := []byte{
|
||||
byte(header.ICMPv4Echo), 0,
|
||||
0xff, 0xff,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
if err := RewriteChecksum(header.IPv4ProtocolNumber, packet, zero, zero); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
icmpHdr := header.ICMPv4(packet)
|
||||
if got, want := icmpHdr.Checksum(), header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksumPayloadV4(icmpHdr.Payload())); got != want {
|
||||
t.Fatalf("unexpected ipv4 checksum: got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ipv6", func(t *testing.T) {
|
||||
packet := []byte{
|
||||
byte(header.ICMPv6EchoReply), 0,
|
||||
0xff, 0xff,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
|
||||
if err := RewriteChecksum(header.IPv6ProtocolNumber, packet, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
icmpHdr := header.ICMPv6(packet)
|
||||
want := header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
|
||||
Header: icmpHdr[:header.ICMPv6MinimumSize],
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
PayloadLen: len(icmpHdr.Payload()),
|
||||
PayloadCsum: checksumPayloadV6(icmpHdr.Payload()),
|
||||
})
|
||||
if got := icmpHdr.Checksum(); got != want {
|
||||
t.Fatalf("unexpected ipv6 checksum: got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildLocalEchoReply(t *testing.T) {
|
||||
t.Run("ipv4", func(t *testing.T) {
|
||||
request := []byte{
|
||||
byte(header.ICMPv4Echo), 0,
|
||||
0, 0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
src := tcpip.Address{}
|
||||
dst := tcpip.Address{}
|
||||
if err := RewriteChecksum(header.IPv4ProtocolNumber, request, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reply, err := BuildLocalEchoReply(header.IPv4ProtocolNumber, request, dst, src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if request[0] != byte(header.ICMPv4Echo) {
|
||||
t.Fatal("request mutated")
|
||||
}
|
||||
icmpHdr := header.ICMPv4(reply)
|
||||
if icmpHdr.Type() != header.ICMPv4EchoReply || icmpHdr.Code() != header.ICMPv4UnusedCode {
|
||||
t.Fatalf("unexpected ipv4 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
|
||||
}
|
||||
if icmpHdr.Ident() != 0x1234 || icmpHdr.Sequence() != 0x5678 {
|
||||
t.Fatalf("unexpected ipv4 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ipv6", func(t *testing.T) {
|
||||
request := []byte{
|
||||
byte(header.ICMPv6EchoRequest), 0,
|
||||
0, 0,
|
||||
0xab, 0xcd,
|
||||
0xef, 0x01,
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
src := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
dst := tcpip.AddrFromSlice([]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
|
||||
if err := RewriteChecksum(header.IPv6ProtocolNumber, request, src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reply, err := BuildLocalEchoReply(header.IPv6ProtocolNumber, request, dst, src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if request[0] != byte(header.ICMPv6EchoRequest) {
|
||||
t.Fatal("request mutated")
|
||||
}
|
||||
icmpHdr := header.ICMPv6(reply)
|
||||
if icmpHdr.Type() != header.ICMPv6EchoReply || icmpHdr.Code() != header.ICMPv6UnusedCode {
|
||||
t.Fatalf("unexpected ipv6 reply type/code: %d/%d", icmpHdr.Type(), icmpHdr.Code())
|
||||
}
|
||||
if icmpHdr.Ident() != 0xabcd || icmpHdr.Sequence() != 0xef01 {
|
||||
t.Fatalf("unexpected ipv6 ident/sequence: %x/%x", icmpHdr.Ident(), icmpHdr.Sequence())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func checksumPayloadV4(payload []byte) uint16 {
|
||||
return checksum.Checksum(payload, 0)
|
||||
}
|
||||
|
||||
func checksumPayloadV6(payload []byte) uint16 {
|
||||
return checksum.Checksum(payload, 0)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"math/bits"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4HeaderSize = 20
|
||||
ipv6HeaderSize = 40
|
||||
tcpHeaderSize = 20
|
||||
udpHeaderSize = 8
|
||||
|
||||
protoICMPv4 = 1
|
||||
protoTCP = 6
|
||||
protoUDP = 17
|
||||
protoICMPv6 = 58
|
||||
|
||||
// the smallest that a link is believed to take whatever is said about it: what every IPv4 host takes,
|
||||
// and the smallest link that IPv6 runs on
|
||||
ipv4SmallestMTU = 576
|
||||
ipv6SmallestMTU = 1280
|
||||
|
||||
// the header that an IPv6 packet has in front of each of its fragments, and its number
|
||||
fragmentHeaderSize = 8
|
||||
protoFragment = 44
|
||||
// fragmentsMost is how many packets can be waiting for the rest of their fragments,
|
||||
// fragmentsTime how long they are waited for at most
|
||||
fragmentsMost = 64
|
||||
fragmentsTime = 30 * time.Second
|
||||
)
|
||||
|
||||
// packet is an IP packet with its header read
|
||||
type packet struct {
|
||||
proto byte
|
||||
src, dst netip.Addr
|
||||
// payload is what follows the IP header
|
||||
payload []byte
|
||||
// fragment is set if payload is a part of what follows the IP header: the one at offset of the packet
|
||||
// with the number id, which has more after it or not
|
||||
fragment bool
|
||||
more bool
|
||||
offset int
|
||||
id uint32
|
||||
}
|
||||
|
||||
// parsePacket reads the IP header of b
|
||||
func parsePacket(b []byte) (p packet, ok bool) {
|
||||
if len(b) < ipv4HeaderSize {
|
||||
return p, false
|
||||
}
|
||||
switch b[0] >> 4 {
|
||||
case 4:
|
||||
size := int(b[0]&0xf) * 4
|
||||
total := int(binary.BigEndian.Uint16(b[2:]))
|
||||
if size < ipv4HeaderSize || total < size || total > len(b) {
|
||||
return p, false
|
||||
}
|
||||
if f := binary.BigEndian.Uint16(b[6:]); f&0x3fff != 0 {
|
||||
p.fragment, p.more, p.offset, p.id = true, f&0x2000 != 0, int(f&0x1fff)*8, uint32(binary.BigEndian.Uint16(b[4:]))
|
||||
}
|
||||
p.proto = b[9]
|
||||
p.src = netip.AddrFrom4([4]byte(b[12:16]))
|
||||
p.dst = netip.AddrFrom4([4]byte(b[16:20]))
|
||||
p.payload = b[size:total]
|
||||
case 6:
|
||||
if len(b) < ipv6HeaderSize {
|
||||
return p, false
|
||||
}
|
||||
total := ipv6HeaderSize + int(binary.BigEndian.Uint16(b[4:]))
|
||||
if total > len(b) {
|
||||
return p, false
|
||||
}
|
||||
p.proto = b[6]
|
||||
p.src = netip.AddrFrom16([16]byte(b[8:24]))
|
||||
p.dst = netip.AddrFrom16([16]byte(b[24:40]))
|
||||
p.payload = b[ipv6HeaderSize:total]
|
||||
// hop-by-hop options, routing, destination options
|
||||
for p.proto == 0 || p.proto == 43 || p.proto == 60 {
|
||||
if len(p.payload) < 8 {
|
||||
return p, false
|
||||
}
|
||||
size := int(p.payload[1])*8 + 8
|
||||
if size > len(p.payload) {
|
||||
return p, false
|
||||
}
|
||||
p.proto, p.payload = p.payload[0], p.payload[size:]
|
||||
}
|
||||
if p.proto == protoFragment {
|
||||
f := p.payload
|
||||
if len(f) < fragmentHeaderSize {
|
||||
return p, false
|
||||
}
|
||||
p.fragment, p.more, p.offset, p.id = true, f[3]&1 != 0, int(binary.BigEndian.Uint16(f[2:])&^7), binary.BigEndian.Uint32(f[4:])
|
||||
p.proto, p.payload = f[0], f[fragmentHeaderSize:]
|
||||
}
|
||||
default:
|
||||
return p, false
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
// verified reports whether the checksum of what the packet carries is right
|
||||
func (p packet) verified() bool {
|
||||
data := p.payload
|
||||
switch p.proto {
|
||||
case protoICMPv4:
|
||||
return checksum(sum(data, 0)) == 0
|
||||
case protoUDP:
|
||||
if len(data) < udpHeaderSize {
|
||||
return false
|
||||
}
|
||||
// the packet may be longer than what it carries
|
||||
size := int(binary.BigEndian.Uint16(data[4:]))
|
||||
if size < udpHeaderSize || size > len(data) {
|
||||
return false
|
||||
}
|
||||
if p.src.Is4() && data[6]|data[7] == 0 {
|
||||
// zero stands for no checksum
|
||||
return true
|
||||
}
|
||||
data = data[:size]
|
||||
case protoTCP, protoICMPv6:
|
||||
default:
|
||||
return true
|
||||
}
|
||||
pseudo := uint64(p.proto) + uint64(len(data))
|
||||
if p.src.Is4() {
|
||||
src, dst := p.src.As4(), p.dst.As4()
|
||||
pseudo = sum(dst[:], sum(src[:], pseudo))
|
||||
} else {
|
||||
src, dst := p.src.As16(), p.dst.As16()
|
||||
pseudo = sum(dst[:], sum(src[:], pseudo))
|
||||
}
|
||||
return checksum(sum(data, pseudo)) == 0
|
||||
}
|
||||
|
||||
// putIPHeader writes the header of a packet from src to dst into b, without its length, and returns the size of the header
|
||||
func putIPHeader(b []byte, proto byte, src, dst netip.Addr) int {
|
||||
if src.Is4() {
|
||||
s, d := src.As4(), dst.As4()
|
||||
clear(b[:ipv4HeaderSize])
|
||||
b[0] = 0x45
|
||||
if proto == protoTCP {
|
||||
b[6] = 0x40 // don't fragment
|
||||
}
|
||||
b[8] = 64
|
||||
b[9] = proto
|
||||
copy(b[12:], s[:])
|
||||
copy(b[16:], d[:])
|
||||
return ipv4HeaderSize
|
||||
}
|
||||
s, d := src.As16(), dst.As16()
|
||||
clear(b[:ipv6HeaderSize])
|
||||
b[0] = 0x60
|
||||
b[6] = proto
|
||||
b[7] = 64
|
||||
copy(b[8:], s[:])
|
||||
copy(b[24:], d[:])
|
||||
return ipv6HeaderSize
|
||||
}
|
||||
|
||||
// setIPLength completes the header ip of a packet that is size bytes long with it
|
||||
func setIPLength(ip []byte, size int) {
|
||||
if len(ip) == ipv4HeaderSize {
|
||||
binary.BigEndian.PutUint16(ip[2:], uint16(size))
|
||||
ip[10], ip[11] = 0, 0
|
||||
binary.BigEndian.PutUint16(ip[10:], checksum(sum(ip, 0)))
|
||||
return
|
||||
}
|
||||
binary.BigEndian.PutUint16(ip[4:], uint16(size-ipv6HeaderSize))
|
||||
}
|
||||
|
||||
// transportChecksum is the checksum of a TCP or UDP segment, header and payload, of a packet with the header ip.
|
||||
// The checksum field of header is zero
|
||||
func transportChecksum(ip []byte, proto byte, header, payload []byte) uint16 {
|
||||
return checksum(sum(payload, sum(header, pseudoSum(ip, proto, len(header)+len(payload)))))
|
||||
}
|
||||
|
||||
// pseudoSum is what the header ip of a packet adds to the checksum of its TCP or UDP segment of size bytes
|
||||
func pseudoSum(ip []byte, proto byte, size int) uint64 {
|
||||
addresses := ip[12:ipv4HeaderSize]
|
||||
if len(ip) == ipv6HeaderSize {
|
||||
addresses = ip[8:]
|
||||
}
|
||||
return sum(addresses, uint64(proto)+uint64(size))
|
||||
}
|
||||
|
||||
// sum adds the 16-bit words of b to s, with the carry of every addition added back
|
||||
func sum(b []byte, s uint64) uint64 {
|
||||
var c uint64
|
||||
for len(b) >= 32 {
|
||||
s, c = bits.Add64(s, binary.BigEndian.Uint64(b), c)
|
||||
s, c = bits.Add64(s, binary.BigEndian.Uint64(b[8:]), c)
|
||||
s, c = bits.Add64(s, binary.BigEndian.Uint64(b[16:]), c)
|
||||
s, c = bits.Add64(s, binary.BigEndian.Uint64(b[24:]), c)
|
||||
b = b[32:]
|
||||
}
|
||||
for len(b) >= 8 {
|
||||
s, c = bits.Add64(s, binary.BigEndian.Uint64(b), c)
|
||||
b = b[8:]
|
||||
}
|
||||
if len(b) >= 4 {
|
||||
s, c = bits.Add64(s, uint64(binary.BigEndian.Uint32(b)), c)
|
||||
b = b[4:]
|
||||
}
|
||||
if len(b) >= 2 {
|
||||
s, c = bits.Add64(s, uint64(binary.BigEndian.Uint16(b)), c)
|
||||
b = b[2:]
|
||||
}
|
||||
if len(b) == 1 {
|
||||
s, c = bits.Add64(s, uint64(b[0])<<8, c)
|
||||
}
|
||||
s, c = bits.Add64(s, 0, c)
|
||||
return s + c
|
||||
}
|
||||
|
||||
// checksum folds s into the 16 bits that go into a header
|
||||
func checksum(s uint64) uint16 {
|
||||
s = s>>32 + s&0xffffffff
|
||||
s = s>>16 + s&0xffff
|
||||
s = s>>16 + s&0xffff
|
||||
s = s>>16 + s&0xffff
|
||||
return ^uint16(s)
|
||||
}
|
||||
|
||||
// writeFragments writes a packet that is larger than mtu to tun in fragments that are not. header is its IP header
|
||||
// with what follows that up to payload, which is no more than the header of a UDP packet. id is the number
|
||||
// of the packet if it is an IPv6 one
|
||||
func writeFragments(tun Tun, mtu int, id uint32, header, payload []byte) error {
|
||||
var b [ipv6HeaderSize + fragmentHeaderSize + udpHeaderSize]byte
|
||||
size := ipv4HeaderSize
|
||||
if header[0]>>4 == 6 {
|
||||
size = ipv6HeaderSize
|
||||
}
|
||||
n := copy(b[:], header[:size])
|
||||
if size == ipv6HeaderSize {
|
||||
// the number goes into a header of its own, behind the IP header. That of IPv4 has it
|
||||
b[6], b[n] = protoFragment, header[6]
|
||||
binary.BigEndian.PutUint32(b[n+4:], id)
|
||||
n += fragmentHeaderSize
|
||||
}
|
||||
// first goes in front of payload, in the first fragment
|
||||
first := header[size:]
|
||||
if n+len(first) > len(b) {
|
||||
return errors.New("no room for a header of ", len(first), " bytes in a fragment")
|
||||
}
|
||||
// every fragment but the last is a number of 8 bytes
|
||||
room, total := (mtu-n)&^7, len(first)+len(payload)
|
||||
if room < 8 {
|
||||
return errors.New("no room for a fragment in an MTU of ", mtu)
|
||||
}
|
||||
for offset := 0; offset < total; {
|
||||
h, part := b[:n], min(room, total-offset)
|
||||
last := offset+part == total
|
||||
if size == ipv6HeaderSize {
|
||||
f := uint16(offset)
|
||||
if !last {
|
||||
f |= 1
|
||||
}
|
||||
binary.BigEndian.PutUint16(b[size+2:], f)
|
||||
} else {
|
||||
f := uint16(offset / 8)
|
||||
if !last {
|
||||
f |= 0x2000
|
||||
}
|
||||
binary.BigEndian.PutUint16(b[6:], f)
|
||||
}
|
||||
setIPLength(b[:size], n+part)
|
||||
data := payload[max(offset-len(first), 0):min(offset+part-len(first), len(payload))]
|
||||
if offset == 0 {
|
||||
h = b[:n+copy(b[n:], first)]
|
||||
}
|
||||
if err := tun.WritePacket(h, data); err != nil {
|
||||
return err
|
||||
}
|
||||
offset += part
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fragmented is a packet of which not every fragment has come yet
|
||||
type fragmented struct {
|
||||
// data is what follows its IP header, as far as its last fragment that has come goes
|
||||
data []byte
|
||||
// parts are what of data has come, in order and apart from each other
|
||||
parts [][2]int
|
||||
// size is that of data when all of it has come, known from the last fragment
|
||||
size int
|
||||
known bool
|
||||
since time.Time
|
||||
}
|
||||
|
||||
// reassembly puts the packets that come in fragments together
|
||||
type reassembly struct {
|
||||
mu sync.Mutex
|
||||
packets map[fragments]*fragmented
|
||||
}
|
||||
|
||||
// fragments is what the fragments of a packet have in common
|
||||
type fragments struct {
|
||||
src, dst netip.Addr
|
||||
proto byte
|
||||
id uint32
|
||||
}
|
||||
|
||||
// add takes the fragment p of a packet. With the last one that was missing, it returns the packet.
|
||||
// A packet whose fragments do not fit each other is given up
|
||||
func (r *reassembly) add(p packet) (packet, bool) {
|
||||
fragment, offset, end := p.payload, p.offset, p.offset+len(p.payload)
|
||||
// what an IP packet can carry
|
||||
most := math.MaxUint16
|
||||
if p.src.Is4() {
|
||||
most -= ipv4HeaderSize
|
||||
}
|
||||
if len(fragment) == 0 || end > most || p.more && len(fragment)%8 != 0 {
|
||||
return p, false
|
||||
}
|
||||
key, now := fragments{p.src, p.dst, p.proto, p.id}, time.Now()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
f := r.packets[key]
|
||||
if f != nil && now.Sub(f.since) > fragmentsTime {
|
||||
// what is left of a packet whose other fragments were lost: this is one of another packet with its number
|
||||
f = nil
|
||||
}
|
||||
if f == nil {
|
||||
if _, stale := r.packets[key]; !stale && len(r.packets) >= fragmentsMost {
|
||||
// the one that is waited for the longest makes room, not the one that comes now
|
||||
var longest fragments
|
||||
var since time.Time
|
||||
for other, old := range r.packets {
|
||||
if since.IsZero() || old.since.Before(since) {
|
||||
longest, since = other, old.since
|
||||
}
|
||||
}
|
||||
delete(r.packets, longest)
|
||||
}
|
||||
if r.packets == nil {
|
||||
r.packets = make(map[fragments]*fragmented)
|
||||
}
|
||||
f = &fragmented{since: now}
|
||||
r.packets[key] = f
|
||||
}
|
||||
for _, part := range f.parts {
|
||||
if offset >= part[0] && end <= part[1] {
|
||||
// came already
|
||||
return p, false
|
||||
}
|
||||
if offset < part[1] && part[0] < end {
|
||||
// fragments of one packet do not overlap: these are of two, or made to be taken for something else
|
||||
delete(r.packets, key)
|
||||
return p, false
|
||||
}
|
||||
}
|
||||
if f.known && end > f.size || !p.more && len(f.data) > end {
|
||||
// past its end, or an end before what came
|
||||
delete(r.packets, key)
|
||||
return p, false
|
||||
}
|
||||
if !p.more {
|
||||
f.size, f.known = end, true
|
||||
}
|
||||
if len(f.data) < end {
|
||||
f.data = append(f.data, make([]byte, end-len(f.data))...)
|
||||
}
|
||||
copy(f.data[offset:], fragment)
|
||||
i := 0
|
||||
for i < len(f.parts) && f.parts[i][1] < offset {
|
||||
i++
|
||||
}
|
||||
j := i
|
||||
for ; j < len(f.parts) && f.parts[j][0] <= end; j++ {
|
||||
offset, end = min(offset, f.parts[j][0]), max(end, f.parts[j][1])
|
||||
}
|
||||
f.parts = slices.Replace(f.parts, i, j, [2]int{offset, end})
|
||||
if !f.known || f.parts[0] != [2]int{0, f.size} {
|
||||
return p, false
|
||||
}
|
||||
delete(r.packets, key)
|
||||
p.payload, p.fragment = f.data[:f.size], false
|
||||
return p, true
|
||||
}
|
||||
@@ -1,17 +1,544 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
// Stack interface implement ip protocol stack, bridging raw network packets and data streams
|
||||
type Stack interface {
|
||||
// Start makes the stack read the packets of its tun
|
||||
Start() error
|
||||
Close() error
|
||||
// Input takes a packet of a tun that hands them over instead of being read. The packet is not kept,
|
||||
// but it may be written to. The packets of one connection have to come one after another
|
||||
Input(packet []byte)
|
||||
// DialTCP opens a connection to address from the one of StackOptions.Addresses that is of its family
|
||||
DialTCP(ctx context.Context, address netip.AddrPort) (net.Conn, error)
|
||||
// DialUDP opens a port on StackOptions.Addresses. What anyone sends to the port is read from it
|
||||
DialUDP() (net.PacketConn, error)
|
||||
}
|
||||
|
||||
// StackOptions for the stack implementation
|
||||
type StackOptions struct {
|
||||
Tun Tun
|
||||
MTU uint32
|
||||
IdleTimeout time.Duration
|
||||
// Addresses are what the stack opens connections from
|
||||
Addresses []netip.Addr
|
||||
// Remote is set if the tun is a tunnel to another system and not the tun of this one: what comes through it
|
||||
// has come a long way, so its checksums are looked at, nothing but that way limits the windows, and the
|
||||
// segments of TCP carry the times of both sides if the other side has them
|
||||
Remote bool
|
||||
}
|
||||
|
||||
// flow is what the packets of one connection have in common, src is the side of the system
|
||||
type flow struct {
|
||||
src, dst netip.AddrPort
|
||||
}
|
||||
|
||||
// stack turns the packets that the system sends to the tun into connections and back.
|
||||
// It has only what the two need to talk through a tun, and a local link when the system passes on
|
||||
// what others send, see tcpConn. The tun may also be a tunnel to a system that is far away, and the stack
|
||||
// the one that opens the connections
|
||||
type stack struct {
|
||||
ctx context.Context
|
||||
tun Tun
|
||||
mtu int
|
||||
// segmenter is the tun if it takes segments that are larger than its MTU
|
||||
segmenter segmenter
|
||||
// window is the window of every connection
|
||||
window int32
|
||||
// verify is set if the checksums of what comes are looked at
|
||||
verify bool
|
||||
// stamps is set if connections carry the times of RFC 7323 when the other side does
|
||||
stamps bool
|
||||
// handle is given every new connection that the other side opens. Without it those are refused
|
||||
handle func(conn net.Conn, destination net.Destination)
|
||||
udp *udpConnectionHandler
|
||||
// addresses are what connections are opened from
|
||||
addresses []netip.Addr
|
||||
|
||||
mu sync.Mutex
|
||||
conns map[flow]*tcpConn
|
||||
// sockets are the ports that DialUDP has opened
|
||||
sockets map[netip.AddrPort]*udpSocket
|
||||
closed bool
|
||||
// opening is how many of conns wait for the answer to their SYN or SYN-ACK
|
||||
opening atomic.Int32
|
||||
// fragments are the packets that are larger than a link on their way and come in parts.
|
||||
// identification numbers the ones that leave like that: a counter for each of a few groups of destinations,
|
||||
// each from a start of its own, so that no two of them that go one way meet, and no destination learns how much
|
||||
// goes to the others
|
||||
fragments reassembly
|
||||
identification [64]atomic.Uint32
|
||||
}
|
||||
|
||||
// NewStack builds new ip stack. Without a handler it takes no connections, only opens them
|
||||
func NewStack(ctx context.Context, options StackOptions, handler ConnectionHandler) (Stack, error) {
|
||||
s := &stack{
|
||||
ctx: ctx,
|
||||
tun: options.Tun,
|
||||
mtu: int(options.MTU),
|
||||
window: tcpWindow,
|
||||
addresses: options.Addresses,
|
||||
verify: options.Remote,
|
||||
stamps: options.Remote,
|
||||
conns: make(map[flow]*tcpConn),
|
||||
sockets: make(map[netip.AddrPort]*udpSocket),
|
||||
}
|
||||
if t, ok := options.Tun.(segmenter); ok && t.Segments() {
|
||||
s.segmenter = t
|
||||
s.window = tcpLargeWindow
|
||||
}
|
||||
if options.Remote {
|
||||
s.window = tcpLargeWindow
|
||||
}
|
||||
for i := range s.identification {
|
||||
s.identification[i].Store(rand.Uint32())
|
||||
}
|
||||
if handler != nil {
|
||||
s.handle = handler.HandleConnection
|
||||
s.udp = newUdpConnectionHandler(s.handle, s.writeUDP)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Start is called by Handler to bring stack to life
|
||||
func (s *stack) Start() error {
|
||||
go s.read()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is called by Handler to shut down the stack
|
||||
func (s *stack) Close() error {
|
||||
s.mu.Lock()
|
||||
s.closed = true
|
||||
conns := make([]*tcpConn, 0, len(s.conns))
|
||||
for _, c := range s.conns {
|
||||
conns = append(conns, c)
|
||||
}
|
||||
sockets := s.sockets
|
||||
s.sockets = nil
|
||||
s.mu.Unlock()
|
||||
for _, c := range conns {
|
||||
c.mu.Lock()
|
||||
if c.err == nil {
|
||||
c.reset()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
for _, u := range sockets {
|
||||
u.queue.close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// read takes the packets of the tun until it is closed
|
||||
func (s *stack) read() {
|
||||
// a tun that does not split segments gives them as large as a packet can be
|
||||
b := make([]byte, packetRoom+1<<16)
|
||||
for {
|
||||
b, err := s.tun.ReadPacket(b)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if !closed {
|
||||
errors.LogErrorInner(s.ctx, err, "[tun] failed to read a packet, no more are read")
|
||||
}
|
||||
return
|
||||
}
|
||||
s.Input(b)
|
||||
}
|
||||
}
|
||||
|
||||
// Input implements Stack. Several tunnels may hand over their packets at once
|
||||
func (s *stack) Input(b []byte) {
|
||||
p, ok := parsePacket(b)
|
||||
if !ok || p.src.Is4In6() || p.dst.Is4In6() || p.src.IsLoopback() || p.dst.IsLoopback() || p.dst.IsUnspecified() || p.src.IsMulticast() {
|
||||
// nothing comes from many at once, and an address that stands for this machine itself is not one
|
||||
// that the other side of a tun can ask for. Nor is an IPv6 address that stands for an IPv4 one, which
|
||||
// would be dialed as that: ::ffff:0.0.0.0 is this machine as well
|
||||
return
|
||||
}
|
||||
if s.verify && b[0]>>4 == 4 && checksum(sum(b[:int(b[0]&0xf)*4], 0)) != 0 {
|
||||
return
|
||||
}
|
||||
// what was put together may be of two packets with one number, its checksum tells
|
||||
verify := s.verify || p.fragment
|
||||
if p.fragment {
|
||||
p, ok = s.fragments.add(p)
|
||||
}
|
||||
if !ok || verify && !p.verified() {
|
||||
return
|
||||
}
|
||||
switch p.proto {
|
||||
case protoTCP:
|
||||
s.handleTCP(p)
|
||||
case protoUDP:
|
||||
s.handleUDP(p)
|
||||
case protoICMPv4, protoICMPv6:
|
||||
s.handleICMP(p)
|
||||
}
|
||||
}
|
||||
|
||||
// local returns the address that packets to remote are sent from
|
||||
func (s *stack) local(remote netip.Addr) (netip.Addr, error) {
|
||||
for _, a := range s.addresses {
|
||||
if a.Is4() == remote.Is4() {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, errors.New("no address to reach ", remote, " from")
|
||||
}
|
||||
|
||||
// ephemeral returns a port for what is opened from here: the first that free takes, from a random one on
|
||||
func ephemeral(free func(port uint16) bool) (uint16, bool) {
|
||||
start := rand.Uint32()
|
||||
for i := range uint32(1 << 15) {
|
||||
if port := uint16(1<<15 | (start+i)&(1<<15-1)); free(port) {
|
||||
return port, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (s *stack) handleTCP(p packet) {
|
||||
t := p.payload
|
||||
if len(t) < tcpHeaderSize {
|
||||
return
|
||||
}
|
||||
size := int(t[12]>>4) * 4
|
||||
if size < tcpHeaderSize || size > len(t) {
|
||||
return
|
||||
}
|
||||
f := flow{
|
||||
src: netip.AddrPortFrom(p.src, binary.BigEndian.Uint16(t)),
|
||||
dst: netip.AddrPortFrom(p.dst, binary.BigEndian.Uint16(t[2:])),
|
||||
}
|
||||
seg := segment{
|
||||
seq: binary.BigEndian.Uint32(t[4:]),
|
||||
ack: binary.BigEndian.Uint32(t[8:]),
|
||||
flags: t[13],
|
||||
window: binary.BigEndian.Uint16(t[14:]),
|
||||
options: t[tcpHeaderSize:size],
|
||||
payload: t[size:],
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
c := s.conns[f]
|
||||
s.mu.Unlock()
|
||||
if c != nil && c.input(seg) {
|
||||
return
|
||||
}
|
||||
if seg.flags&(tcpSyn|tcpAck|tcpRst) != tcpSyn || s.handle == nil {
|
||||
if seg.flags&tcpRst == 0 {
|
||||
s.refuse(f, seg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if s.opening.Load() >= tcpOpening {
|
||||
return
|
||||
}
|
||||
c = newTCPConn(s, f)
|
||||
c.synchronize(seg)
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.conns[f] = c
|
||||
s.mu.Unlock()
|
||||
s.opening.Add(1)
|
||||
c.mu.Lock()
|
||||
c.sendSyn()
|
||||
c.arm(tcpTick)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// refuse answers a segment of a connection that is not known with a reset
|
||||
func (s *stack) refuse(f flow, seg segment) {
|
||||
var b [ipv6HeaderSize + tcpHeaderSize]byte
|
||||
n := putIPHeader(b[:], protoTCP, f.dst.Addr(), f.src.Addr())
|
||||
t := b[n : n+tcpHeaderSize]
|
||||
binary.BigEndian.PutUint16(t, f.dst.Port())
|
||||
binary.BigEndian.PutUint16(t[2:], f.src.Port())
|
||||
t[12] = tcpHeaderSize / 4 << 4
|
||||
if seg.flags&tcpAck != 0 {
|
||||
binary.BigEndian.PutUint32(t[4:], seg.ack)
|
||||
t[13] = tcpRst
|
||||
} else {
|
||||
ack := seg.seq + uint32(len(seg.payload))
|
||||
if seg.flags&(tcpSyn|tcpFin) != 0 {
|
||||
ack++
|
||||
}
|
||||
binary.BigEndian.PutUint32(t[8:], ack)
|
||||
t[13] = tcpRst | tcpAck
|
||||
}
|
||||
setIPLength(b[:n], n+tcpHeaderSize)
|
||||
binary.BigEndian.PutUint16(t[16:], transportChecksum(b[:n], protoTCP, t, nil))
|
||||
_ = s.tun.WritePacket(b[:n+tcpHeaderSize], nil)
|
||||
}
|
||||
|
||||
// remove forgets a connection. c.mu is held
|
||||
func (s *stack) remove(c *tcpConn) {
|
||||
if !c.established {
|
||||
s.opening.Add(-1)
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.conns[c.flow] == c {
|
||||
delete(s.conns, c.flow)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *stack) handleUDP(p packet) {
|
||||
u := p.payload
|
||||
if len(u) < udpHeaderSize {
|
||||
return
|
||||
}
|
||||
size := int(binary.BigEndian.Uint16(u[4:]))
|
||||
if size < udpHeaderSize || size > len(u) {
|
||||
return
|
||||
}
|
||||
var socket *udpSocket
|
||||
if s.handle == nil {
|
||||
// only for a port that was opened from here
|
||||
s.mu.Lock()
|
||||
socket = s.sockets[netip.AddrPortFrom(p.dst, binary.BigEndian.Uint16(u[2:]))]
|
||||
s.mu.Unlock()
|
||||
if socket == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// the packet waits for its reader, longer than read keeps it
|
||||
b := buf.NewWithSize(int32(size - udpHeaderSize))
|
||||
b.Write(u[udpHeaderSize:size])
|
||||
src := net.UDPDestination(net.IPAddress(p.src.AsSlice()), net.Port(binary.BigEndian.Uint16(u)))
|
||||
if socket == nil {
|
||||
s.udp.HandlePacket(src, net.UDPDestination(net.IPAddress(p.dst.AsSlice()), net.Port(binary.BigEndian.Uint16(u[2:]))), b)
|
||||
} else if b.UDP = &src; !socket.queue.push(b) {
|
||||
b.Release()
|
||||
}
|
||||
}
|
||||
|
||||
// writeUDP writes a packet with the payload from src to dst to the tun
|
||||
func (s *stack) writeUDP(payload []byte, src net.Destination, dst net.Destination) error {
|
||||
srcIP, _ := netip.AddrFromSlice(src.Address.IP())
|
||||
dstIP, _ := netip.AddrFromSlice(dst.Address.IP())
|
||||
return s.sendUDP(payload, netip.AddrPortFrom(srcIP, uint16(src.Port)), netip.AddrPortFrom(dstIP, uint16(dst.Port)))
|
||||
}
|
||||
|
||||
func (s *stack) sendUDP(payload []byte, src, dst netip.AddrPort) error {
|
||||
srcIP, dstIP := src.Addr().Unmap(), dst.Addr().Unmap()
|
||||
if !srcIP.IsValid() || srcIP.Is4() != dstIP.Is4() || len(payload) > math.MaxUint16-udpHeaderSize {
|
||||
return errors.New("failed to write udp packet of ", len(payload), " bytes from ", src, " to ", dst)
|
||||
}
|
||||
var b [ipv6HeaderSize + udpHeaderSize]byte
|
||||
n := putIPHeader(b[:], protoUDP, srcIP, dstIP)
|
||||
u := b[n : n+udpHeaderSize]
|
||||
binary.BigEndian.PutUint16(u, src.Port())
|
||||
binary.BigEndian.PutUint16(u[2:], dst.Port())
|
||||
binary.BigEndian.PutUint16(u[4:], uint16(udpHeaderSize+len(payload)))
|
||||
check := transportChecksum(b[:n], protoUDP, u, payload)
|
||||
if check == 0 {
|
||||
// zero stands for no checksum
|
||||
check = 0xffff
|
||||
}
|
||||
binary.BigEndian.PutUint16(u[6:], check)
|
||||
return s.write(b[:n+udpHeaderSize], payload)
|
||||
}
|
||||
|
||||
// write completes the IP header of a packet that is in two parts and writes it to the tun, in fragments if it is
|
||||
// larger than the MTU. A segment of a connection never is, and is not written by this
|
||||
func (s *stack) write(header, payload []byte) error {
|
||||
size, last := ipv6HeaderSize, 39
|
||||
if header[0]>>4 == 4 {
|
||||
size, last = ipv4HeaderSize, 19
|
||||
}
|
||||
// the group is chosen by the last bytes of the destination
|
||||
id := s.identification[(header[last]^header[last-1]<<3)%byte(len(s.identification))].Add(1)
|
||||
if header[0]>>4 == 4 {
|
||||
// a link on the way may split it as well, and what is put together there is told apart by its number
|
||||
binary.BigEndian.PutUint16(header[4:], uint16(id))
|
||||
}
|
||||
if len(header)+len(payload) > s.mtu {
|
||||
return writeFragments(s.tun, s.mtu, id, header, payload)
|
||||
}
|
||||
setIPLength(header[:size], len(header)+len(payload))
|
||||
return s.tun.WritePacket(header, payload)
|
||||
}
|
||||
|
||||
// DialUDP implements Stack
|
||||
func (s *stack) DialUDP() (net.PacketConn, error) {
|
||||
u := &udpSocket{stack: s, queue: newPacketQueue(udpQueueLimit)}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
port, ok := ephemeral(func(port uint16) bool {
|
||||
for _, a := range s.addresses {
|
||||
if s.sockets[netip.AddrPortFrom(a, port)] != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !ok || len(s.addresses) == 0 {
|
||||
return nil, errors.New("no port left on ", s.addresses)
|
||||
}
|
||||
u.port = port
|
||||
for _, a := range s.addresses {
|
||||
s.sockets[netip.AddrPortFrom(a, port)] = u
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// udpSocket is a UDP port that was opened from here, on every address of the stack. It is not bound to one
|
||||
// other side: what anyone sends to it is read from it, with who sent it
|
||||
type udpSocket struct {
|
||||
stack *stack
|
||||
port uint16
|
||||
queue *packetQueue
|
||||
}
|
||||
|
||||
func (u *udpSocket) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
b, ok := u.queue.pop()
|
||||
if !ok {
|
||||
return 0, nil, gonet.ErrClosed
|
||||
}
|
||||
defer b.Release()
|
||||
return copy(p, b.Bytes()), b.UDP.RawNetAddr(), nil
|
||||
}
|
||||
|
||||
func (u *udpSocket) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
to, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
return 0, errors.New("not a udp address: ", addr)
|
||||
}
|
||||
local, err := u.stack.local(to.AddrPort().Addr().Unmap())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := u.stack.sendUDP(p, netip.AddrPortFrom(local, u.port), to.AddrPort()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (u *udpSocket) Close() error {
|
||||
u.stack.mu.Lock()
|
||||
for _, a := range u.stack.addresses {
|
||||
if local := netip.AddrPortFrom(a, u.port); u.stack.sockets[local] == u {
|
||||
delete(u.stack.sockets, local)
|
||||
}
|
||||
}
|
||||
u.stack.mu.Unlock()
|
||||
u.queue.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr implements net.PacketConn with the first address of the stack
|
||||
func (u *udpSocket) LocalAddr() net.Addr {
|
||||
return gonet.UDPAddrFromAddrPort(netip.AddrPortFrom(u.stack.addresses[0], u.port))
|
||||
}
|
||||
|
||||
func (u *udpSocket) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *udpSocket) SetReadDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *udpSocket) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleICMP answers an echo request as its destination would, and takes the word that a packet of a connection
|
||||
// was too large for a link on its way or found no way at all
|
||||
func (s *stack) handleICMP(p packet) {
|
||||
m := p.payload
|
||||
request, reply := byte(8), byte(0)
|
||||
if p.proto == protoICMPv6 {
|
||||
request, reply = 128, 129
|
||||
}
|
||||
if len(m) < 8 {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case p.proto == protoICMPv4 && m[0] == 3 && m[1] == 4:
|
||||
s.undelivered(m[8:], max(int(binary.BigEndian.Uint16(m[6:])), ipv4SmallestMTU))
|
||||
return
|
||||
case p.proto == protoICMPv6 && m[0] == 2:
|
||||
s.undelivered(m[8:], max(int(binary.BigEndian.Uint32(m[4:])), ipv6SmallestMTU))
|
||||
return
|
||||
case p.proto == protoICMPv4 && m[0] == 3, p.proto == protoICMPv6 && m[0] == 1:
|
||||
s.undelivered(m[8:], 0)
|
||||
return
|
||||
}
|
||||
if m[0] != request || m[1] != 0 || s.handle == nil && !slices.Contains(s.addresses, p.dst) {
|
||||
// a stack that takes no connections is one host, not all of them
|
||||
return
|
||||
}
|
||||
var b [ipv6HeaderSize]byte
|
||||
n := putIPHeader(b[:], p.proto, p.dst, p.src)
|
||||
// the reply is the request with another type
|
||||
m[0], m[2], m[3] = reply, 0, 0
|
||||
if p.proto == protoICMPv6 {
|
||||
binary.BigEndian.PutUint16(m[2:], transportChecksum(b[:n], protoICMPv6, m, nil))
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(m[2:], checksum(sum(m, 0)))
|
||||
}
|
||||
_ = s.write(b[:n], m)
|
||||
}
|
||||
|
||||
// undelivered takes the start of a packet that was sent from here and did not get to where it goes. If it did not fit
|
||||
// a link on its way, which takes packets of mtu bytes, the connection that sent it goes on with smaller segments.
|
||||
// Without an mtu there is no way at all, and a connection that is being opened is not waited for
|
||||
func (s *stack) undelivered(b []byte, mtu int) {
|
||||
var proto byte
|
||||
var local, remote netip.Addr
|
||||
switch {
|
||||
case len(b) >= ipv4HeaderSize && b[0]>>4 == 4:
|
||||
proto, local, remote = b[9], netip.AddrFrom4([4]byte(b[12:16])), netip.AddrFrom4([4]byte(b[16:20]))
|
||||
b = b[min(int(b[0]&0xf)*4, len(b)):]
|
||||
case len(b) >= ipv6HeaderSize && b[0]>>4 == 6:
|
||||
proto, local, remote = b[6], netip.AddrFrom16([16]byte(b[8:24])), netip.AddrFrom16([16]byte(b[24:40]))
|
||||
b = b[ipv6HeaderSize:]
|
||||
}
|
||||
// of the segment there are the ports and the sequence number at least
|
||||
if proto != protoTCP || len(b) < 8 {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
c := s.conns[flow{
|
||||
src: netip.AddrPortFrom(remote, binary.BigEndian.Uint16(b[2:])),
|
||||
dst: netip.AddrPortFrom(local, binary.BigEndian.Uint16(b)),
|
||||
}]
|
||||
s.mu.Unlock()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if seq := binary.BigEndian.Uint32(b[4:]); mtu != 0 {
|
||||
c.shrink(seq, mtu)
|
||||
} else {
|
||||
c.unreachable(seq)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultNIC tcpip.NICID = 1
|
||||
|
||||
tcpRXBufMinSize = tcp.MinBufferSize
|
||||
tcpRXBufDefSize = tcp.DefaultSendBufferSize
|
||||
tcpRXBufMaxSize = 8 << 20 // 8MiB
|
||||
|
||||
tcpTXBufMinSize = tcp.MinBufferSize
|
||||
tcpTXBufDefSize = tcp.DefaultReceiveBufferSize
|
||||
tcpTXBufMaxSize = 6 << 20 // 6MiB
|
||||
)
|
||||
|
||||
// stackGVisor is ip stack implemented by gVisor package
|
||||
type stackGVisor struct {
|
||||
ctx context.Context
|
||||
tun Tun
|
||||
idleTimeout time.Duration
|
||||
handler *Handler
|
||||
stack *stack.Stack
|
||||
endpoint stack.LinkEndpoint
|
||||
}
|
||||
|
||||
// NewStack builds new ip stack (using gVisor)
|
||||
func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) {
|
||||
gStack := &stackGVisor{
|
||||
ctx: ctx,
|
||||
tun: options.Tun,
|
||||
idleTimeout: options.IdleTimeout,
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
return gStack, nil
|
||||
}
|
||||
|
||||
// Start is called by Handler to bring stack to life
|
||||
func (t *stackGVisor) Start() error {
|
||||
linkEndpoint, err := t.tun.newEndpoint()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ipStack, err := createStack(linkEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tcpForwarder := tcp.NewForwarder(ipStack, 0, 65535, func(r *tcp.ForwarderRequest) {
|
||||
go func(r *tcp.ForwarderRequest) {
|
||||
var wq waiter.Queue
|
||||
id := r.ID()
|
||||
|
||||
// Perform a TCP three-way handshake.
|
||||
ep, err := r.CreateEndpoint(&wq)
|
||||
if err != nil {
|
||||
errors.LogError(t.ctx, err.String())
|
||||
r.Complete(true)
|
||||
return
|
||||
}
|
||||
|
||||
options := ep.SocketOptions()
|
||||
options.SetKeepAlive(false)
|
||||
options.SetReuseAddress(true)
|
||||
options.SetReusePort(true)
|
||||
|
||||
t.handler.HandleConnection(
|
||||
gonet.NewTCPConn(&wq, ep),
|
||||
// local address on the gVisor side is connection destination
|
||||
net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)),
|
||||
)
|
||||
|
||||
// close the socket
|
||||
ep.Close()
|
||||
// send connection complete upstream
|
||||
r.Complete(false)
|
||||
}(r)
|
||||
})
|
||||
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
|
||||
|
||||
// Use custom UDP packet handler, instead of strict gVisor forwarder, for FullCone NAT support
|
||||
udpForwarder := newUdpConnectionHandler(t.handler.HandleConnection, t.writeRawUDPPacket)
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
|
||||
data := pkt.Data().AsRange().ToSlice()
|
||||
// if len(data) == 0 {
|
||||
// return false
|
||||
// }
|
||||
// source/destination of the packet we process as incoming, on gVisor side are Remote/Local
|
||||
// in other terms, src is the side behind tun, dst is the side behind gVisor
|
||||
// this function handle packets passing from the tun to the gVisor, therefore the src/dst assignement
|
||||
srcIP := net.IPAddress(id.RemoteAddress.AsSlice())
|
||||
dstIP := net.IPAddress(id.LocalAddress.AsSlice())
|
||||
if srcIP == nil || dstIP == nil {
|
||||
panic(id)
|
||||
}
|
||||
src := net.UDPDestination(srcIP, net.Port(id.RemotePort))
|
||||
dst := net.UDPDestination(dstIP, net.Port(id.LocalPort))
|
||||
udpForwarder.HandlePacket(src, dst, data)
|
||||
return true
|
||||
})
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, t.handleICMPv4Packet)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, t.handleICMPv6Packet)
|
||||
|
||||
t.stack = ipStack
|
||||
t.endpoint = linkEndpoint
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *stackGVisor) writeRawUDPPacket(payload []byte, src net.Destination, dst net.Destination) error {
|
||||
udpLen := header.UDPMinimumSize + len(payload)
|
||||
srcIP := tcpip.AddrFromSlice(src.Address.IP())
|
||||
dstIP := tcpip.AddrFromSlice(dst.Address.IP())
|
||||
|
||||
// build packet with appropriate IP header size
|
||||
isIPv4 := dst.Address.Family().IsIPv4()
|
||||
ipHdrSize := header.IPv6MinimumSize
|
||||
ipProtocol := header.IPv6ProtocolNumber
|
||||
if isIPv4 {
|
||||
ipHdrSize = header.IPv4MinimumSize
|
||||
ipProtocol = header.IPv4ProtocolNumber
|
||||
}
|
||||
|
||||
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize,
|
||||
Payload: buffer.MakeWithData(payload),
|
||||
})
|
||||
defer pkt.DecRef()
|
||||
|
||||
// Build UDP header
|
||||
udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
|
||||
udpHdr.Encode(&header.UDPFields{
|
||||
SrcPort: uint16(src.Port),
|
||||
DstPort: uint16(dst.Port),
|
||||
Length: uint16(udpLen),
|
||||
})
|
||||
|
||||
// Calculate and set UDP checksum
|
||||
xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen))
|
||||
udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum)))
|
||||
|
||||
// Build IP header
|
||||
if isIPv4 {
|
||||
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
|
||||
ipHdr.Encode(&header.IPv4Fields{
|
||||
TotalLength: uint16(header.IPv4MinimumSize + udpLen),
|
||||
TTL: 64,
|
||||
Protocol: uint8(header.UDPProtocolNumber),
|
||||
SrcAddr: srcIP,
|
||||
DstAddr: dstIP,
|
||||
})
|
||||
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
|
||||
} else {
|
||||
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
|
||||
ipHdr.Encode(&header.IPv6Fields{
|
||||
PayloadLength: uint16(udpLen),
|
||||
TransportProtocol: header.UDPProtocolNumber,
|
||||
HopLimit: 64,
|
||||
SrcAddr: srcIP,
|
||||
DstAddr: dstIP,
|
||||
})
|
||||
}
|
||||
|
||||
// dispatch the packet
|
||||
err := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView()))
|
||||
if err != nil {
|
||||
return errors.New("failed to write raw udp packet back to stack", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is called by Handler to shut down the stack
|
||||
func (t *stackGVisor) Close() error {
|
||||
if t.stack == nil {
|
||||
return nil
|
||||
}
|
||||
t.endpoint.Attach(nil)
|
||||
t.stack.Close()
|
||||
for _, endpoint := range t.stack.CleanupEndpoints() {
|
||||
endpoint.Abort()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStack configure gVisor ip stack
|
||||
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
|
||||
opts := stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
|
||||
HandleLocal: false,
|
||||
}
|
||||
gStack := stack.New(opts)
|
||||
|
||||
err := gStack.CreateNIC(defaultNIC, ep)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.String())
|
||||
}
|
||||
|
||||
gStack.SetRouteTable([]tcpip.Route{
|
||||
{Destination: header.IPv4EmptySubnet, NIC: defaultNIC},
|
||||
{Destination: header.IPv6EmptySubnet, NIC: defaultNIC},
|
||||
})
|
||||
|
||||
err = gStack.SetSpoofing(defaultNIC, true)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.String())
|
||||
}
|
||||
err = gStack.SetPromiscuousMode(defaultNIC, true)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.String())
|
||||
}
|
||||
|
||||
cOpt := tcpip.CongestionControlOption("cubic")
|
||||
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &cOpt)
|
||||
sOpt := tcpip.TCPSACKEnabled(true)
|
||||
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt)
|
||||
mOpt := tcpip.TCPModerateReceiveBufferOption(true)
|
||||
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &mOpt)
|
||||
|
||||
// Disable RACK/TLP loss recovery to fix connection stalls under high load
|
||||
rOpt := tcpip.TCPRecovery(0)
|
||||
gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &rOpt)
|
||||
|
||||
tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{
|
||||
Min: tcpRXBufMinSize,
|
||||
Default: tcpRXBufDefSize,
|
||||
Max: tcpRXBufMaxSize,
|
||||
}
|
||||
err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.String())
|
||||
}
|
||||
|
||||
tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{
|
||||
Min: tcpTXBufMinSize,
|
||||
Default: tcpTXBufDefSize,
|
||||
Max: tcpTXBufMaxSize,
|
||||
}
|
||||
err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.String())
|
||||
}
|
||||
|
||||
return gStack, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user