mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-15 02:29:21 +03:00
Compare commits
21
Commits
main
...
lua-sandbox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b408f7f0c7 | ||
|
|
ef4f4888df | ||
|
|
c2a9b074f4 | ||
|
|
9726bbc9d7 | ||
|
|
6cd0c8a414 | ||
|
|
796bbd763a | ||
|
|
e04191e65b | ||
|
|
bd28a87f0b | ||
|
|
a04770587e | ||
|
|
eb9c6030ab | ||
|
|
e4124d3dbb | ||
|
|
4c0f3a94c5 | ||
|
|
19842ed0c2 | ||
|
|
34efc32ff0 | ||
|
|
4828ed8270 | ||
|
|
e84f3065dc | ||
|
|
fa9dc21a55 | ||
|
|
590a5236bb | ||
|
|
1aae252fc3 | ||
|
|
20c4c8f956 | ||
|
|
4c571f5e06 |
+1
-1
@@ -52,7 +52,7 @@ func luaServers(s *DNS) []luaDNSServer {
|
|||||||
|
|
||||||
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
|
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
|
||||||
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
||||||
pushIPs := xlua.NewSlicePusher[net.IP](L)
|
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
|
||||||
|
|
||||||
serverList := L.CreateTable(len(servers), 0)
|
serverList := L.CreateTable(len(servers), 0)
|
||||||
for i, client := range servers {
|
for i, client := range servers {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import (
|
|||||||
|
|
||||||
const idleTimeout = 30 * time.Second
|
const idleTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
// DO NOT EDIT. Ignored by geodata, rejected by Lua.
|
||||||
|
const luaGuardHeader = "\x12\x01\x00"
|
||||||
|
|
||||||
type stage struct {
|
type stage struct {
|
||||||
target string
|
target string
|
||||||
temp 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 {
|
if err := d.fetch(asset.Url, temp); err != nil {
|
||||||
temp.Close()
|
temp.Close()
|
||||||
return stage{}, err
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -62,7 +62,7 @@ func (r *Router) RegisterLua(L *lua.LState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func registerLuaContext(L *lua.LState) {
|
func registerLuaContext(L *lua.LState) {
|
||||||
pushIPs := xlua.NewSlicePusher[net.IP](L)
|
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
|
||||||
attributes := L.NewTypeMetatable(luaAttributesType)
|
attributes := L.NewTypeMetatable(luaAttributesType)
|
||||||
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
|
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||||
values := L.CheckUserData(1).Value.(map[string]string)
|
values := L.CheckUserData(1).Value.(map[string]string)
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func checkFile(file, code string) error {
|
func checkFile(file, code string) error {
|
||||||
r, err := filesystem.OpenAsset(file)
|
r, size, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to open ", file).Base(err)
|
return errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
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 errors.New("failed to check code ", code, " from ", file).Base(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -30,12 +30,12 @@ func checkFile(file, code string) error {
|
|||||||
|
|
||||||
func loadFile(file, code string) ([]byte, error) {
|
func loadFile(file, code string) ([]byte, error) {
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
r, err := filesystem.OpenAsset(file)
|
r, size, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to open ", file).Base(err)
|
return nil, errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
bs, err := find(r, []byte(code), true)
|
bs, err := find(r, []byte(code), true, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
|
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.
|
// unmarshalling it into a []*Domain, so value is only valid during fn.
|
||||||
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
|
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
r, err := filesystem.OpenAsset(file)
|
r, size, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to open ", file).Base(err)
|
return errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
br := bufio.NewReaderSize(r, 64*1024)
|
br := bufio.NewReaderSize(r, 64*1024)
|
||||||
n, err := seek(br, []byte(code))
|
n, err := seek(br, []byte(code), size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to load code ", code, " from ", file).Base(err)
|
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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if shift == 63 && b > 1 {
|
||||||
|
break // Only one value bit fits in the tenth byte.
|
||||||
|
}
|
||||||
x |= (uint64(b) & 0x7F) << shift
|
x |= (uint64(b) & 0x7F) << shift
|
||||||
if (b & 0x80) == 0 {
|
if (b & 0x80) == 0 {
|
||||||
return x, nil
|
return x, nil
|
||||||
@@ -123,9 +126,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
|
|||||||
return 0, errors.New("varint overflow")
|
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)
|
br := bufio.NewReaderSize(r, 64*1024)
|
||||||
bodyL, err := seek(br, code)
|
bodyL, err := seek(br, code, size)
|
||||||
if err != nil || !readBody {
|
if err != nil || !readBody {
|
||||||
return nil, err
|
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.
|
// 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)
|
codeL := len(code)
|
||||||
if codeL == 0 {
|
if codeL == 0 {
|
||||||
return 0, errors.New("empty code")
|
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
|
need := 2 + codeL // TODO: if code too long
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if _, err := br.ReadByte(); err != nil {
|
tag, err := br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +157,10 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
bodyL := int(x)
|
if x == 0 || x > uint64(size) || x > uint64(^uint(0)>>1) {
|
||||||
if bodyL <= 0 {
|
return 0, errors.New("invalid body length: ", x)
|
||||||
return 0, errors.New("invalid body length: ", bodyL)
|
|
||||||
}
|
}
|
||||||
|
bodyL := int(x)
|
||||||
|
|
||||||
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single
|
// 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.
|
// 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
|
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
|
return bodyL, nil
|
||||||
}
|
}
|
||||||
if _, err := br.Discard(bodyL); err != nil {
|
if _, err := br.Discard(bodyL); err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package geodata
|
package geodata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,6 +13,58 @@ import (
|
|||||||
"google.golang.org/protobuf/proto"
|
"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 siteEntry struct {
|
||||||
Type Domain_Type
|
Type Domain_Type
|
||||||
Value string
|
Value string
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ func RegisterLua(L *lua.LState) {
|
|||||||
"Match": luaIPMatch,
|
"Match": luaIPMatch,
|
||||||
"AnyMatch": luaIPAnyMatch,
|
"AnyMatch": luaIPAnyMatch,
|
||||||
"Matches": luaIPMatches,
|
"Matches": luaIPMatches,
|
||||||
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[net.IP](L)),
|
"FilterIPs": newLuaIPFilterIPs(xlua.NewReadOnlySlicePusher[net.IP](L)),
|
||||||
})
|
}, "SetReverse", "ToggleReverse")
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ local geodata = require("xray.geodata")
|
|||||||
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
|
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
|
||||||
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
|
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
|
||||||
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
|
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
|
||||||
|
assert(matcher.Reload == nil and matcher.reload == nil)
|
||||||
assert(matcher:match(ip))
|
assert(matcher:match(ip))
|
||||||
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
|
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
|
||||||
assert(not matcher:AnyMatch(nil))
|
assert(not matcher:AnyMatch(nil))
|
||||||
@@ -97,6 +98,11 @@ assert(matcher.missing == nil)
|
|||||||
|
|
||||||
local domain = geodata.BuildDomainMatcher("full:example.com")
|
local domain = geodata.BuildDomainMatcher("full:example.com")
|
||||||
assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny)
|
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:matchAny("example.com"))
|
||||||
assert(#domain:Match("example.com") == 1)
|
assert(#domain:Match("example.com") == 1)
|
||||||
assert(domain:match("example.com")[1] == 0)
|
assert(domain:match("example.com")[1] == 0)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
-3
@@ -1,6 +1,9 @@
|
|||||||
package lua
|
package lua
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
luar "layeh.com/gopher-luar"
|
luar "layeh.com/gopher-luar"
|
||||||
)
|
)
|
||||||
@@ -22,21 +25,88 @@ func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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")
|
||||||
|
methods.ForEach(func(key, method glua.LValue) {
|
||||||
|
if method == appendText {
|
||||||
|
methods.RawSet(key, glua.LNil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
visit(reflect.TypeFor[net.IPMask]())
|
||||||
|
}
|
||||||
|
visit(typ.Elem())
|
||||||
|
}
|
||||||
|
visit(reflect.TypeFor[S]())
|
||||||
|
}
|
||||||
|
|
||||||
// DirectMethod handles a Lua call without luar's reflected method invocation.
|
// 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 returns the result count and whether it handled the arguments. On false,
|
||||||
// it must leave the stack unchanged for the original luar wrapper.
|
// it must leave the stack unchanged for the original luar wrapper.
|
||||||
type DirectMethod func(L *glua.LState) (nresults int, handled bool)
|
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.
|
// PushWithDirectMethods pushes a luar userdata with typed Go method bindings.
|
||||||
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
|
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
|
||||||
// uses the methods table directly instead of luar's reflected __index handler.
|
// uses the methods table directly instead of luar's reflected __index handler.
|
||||||
// value must expose methods only. Bindings and their closures are installed once
|
// Only directMethods and additionalMethods are exposed, including their aliases.
|
||||||
// per Go type per LState, outside the method-call hot path.
|
// Bindings and their closures are installed once per Go type per LState, outside
|
||||||
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod) {
|
// 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)
|
userdata := luar.New(L, value).(*glua.LUserData)
|
||||||
metatable := userdata.Metatable.(*glua.LTable)
|
metatable := userdata.Metatable.(*glua.LTable)
|
||||||
methods := metatable.RawGetString("methods").(*glua.LTable)
|
methods := metatable.RawGetString("methods").(*glua.LTable)
|
||||||
if metatable.RawGetString("__index") != methods {
|
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 {
|
for name, direct := range directMethods {
|
||||||
original := methods.RawGetString(name)
|
original := methods.RawGetString(name)
|
||||||
fn := L.NewFunction(func(L *glua.LState) int {
|
fn := L.NewFunction(func(L *glua.LState) int {
|
||||||
@@ -52,6 +122,10 @@ func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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)
|
metatable.RawSetString("__index", methods)
|
||||||
}
|
}
|
||||||
L.Push(userdata)
|
L.Push(userdata)
|
||||||
|
|||||||
@@ -47,6 +47,141 @@ assert(ok and result == true)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
readOnly := luar.MT(L, ips[0]).RawGetString("__newindex")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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) {
|
func TestSlicePusherMetatablePerState(t *testing.T) {
|
||||||
first := glua.NewState()
|
first := glua.NewState()
|
||||||
defer first.Close()
|
defer first.Close()
|
||||||
|
|||||||
@@ -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,82 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
-4
@@ -3,11 +3,14 @@ package lua
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/base64"
|
"github.com/xtls/xray-core/common/base64"
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
"github.com/xtls/xray-core/common/platform"
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
"github.com/yuin/gopher-lua/parse"
|
"github.com/yuin/gopher-lua/parse"
|
||||||
@@ -15,7 +18,8 @@ import (
|
|||||||
|
|
||||||
// Program holds immutable bytecode that can be run by independent LStates.
|
// Program holds immutable bytecode that can be run by independent LStates.
|
||||||
type Program struct {
|
type Program struct {
|
||||||
proto *glua.FunctionProto
|
proto *glua.FunctionProto
|
||||||
|
privileged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LStateFactory returns a fully initialized state or nil and an error.
|
// LStateFactory returns a fully initialized state or nil and an error.
|
||||||
@@ -27,10 +31,15 @@ func Compile(script string) (*Program, error) {
|
|||||||
var code []byte
|
var code []byte
|
||||||
var err error
|
var err error
|
||||||
name := "[inline]"
|
name := "[inline]"
|
||||||
|
privileged := false
|
||||||
if strings.HasPrefix(script, base64.GzipPrefix) || strings.HasPrefix(script, base64.Prefix) {
|
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)
|
code, err = base64.Decode(script)
|
||||||
} else if name, err = platform.ResolveLuaFile(script); err == nil {
|
} else if name, err = platform.ResolveLuaFile(script); err == nil {
|
||||||
code, err = os.ReadFile(name)
|
if code, err = os.ReadFile(name); err == nil {
|
||||||
|
privileged, err = verifyPrivilegedSignature(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -44,14 +53,19 @@ func Compile(script string) (*Program, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Program{proto: proto}, nil
|
return &Program{proto: proto, privileged: privileged}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewState creates a state, runs register, executes the program under ctx, and
|
// NewState creates a state, runs register, executes the program under ctx, and
|
||||||
// runs validate. It removes the initialization context before returning a state
|
// runs validate. It removes the initialization context before returning a state
|
||||||
// owned by the caller.
|
// owned by the caller.
|
||||||
func (p *Program) NewState(ctx context.Context, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
|
func (p *Program) NewState(ctx context.Context, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
|
||||||
L := glua.NewState()
|
var L *glua.LState
|
||||||
|
if p.privileged {
|
||||||
|
L = glua.NewState()
|
||||||
|
} else {
|
||||||
|
L = newSandboxedState()
|
||||||
|
}
|
||||||
valid := false
|
valid := false
|
||||||
defer func() {
|
defer func() {
|
||||||
if !valid {
|
if !valid {
|
||||||
@@ -63,6 +77,10 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
|
|||||||
if register != nil {
|
if register != nil {
|
||||||
register(L)
|
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))
|
L.Push(L.NewFunctionFromProto(p.proto))
|
||||||
// Execute the Lua script's top level.
|
// Execute the Lua script's top level.
|
||||||
if err := L.PCall(0, 0, nil); err != nil {
|
if err := L.PCall(0, 0, nil); err != nil {
|
||||||
@@ -77,6 +95,144 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
|
|||||||
return L, nil
|
return L, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSandboxedState() *glua.LState {
|
||||||
|
L := glua.NewState(glua.Options{SkipOpenLibs: true})
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return glua.LString(redactMemoryAddresses(string(message)))
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
osLib.ForEach(func(key, _ glua.LValue) {
|
||||||
|
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 {
|
||||||
|
return memoryAddressPattern.ReplaceAllString(message, "<address>")
|
||||||
|
}
|
||||||
|
|
||||||
// NewStateFactory returns a factory that gives each state an initialization timeout.
|
// NewStateFactory returns a factory that gives each state an initialization timeout.
|
||||||
func (p *Program) NewStateFactory(initTimeout time.Duration, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
|
func (p *Program) NewStateFactory(initTimeout time.Duration, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
|
||||||
return func(ctx context.Context) (*glua.LState, error) {
|
return func(ctx context.Context) (*glua.LState, error) {
|
||||||
|
|||||||
+989
-8
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configFile struct {
|
||||||
|
path string
|
||||||
|
info os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
var configFiles []configFile
|
||||||
|
|
||||||
|
// ReadConfigFile reads a config source and records local regular files.
|
||||||
|
func ReadConfigFile(file *os.File) ([]byte, error) {
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path := ""
|
||||||
|
if info.Mode().IsRegular() && file != os.Stdin {
|
||||||
|
path, err = filepath.Abs(file.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path, err = filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if info.Mode().IsRegular() {
|
||||||
|
configFiles = append(configFiles, configFile{path: path, info: info})
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isConfigFile(path string, info os.FileInfo) bool {
|
||||||
|
for _, file := range configFiles {
|
||||||
|
if file.path == path || os.SameFile(file.info, info) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -34,12 +34,13 @@ func ReadAsset(file string) ([]byte, error) {
|
|||||||
return ReadFile(path)
|
return ReadFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenAsset(file string) (io.ReadCloser, error) {
|
func OpenAsset(file string) (io.ReadCloser, int64, error) {
|
||||||
path, _, err := getAssetFileLocation(file)
|
path, info, err := getAssetFileLocation(file)
|
||||||
if err != nil {
|
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) {
|
func StatAsset(file string) (os.FileInfo, error) {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func GetConfDirPath() string {
|
|||||||
return configPath
|
return configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveLuaFile finds a local Lua script and returns its absolute path.
|
// ResolveLuaFile finds a local .lua script and returns its resolved absolute path.
|
||||||
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
|
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
|
||||||
func ResolveLuaFile(path string) (string, error) {
|
func ResolveLuaFile(path string) (string, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
@@ -113,15 +113,37 @@ func ResolveLuaFile(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resolveFile(paths)
|
path, info, err := resolveFile(paths)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
path, err = resolveLuaTarget(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if isConfigFile(path, info) {
|
||||||
|
return "", fmt.Errorf("Lua file was loaded as a configuration file: %s", path)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveFile(paths []string) (string, error) {
|
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, os.FileInfo, error) {
|
||||||
var tried []string
|
var tried []string
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
path, err := filepath.Abs(path)
|
path, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to resolve file path: %w", err)
|
return "", nil, fmt.Errorf("failed to resolve file path: %w", err)
|
||||||
}
|
}
|
||||||
tried = append(tried, path)
|
tried = append(tried, path)
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
@@ -129,12 +151,12 @@ func resolveFile(paths []string) (string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to inspect file %q: %w", path, err)
|
return "", nil, fmt.Errorf("failed to inspect file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
if !info.Mode().IsRegular() {
|
if !info.Mode().IsRegular() {
|
||||||
return "", fmt.Errorf("file is not a regular file: %s", path)
|
return "", nil, fmt.Errorf("file is not a regular file: %s", path)
|
||||||
}
|
}
|
||||||
return path, nil
|
return path, info, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
|
return "", nil, fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
@@ -88,6 +89,10 @@ func TestResolveLuaFile(t *testing.T) {
|
|||||||
for _, path := range paths[:3] {
|
for _, path := range paths[:3] {
|
||||||
common.Must(os.WriteFile(path, nil, 0o600))
|
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] {
|
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
|
||||||
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
|
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
|
||||||
}
|
}
|
||||||
@@ -115,3 +120,35 @@ func TestResolveLuaFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package serial_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
xlua "github.com/xtls/xray-core/common/lua"
|
||||||
|
"github.com/xtls/xray-core/core"
|
||||||
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
|
"github.com/xtls/xray-core/infra/conf/serial"
|
||||||
|
_ "github.com/xtls/xray-core/main/confloader/external"
|
||||||
|
glua "github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigSourceSecurity(t *testing.T) {
|
||||||
|
// Keep registered files alive until every file identity check finishes.
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Run("polyglots", testConfigLuaPolyglots)
|
||||||
|
t.Run("merged", func(t *testing.T) { testMergedConfigCannotRunAsLua(t, dir) })
|
||||||
|
t.Run("hardlink", func(t *testing.T) { testConfigHardLinkCannotRunAsLua(t, dir) })
|
||||||
|
t.Run("stdin", func(t *testing.T) { testStdinConfigCannotRunAsLua(t, dir) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfigLuaPolyglots(t *testing.T) {
|
||||||
|
const luaBody = "polyglotExecuted = true\nfunction HandleRoute() return 'direct' end\n"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
decode func(io.Reader) (*conf.Config, error)
|
||||||
|
source string
|
||||||
|
script string
|
||||||
|
}{
|
||||||
|
{"JSON", serial.DecodeJSONConfig, "null = 0\n" + luaBody, ""},
|
||||||
|
{"YAML", serial.DecodeYAMLConfig, `--[[: ignored
|
||||||
|
routing:
|
||||||
|
script: payload.lua
|
||||||
|
--]] --: ignored
|
||||||
|
_G: assert((function() polyglotExecuted = true; function HandleRoute() return 'direct' end end)())
|
||||||
|
`, "payload.lua"},
|
||||||
|
{"TOML", serial.DecodeTOMLConfig, `routing = { script = "payload.lua" }
|
||||||
|
-- = """
|
||||||
|
polyglotExecuted = true
|
||||||
|
function HandleRoute() return 'direct' end
|
||||||
|
-- """
|
||||||
|
`, "payload.lua"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
config, err := tc.decode(strings.NewReader(tc.source))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
script := ""
|
||||||
|
if config.RouterConfig != nil {
|
||||||
|
script = config.RouterConfig.Script
|
||||||
|
}
|
||||||
|
if script != tc.script {
|
||||||
|
t.Fatalf("routing.script = %q, want %q", script, tc.script)
|
||||||
|
}
|
||||||
|
if built, err := config.Build(); err != nil || built == nil {
|
||||||
|
t.Fatalf("config Build failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := serial.DecodeJSONConfigStrict(strings.NewReader(tc.source)); err == nil {
|
||||||
|
t.Fatal("strict JSON accepted the polyglot")
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.confdir", dir)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "payload.lua"), []byte(tc.source), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(dir, "payload."+strings.ToLower(tc.name))
|
||||||
|
if err := os.WriteFile(configPath, []byte(tc.source), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program, err := xlua.Compile(configPath); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
|
||||||
|
t.Fatalf("Compile(%q) = %v, %v; want .lua extension rejection", configPath, program, err)
|
||||||
|
}
|
||||||
|
program, err := xlua.Compile("payload.lua")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
state, err := program.NewState(ctx, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer state.Close()
|
||||||
|
if state.GetGlobal("polyglotExecuted") != glua.LTrue {
|
||||||
|
t.Fatal("Lua top-level code did not execute")
|
||||||
|
}
|
||||||
|
if err := state.CallByParam(glua.P{Fn: state.GetGlobal("HandleRoute"), NRet: 1, Protect: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := state.Get(-1); got != glua.LString("direct") {
|
||||||
|
t.Fatalf("HandleRoute() = %v, want direct", got)
|
||||||
|
}
|
||||||
|
t.Logf("config built with routing.script=%q; the same bytes executed as Lua", script)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMergedConfigCannotRunAsLua(t *testing.T, dir string) {
|
||||||
|
t.Setenv("xray.location.confdir", dir)
|
||||||
|
base := filepath.Join(dir, "base.lua")
|
||||||
|
for format, body := range map[string]string{
|
||||||
|
"json": "null = 0\nfunction HandleRoute() return 'direct' end\n",
|
||||||
|
"yaml": "_G: assert((function() function HandleRoute() return 'direct' end end)())\n",
|
||||||
|
"toml": "-- = \"\"\"\nfunction HandleRoute() return 'direct' end\n-- \"\"\"\n",
|
||||||
|
} {
|
||||||
|
t.Run(format, func(t *testing.T) {
|
||||||
|
path := filepath.Join(dir, format+".lua")
|
||||||
|
if err := os.WriteFile(base, []byte(`{"routing":{"script":"`+format+`.lua"}}`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program, err := xlua.Compile(path); err != nil || program == nil {
|
||||||
|
t.Fatalf("fixture must compile before config loading: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := serial.BuildConfig([]*core.ConfigSource{
|
||||||
|
{Name: base, Format: "json"},
|
||||||
|
{Name: path, Format: format},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, source := range []string{base, path} {
|
||||||
|
if program, err := xlua.Compile(source); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||||
|
t.Fatalf("Compile(%q) = %v, %v; want config source rejection", source, program, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.Rename(path, path+".old"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte("function HandleRoute() end"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program, err := xlua.Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||||
|
t.Fatalf("Compile = %v, %v; want replaced config path rejection", program, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, "ordinary.lua")
|
||||||
|
if err := os.WriteFile(path, []byte("function HandleRoute() end"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program, err := xlua.Compile(path); err != nil || program == nil {
|
||||||
|
t.Fatalf("ordinary script in the config directory must remain usable: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfigHardLinkCannotRunAsLua(t *testing.T, dir string) {
|
||||||
|
path := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(path, []byte("null = 0\nfunction HandleRoute() end"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
alias := filepath.Join(dir, "alias.lua")
|
||||||
|
if err := os.Link(path, alias); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := serial.BuildConfig([]*core.ConfigSource{{Name: path, Format: "json"}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program, err := xlua.Compile(alias); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||||
|
t.Fatalf("Compile = %v, %v; want config hard link rejection", program, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStdinConfigCannotRunAsLua(t *testing.T, dir string) {
|
||||||
|
path := filepath.Join(dir, "config.lua")
|
||||||
|
if err := os.WriteFile(path, []byte("null = 0\nfunction HandleRoute() end"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
originalStdin := os.Stdin
|
||||||
|
os.Stdin = file
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Stdin = originalStdin
|
||||||
|
file.Close()
|
||||||
|
})
|
||||||
|
if _, err := serial.BuildConfig([]*core.ConfigSource{{Name: "stdin:", Format: "json"}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program, err := xlua.Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "loaded as a configuration file") {
|
||||||
|
t.Fatalf("Compile = %v, %v; want redirected config rejection", program, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/main/commands/all/api"
|
"github.com/xtls/xray-core/main/commands/all/api"
|
||||||
"github.com/xtls/xray-core/main/commands/all/base64"
|
"github.com/xtls/xray-core/main/commands/all/base64"
|
||||||
"github.com/xtls/xray-core/main/commands/all/convert"
|
"github.com/xtls/xray-core/main/commands/all/convert"
|
||||||
|
"github.com/xtls/xray-core/main/commands/all/lua"
|
||||||
"github.com/xtls/xray-core/main/commands/all/tls"
|
"github.com/xtls/xray-core/main/commands/all/tls"
|
||||||
"github.com/xtls/xray-core/main/commands/base"
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
)
|
)
|
||||||
@@ -14,9 +15,11 @@ func init() {
|
|||||||
api.CmdAPI,
|
api.CmdAPI,
|
||||||
base64.CmdBase64,
|
base64.CmdBase64,
|
||||||
convert.CmdConvert,
|
convert.CmdConvert,
|
||||||
|
lua.CmdLua,
|
||||||
tls.CmdTLS,
|
tls.CmdTLS,
|
||||||
cmdUUID,
|
cmdUUID,
|
||||||
cmdX25519,
|
cmdX25519,
|
||||||
|
cmdEd25519,
|
||||||
cmdWG,
|
cmdWG,
|
||||||
cmdMLDSA65,
|
cmdMLDSA65,
|
||||||
cmdMLKEM768,
|
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)
|
||||||
|
}
|
||||||
Vendored
+8
-2
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/platform"
|
||||||
"github.com/xtls/xray-core/common/utils"
|
"github.com/xtls/xray-core/common/utils"
|
||||||
"github.com/xtls/xray-core/main/confloader"
|
"github.com/xtls/xray-core/main/confloader"
|
||||||
)
|
)
|
||||||
@@ -28,10 +29,15 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
|||||||
data, err = FetchHTTPContent(arg)
|
data, err = FetchHTTPContent(arg)
|
||||||
|
|
||||||
case arg == "stdin:":
|
case arg == "stdin:":
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = platform.ReadConfigFile(os.Stdin)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
data, err = os.ReadFile(arg)
|
file, openErr := os.Open(arg)
|
||||||
|
if openErr != nil {
|
||||||
|
return nil, openErr
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
data, err = platform.ReadConfigFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+13
-8
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/cmdarg"
|
"github.com/xtls/xray-core/common/cmdarg"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
clog "github.com/xtls/xray-core/common/log"
|
clog "github.com/xtls/xray-core/common/log"
|
||||||
|
"github.com/xtls/xray-core/common/lua"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
"github.com/xtls/xray-core/common/platform"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
"github.com/xtls/xray-core/main/commands/base"
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
@@ -36,9 +37,8 @@ The -confdir=dir flag sets a dir with multiple json config
|
|||||||
The -format=json flag sets the format of config files.
|
The -format=json flag sets the format of config files.
|
||||||
Default "auto".
|
Default "auto".
|
||||||
|
|
||||||
The config root env object sets process environment variables after all config
|
The --lua-privileged-pubkey=key flag sets the trusted Ed25519 public key
|
||||||
files are parsed. Variables needed to locate or parse config files must be set
|
for privileged local Lua scripts, encoded as unpadded Base64URL.
|
||||||
in the process environment before Xray starts.
|
|
||||||
|
|
||||||
The -test flag tells Xray to test config files only,
|
The -test flag tells Xray to test config files only,
|
||||||
without launching the server.
|
without launching the server.
|
||||||
@@ -53,11 +53,12 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main
|
configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main
|
||||||
configDir string
|
configDir string
|
||||||
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
|
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.")
|
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
|
||||||
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
|
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
|
||||||
|
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
|
/* We have to do this here because Golang's Test will also need to parse flag, before
|
||||||
* main func in this file is run.
|
* main func in this file is run.
|
||||||
@@ -214,6 +215,10 @@ func getConfigFormat() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startXray() (core.Server, error) {
|
func startXray() (core.Server, error) {
|
||||||
|
if err := lua.SetPrivilegedPublicKey(*luaPrivilegedPublicKey); err != nil {
|
||||||
|
return nil, errors.New("failed to configure Lua privileged authorization").Base(err)
|
||||||
|
}
|
||||||
|
|
||||||
configFiles := getConfigFilePath(true)
|
configFiles := getConfigFilePath(true)
|
||||||
|
|
||||||
c, err := core.LoadConfig(getConfigFormat(), configFiles)
|
c, err := core.LoadConfig(getConfigFormat(), configFiles)
|
||||||
|
|||||||
Reference in New Issue
Block a user