mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-14 11:34:03 +00:00
Compare commits
10
Commits
main
...
lua-sandbox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c0f3a94c5 | ||
|
|
19842ed0c2 | ||
|
|
34efc32ff0 | ||
|
|
4828ed8270 | ||
|
|
e84f3065dc | ||
|
|
fa9dc21a55 | ||
|
|
590a5236bb | ||
|
|
1aae252fc3 | ||
|
|
20c4c8f956 | ||
|
|
4c571f5e06 |
@@ -45,7 +45,7 @@ func RegisterLua(L *lua.LState) {
|
|||||||
"AnyMatch": luaIPAnyMatch,
|
"AnyMatch": luaIPAnyMatch,
|
||||||
"Matches": luaIPMatches,
|
"Matches": luaIPMatches,
|
||||||
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[net.IP](L)),
|
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[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)
|
||||||
|
|||||||
+28
-3
@@ -27,16 +27,37 @@ func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
|||||||
// 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 +73,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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
-3
@@ -8,6 +8,7 @@ import (
|
|||||||
"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"
|
||||||
@@ -16,6 +17,7 @@ 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 +29,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 +51,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 +75,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 {
|
||||||
|
// Host registration needs package.preload; scripts must not access the cache or loaders.
|
||||||
|
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 +93,68 @@ 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},
|
||||||
|
} {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
L.GetGlobal(glua.StringLibName).(*glua.LTable).RawSetString("dump", glua.LNil)
|
||||||
|
L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil)
|
||||||
|
|
||||||
|
// Hide object addresses without invoking __tostring.
|
||||||
|
L.SetGlobal("tostring", L.NewFunction(func(L *glua.LState) int {
|
||||||
|
value := L.CheckAny(1)
|
||||||
|
switch value.Type() {
|
||||||
|
case glua.LTNil, glua.LTBool, glua.LTNumber, glua.LTString:
|
||||||
|
L.Push(glua.LString(value.String()))
|
||||||
|
default:
|
||||||
|
L.Push(glua.LString("<opaque>"))
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Capture preload before package is hidden; require() reads loaders from the registry.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|||||||
+679
-4
@@ -2,16 +2,21 @@ package lua
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
"weak"
|
||||||
|
|
||||||
xbase64 "github.com/xtls/xray-core/common/base64"
|
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,6 +88,8 @@ func TestNewStateClosesFailedValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Re-enable these tests and their helper when Compile supports inline scripts again.
|
||||||
|
/*
|
||||||
func compressSource(t *testing.T, code string) []byte {
|
func compressSource(t *testing.T, code string) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var data bytes.Buffer
|
var data bytes.Buffer
|
||||||
@@ -152,9 +159,11 @@ func TestCompileInlineDiagnostics(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
code string
|
code string
|
||||||
compileError bool
|
compileError bool
|
||||||
|
wantMessage string
|
||||||
}{
|
}{
|
||||||
{"function broken(", true},
|
{"function broken(", true, ""},
|
||||||
{`error("initialization failed")`, false},
|
{`error("initialization failed")`, false, ""},
|
||||||
|
{"\n-- @xray:privileged-signature=invalid\n\nerror('failure')", false, "[inline]:4: failure"},
|
||||||
} {
|
} {
|
||||||
program, err := Compile(xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(tc.code)))
|
program, err := Compile(xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(tc.code)))
|
||||||
if tc.compileError {
|
if tc.compileError {
|
||||||
@@ -175,8 +184,12 @@ func TestCompileInlineDiagnostics(t *testing.T) {
|
|||||||
if err == nil || !strings.Contains(err.Error(), "[inline]") {
|
if err == nil || !strings.Contains(err.Error(), "[inline]") {
|
||||||
t.Fatalf("Lua error = %v, want [inline] diagnostic", err)
|
t.Fatalf("Lua error = %v, want [inline] diagnostic", err)
|
||||||
}
|
}
|
||||||
|
if tc.wantMessage != "" && !strings.Contains(err.Error(), tc.wantMessage) {
|
||||||
|
t.Fatalf("Lua error = %v, want %q", err, tc.wantMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func TestCompileRejectsReadFailure(t *testing.T) {
|
func TestCompileRejectsReadFailure(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "unreadable.lua")
|
path := filepath.Join(t.TempDir(), "unreadable.lua")
|
||||||
@@ -194,3 +207,665 @@ func TestCompileRejectsReadFailure(t *testing.T) {
|
|||||||
t.Fatalf("Compile = %v, %v; want nil program and permission error", program, err)
|
t.Fatalf("Compile = %v, %v; want nil program and permission error", program, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compileTestProgram(t *testing.T, code string) *Program {
|
||||||
|
t.Helper()
|
||||||
|
program, err := Compile(writeTestScript(t, code))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
const privilegedSignatureHeader = "-- @xray:privileged-signature="
|
||||||
|
|
||||||
|
// RFC 8032 test-vector key. The fixture signature was independently computed
|
||||||
|
// with Node's OpenSSL-backed crypto.sign(null, SHA256(body), privateKey), rather
|
||||||
|
// than by the signing helper or the production verifier.
|
||||||
|
const (
|
||||||
|
privilegedTestSeed = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
|
||||||
|
privilegedTestPublicKey = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
|
||||||
|
privilegedFixtureSignature = "rFbZtaaphe_69UEgSvfYwZcLZYHTXLFxdcukfEuLq6pzvmj8qtw6r60fD-7L5dwqSvnE7F1oAV0G_vHP3rLVBA"
|
||||||
|
privilegedFixtureDigest = "edef639ae641bacc9b76cea27473eed11369fbad65e223a87990864e899e578a"
|
||||||
|
privilegedFixtureBody = `-- Ordinary comments participate in the signed digest.
|
||||||
|
value = 1
|
||||||
|
|
||||||
|
function HasPrivilegedLibraries()
|
||||||
|
return io ~= nil and os ~= nil and debug ~= nil and channel ~= nil
|
||||||
|
end`
|
||||||
|
)
|
||||||
|
|
||||||
|
func setTestPrivilegedPublicKey(t *testing.T, key string) {
|
||||||
|
t.Helper()
|
||||||
|
// Each test setup represents a new process with its own startup key.
|
||||||
|
privilegedKey.Once = sync.Once{}
|
||||||
|
privilegedKey.publicKey = nil
|
||||||
|
t.Cleanup(func() {
|
||||||
|
privilegedKey.Once = sync.Once{}
|
||||||
|
privilegedKey.publicKey = nil
|
||||||
|
})
|
||||||
|
if err := SetPrivilegedPublicKey(key); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTestScript(t *testing.T, code string) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), "script.lua")
|
||||||
|
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSignature(t *testing.T, body string) string {
|
||||||
|
t.Helper()
|
||||||
|
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(bytes.TrimSpace([]byte(body)))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), digest[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetPrivilegedPublicKey(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
path := writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)
|
||||||
|
for _, tc := range []struct{ name, key string }{
|
||||||
|
{"invalid base64", "%"},
|
||||||
|
{"padding", privilegedTestPublicKey + "="},
|
||||||
|
{"standard base64 alphabet", strings.ReplaceAll(privilegedTestPublicKey, "_", "/")},
|
||||||
|
{"short key", base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize-1))},
|
||||||
|
{"long key", base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize+1))},
|
||||||
|
{"leading whitespace", " " + privilegedTestPublicKey},
|
||||||
|
{"trailing whitespace", privilegedTestPublicKey + "\t"},
|
||||||
|
{"embedded newline", privilegedTestPublicKey[:8] + "\n" + privilegedTestPublicKey[8:]},
|
||||||
|
{"embedded carriage return", privilegedTestPublicKey[:8] + "\r" + privilegedTestPublicKey[8:]},
|
||||||
|
{"nonzero unused bits", privilegedTestPublicKey[:len(privilegedTestPublicKey)-1] + "p"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if err := SetPrivilegedPublicKey(tc.key); err == nil {
|
||||||
|
t.Fatal("invalid public key was accepted")
|
||||||
|
}
|
||||||
|
program, err := Compile(path)
|
||||||
|
if err != nil || program == nil || !program.privileged {
|
||||||
|
t.Fatalf("invalid setter changed the trusted key: Compile = %v, %v", program, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setTestPrivilegedPublicKey(t, "")
|
||||||
|
if program, err := Compile(path); program != nil || err == nil {
|
||||||
|
t.Fatalf("signed script after disabling authorization = %v, %v; want rejection", program, err)
|
||||||
|
}
|
||||||
|
unsigned, err := Compile(writeTestScript(t, privilegedFixtureBody))
|
||||||
|
if err != nil || unsigned == nil || unsigned.privileged {
|
||||||
|
t.Fatalf("unsigned script with authorization disabled = %v, %v; want sandbox", unsigned, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegedPublicKeyIsFixed(t *testing.T) {
|
||||||
|
wrongKey := base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize))
|
||||||
|
for _, key := range []string{"", privilegedTestPublicKey} {
|
||||||
|
name := "disabled"
|
||||||
|
if key != "" {
|
||||||
|
name = "configured"
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, key)
|
||||||
|
path := writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)
|
||||||
|
for _, replacement := range []string{privilegedTestPublicKey, "", wrongKey} {
|
||||||
|
if err := SetPrivilegedPublicKey(replacement); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
program, err := Compile(path)
|
||||||
|
if key == "" {
|
||||||
|
if program != nil || err == nil {
|
||||||
|
t.Fatalf("disabled startup key changed: Compile = %v, %v", program, err)
|
||||||
|
}
|
||||||
|
} else if err != nil || program == nil || !program.privileged {
|
||||||
|
t.Fatalf("trusted startup key changed: Compile = %v, %v", program, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramPrivilegedSignatureFixture(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
digest := sha256.Sum256([]byte(privilegedFixtureBody))
|
||||||
|
if got := hex.EncodeToString(digest[:]); got != privilegedFixtureDigest {
|
||||||
|
t.Fatalf("fixture SHA-256 = %s, want %s", got, privilegedFixtureDigest)
|
||||||
|
}
|
||||||
|
for _, signed := range []bool{false, true} {
|
||||||
|
name, code := "unsigned", privilegedFixtureBody
|
||||||
|
if signed {
|
||||||
|
name, code = "signed", privilegedSignatureHeader+privilegedFixtureSignature+"\n"+code
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
program, err := Compile(writeTestScript(t, code))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program.privileged != signed {
|
||||||
|
t.Fatalf("privileged = %t, want %t", program.privileged, signed)
|
||||||
|
}
|
||||||
|
L, err := program.NewState(context.Background(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
if err := L.CallByParam(glua.P{Fn: L.GetGlobal("HasPrivilegedLibraries"), NRet: 1, Protect: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := L.Get(-1); got != glua.LBool(signed) {
|
||||||
|
t.Fatalf("privileged libraries available = %v, want %t", got, signed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramPrivilegedSignatureHeader(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
header := privilegedSignatureHeader + privilegedFixtureSignature
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, code string
|
||||||
|
privileged bool
|
||||||
|
}{
|
||||||
|
{"match", header + "\n" + privilegedFixtureBody, true},
|
||||||
|
{"no comment space", "--@xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, true},
|
||||||
|
{"comment whitespace", "-- \t @xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, true},
|
||||||
|
{"signature whitespace", privilegedSignatureHeader + " \t" + privilegedFixtureSignature + " \t\n" + privilegedFixtureBody, true},
|
||||||
|
{"blank lines and indentation", " \t\r\n\n \t" + header + "\n" + privilegedFixtureBody, true},
|
||||||
|
{"CRLF header only", "\r\n" + header + "\r\n" + privilegedFixtureBody, true},
|
||||||
|
{"body boundary whitespace", header + "\n \t\r\n\n" + privilegedFixtureBody + " \t\r\n\n", true},
|
||||||
|
{"missing header", privilegedFixtureBody, false},
|
||||||
|
{"after comment", "-- ordinary comment\n" + header + "\n" + privilegedFixtureBody, false},
|
||||||
|
{"after code", "value = 1\n" + header + "\n" + privilegedFixtureBody, false},
|
||||||
|
{"inside string", `value = "` + header + `"`, false},
|
||||||
|
{"wrong marker", "-- xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, false},
|
||||||
|
{"invalid marker after comment", "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
program, err := Compile(writeTestScript(t, tc.code))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if program.privileged != tc.privileged {
|
||||||
|
t.Fatalf("privileged = %t, want %t", program.privileged, tc.privileged)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramRejectsInvalidPrivilegedSignatures(t *testing.T) {
|
||||||
|
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wrongKey := base64.RawURLEncoding.EncodeToString(ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)).Public().(ed25519.PublicKey))
|
||||||
|
bodySignature := base64.RawURLEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), []byte(privilegedFixtureBody)))
|
||||||
|
for _, tc := range []struct{ name, key, signature string }{
|
||||||
|
{"missing key", "", privilegedFixtureSignature},
|
||||||
|
{"wrong key", wrongKey, privilegedFixtureSignature},
|
||||||
|
{"empty signature", privilegedTestPublicKey, ""},
|
||||||
|
{"whitespace-only signature", privilegedTestPublicKey, " \t"},
|
||||||
|
{"invalid base64", privilegedTestPublicKey, "%"},
|
||||||
|
{"padding", privilegedTestPublicKey, privilegedFixtureSignature + "=="},
|
||||||
|
{"standard base64 alphabet", privilegedTestPublicKey, strings.ReplaceAll(privilegedFixtureSignature, "_", "/")},
|
||||||
|
{"short signature", privilegedTestPublicKey, base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SignatureSize-1))},
|
||||||
|
{"long signature", privilegedTestPublicKey, base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SignatureSize+1))},
|
||||||
|
{"embedded carriage return", privilegedTestPublicKey, privilegedFixtureSignature[:8] + "\r" + privilegedFixtureSignature[8:]},
|
||||||
|
{"nonzero unused bits", privilegedTestPublicKey, privilegedFixtureSignature[:len(privilegedFixtureSignature)-1] + "B"},
|
||||||
|
{"damaged signature", privilegedTestPublicKey, "s" + privilegedFixtureSignature[1:]},
|
||||||
|
{"signature over body instead of digest", privilegedTestPublicKey, bodySignature},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, tc.key)
|
||||||
|
program, err := Compile(writeTestScript(t, privilegedSignatureHeader+tc.signature+"\n"+privilegedFixtureBody))
|
||||||
|
if program != nil || err == nil {
|
||||||
|
t.Fatalf("Compile = %v, %v; want nil program and signature rejection", program, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
code := privilegedSignatureHeader + "%\n" + privilegedSignatureHeader + privilegedFixtureSignature + "\n" + privilegedFixtureBody
|
||||||
|
if program, err := Compile(writeTestScript(t, code)); program != nil || err == nil {
|
||||||
|
t.Fatalf("later signature replaced invalid first signature: %v, %v", program, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramSignedBodyTampering(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
for _, tc := range []struct{ name, body string }{
|
||||||
|
{"statement", strings.Replace(privilegedFixtureBody, "value = 1", "value = 2", 1)},
|
||||||
|
{"comment", strings.Replace(privilegedFixtureBody, "Ordinary", "Modified", 1)},
|
||||||
|
{"added comment", "-- added comment\n" + privilegedFixtureBody},
|
||||||
|
{"indentation", strings.Replace(privilegedFixtureBody, " return", " return", 1)},
|
||||||
|
{"internal spaces", strings.Replace(privilegedFixtureBody, "value = 1", "value = 1", 1)},
|
||||||
|
{"internal trailing spaces", strings.Replace(privilegedFixtureBody, "value = 1\n", "value = 1 \t\n", 1)},
|
||||||
|
{"removed blank line", strings.Replace(privilegedFixtureBody, "\n\n", "\n", 1)},
|
||||||
|
{"added blank line", strings.Replace(privilegedFixtureBody, "\n\n", "\n\n\n", 1)},
|
||||||
|
{"LF to CRLF", strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n")},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
code := privilegedSignatureHeader + privilegedFixtureSignature + "\n" + tc.body
|
||||||
|
if program, err := Compile(writeTestScript(t, code)); program != nil || err == nil {
|
||||||
|
t.Fatalf("tampered body Compile = %v, %v; want signature rejection", program, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
crlfBody := strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n")
|
||||||
|
crlfSignature := testSignature(t, crlfBody)
|
||||||
|
program, err := Compile(writeTestScript(t, privilegedSignatureHeader+crlfSignature+"\r\n"+crlfBody))
|
||||||
|
if err != nil || program == nil || !program.privileged {
|
||||||
|
t.Fatalf("correctly signed CRLF body = %v, %v; want privileged program", program, err)
|
||||||
|
}
|
||||||
|
if program, err := Compile(writeTestScript(t, privilegedSignatureHeader+crlfSignature+"\n"+privilegedFixtureBody)); program != nil || err == nil {
|
||||||
|
t.Fatalf("CRLF to LF tampering = %v, %v; want signature rejection", program, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Re-enable this test when Compile supports inline scripts again.
|
||||||
|
/*
|
||||||
|
func TestProgramInlineSignaturesCannotAuthorize(t *testing.T) {
|
||||||
|
for _, key := range []string{"", privilegedTestPublicKey} {
|
||||||
|
setTestPrivilegedPublicKey(t, key)
|
||||||
|
for _, signature := range []string{privilegedFixtureSignature, "%", ""} {
|
||||||
|
code := privilegedSignatureHeader + signature + "\n" + privilegedFixtureBody
|
||||||
|
for _, script := range []string{
|
||||||
|
xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)),
|
||||||
|
xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, code)),
|
||||||
|
} {
|
||||||
|
program, err := Compile(script)
|
||||||
|
if err != nil || program == nil || program.privileged {
|
||||||
|
t.Fatalf("inline signed source Compile = %v, %v; want sandbox without verification", program, err)
|
||||||
|
}
|
||||||
|
L, err := program.NewState(context.Background(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := L.GetGlobal("os"); got != glua.LNil {
|
||||||
|
L.Close()
|
||||||
|
t.Fatalf("inline os = %v, want nil", got)
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestProgramSignedDiagnostics(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
const invalidBody = "\n\nlocal = 1"
|
||||||
|
invalidPath := writeTestScript(t, "\n"+privilegedSignatureHeader+"%\n"+invalidBody)
|
||||||
|
if program, err := Compile(invalidPath); program != nil || err == nil || !strings.Contains(err.Error(), "signature") {
|
||||||
|
t.Fatalf("invalid signature and invalid syntax = %v, %v; want signature error before parsing", program, err)
|
||||||
|
}
|
||||||
|
validPath := writeTestScript(t, "\n"+privilegedSignatureHeader+testSignature(t, invalidBody)+"\n"+invalidBody)
|
||||||
|
if program, err := Compile(validPath); program != nil || err == nil || !strings.Contains(err.Error(), validPath+" line:5(") {
|
||||||
|
t.Fatalf("verified invalid syntax = %v, %v; want original source line 5", program, err)
|
||||||
|
}
|
||||||
|
const body = "\n\nerror('signed failure')"
|
||||||
|
path := writeTestScript(t, "\n"+privilegedSignatureHeader+testSignature(t, body)+"\n"+body)
|
||||||
|
program, err := Compile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
L, err := program.NewState(context.Background(), nil, nil)
|
||||||
|
if L != nil {
|
||||||
|
L.Close()
|
||||||
|
t.Fatal("failing signed script returned a state")
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), path+":5: signed failure") {
|
||||||
|
t.Fatalf("signed initialization error = %v; want original source line 5", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramSandbox(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "escape.lua"), []byte(`disk_loaded = true; return "escaped"`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("LUA_PATH", filepath.ToSlash(filepath.Join(dir, "?.lua")))
|
||||||
|
program := compileTestProgram(t, `
|
||||||
|
local function check()
|
||||||
|
assert(package == nil)
|
||||||
|
assert(require("_G") == _G)
|
||||||
|
for _, name in ipairs({"io", "os", "debug", "channel", "coroutine", "dofile", "loadfile",
|
||||||
|
"load", "loadstring", "getfenv", "setfenv", "collectgarbage", "module",
|
||||||
|
"getmetatable", "setmetatable", "rawget", "rawset", "newproxy", "print", "_printregs"}) do
|
||||||
|
assert(_G[name] == nil, name)
|
||||||
|
end
|
||||||
|
for _, name in ipairs({"io", "os", "debug", "channel", "coroutine", "package", "escape"}) do
|
||||||
|
assert(not pcall(require, name), name)
|
||||||
|
end
|
||||||
|
for _, name in ipairs({"math", "string", "table"}) do
|
||||||
|
assert(require(name) == _G[name], name)
|
||||||
|
end
|
||||||
|
assert(string.dump == nil and ("").dump == nil and require("string").dump == nil)
|
||||||
|
assert(math.randomseed == nil and require("math").randomseed == nil)
|
||||||
|
local helper = require("xray.test")
|
||||||
|
assert(helper.value == "ok" and helper == require("xray.test"))
|
||||||
|
assert(math.abs(-2) == 2 and string.upper("ok") == "OK")
|
||||||
|
assert(table.concat({"a", "b"}) == "ab")
|
||||||
|
end
|
||||||
|
check()
|
||||||
|
function CheckSandbox()
|
||||||
|
check()
|
||||||
|
local injected = false
|
||||||
|
package = {
|
||||||
|
preload = {
|
||||||
|
escape = function() injected = true; return "escaped" end,
|
||||||
|
["xray.late"] = function() injected = true; return "injected" end,
|
||||||
|
},
|
||||||
|
loaded = {escape = "escaped", ["cached.only"] = "injected"},
|
||||||
|
loaders = {function() injected = true; return function() return "escaped" end end},
|
||||||
|
path = testPath,
|
||||||
|
cpath = testPath,
|
||||||
|
}
|
||||||
|
assert(not pcall(require, "escape") and disk_loaded == nil)
|
||||||
|
assert(not pcall(require, "cached.only") and not pcall(require, "package"))
|
||||||
|
assert(require("xray.late") == "host" and require("xray.late") == "host")
|
||||||
|
assert(require("xray.test").value == "ok" and not injected)
|
||||||
|
package = nil
|
||||||
|
end`)
|
||||||
|
var helperLoads, lateLoads int
|
||||||
|
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||||
|
if L.GetGlobal("io") != glua.LNil || L.GetGlobal("loadfile") != glua.LNil {
|
||||||
|
t.Fatal("sandbox was not configured before module registration")
|
||||||
|
}
|
||||||
|
L.SetGlobal("testPath", glua.LString(filepath.ToSlash(filepath.Join(dir, "?.lua"))))
|
||||||
|
L.PreloadModule("xray.test", func(L *glua.LState) int {
|
||||||
|
helperLoads++
|
||||||
|
module := L.NewTable()
|
||||||
|
module.RawSetString("value", glua.LString("ok"))
|
||||||
|
L.Push(module)
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
L.PreloadModule("xray.late", func(L *glua.LState) int {
|
||||||
|
lateLoads++
|
||||||
|
L.Push(glua.LString("host"))
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
if L.GetTop() != 0 {
|
||||||
|
t.Fatalf("stack top = %d, want 0", L.GetTop())
|
||||||
|
}
|
||||||
|
if err := L.CallByParam(glua.P{Fn: L.GetGlobal("CheckSandbox"), Protect: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if helperLoads != 1 || lateLoads != 1 {
|
||||||
|
t.Fatalf("module initialization calls = (%d, %d), want (1, 1)", helperLoads, lateLoads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramSandboxToString(t *testing.T) {
|
||||||
|
program := compileTestProgram(t, `
|
||||||
|
assert(tostring == require("_G").tostring)
|
||||||
|
for _, convert in ipairs({tostring, require("_G").tostring}) do
|
||||||
|
assert(convert(nil) == "nil")
|
||||||
|
assert(convert(true) == "true" and convert(false) == "false")
|
||||||
|
assert(convert(0) == "0" and convert(-42) == "-42" and convert(1.25) == "1.25")
|
||||||
|
assert(convert("") == "" and convert("hello") == "hello")
|
||||||
|
assert(convert(42, {}) == "42")
|
||||||
|
assert(not pcall(convert))
|
||||||
|
for _, value in ipairs({{}, function() end, convert, testUserdata, testThread, testChannel}) do
|
||||||
|
local ok, result = pcall(convert, value)
|
||||||
|
assert(ok and result == "<opaque>")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
string.__tostring = function() error("string __tostring must not run") end
|
||||||
|
assert(tostring("hello") == "hello")
|
||||||
|
string.__tostring = nil
|
||||||
|
`)
|
||||||
|
var metamethodCalls int
|
||||||
|
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||||
|
userdata := L.NewUserData()
|
||||||
|
metatable := L.NewTable()
|
||||||
|
metatable.RawSetString("__tostring", L.NewFunction(func(L *glua.LState) int {
|
||||||
|
metamethodCalls++
|
||||||
|
L.Push(glua.LString("custom"))
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
L.SetMetatable(userdata, metatable)
|
||||||
|
L.SetGlobal("testUserdata", userdata)
|
||||||
|
thread, cancel := L.NewThread()
|
||||||
|
if cancel != nil {
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
}
|
||||||
|
L.SetGlobal("testThread", thread)
|
||||||
|
L.SetGlobal("testChannel", glua.LChannel(make(chan glua.LValue)))
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
if metamethodCalls != 0 {
|
||||||
|
t.Fatalf("__tostring calls = %d, want 0", metamethodCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramSandboxModuleFailures(t *testing.T) {
|
||||||
|
program := compileTestProgram(t, `
|
||||||
|
assert(package == nil)
|
||||||
|
local ok, err = pcall(require, "xray.failure")
|
||||||
|
assert(not ok and string.find(err, "module initialization failed", 1, true))
|
||||||
|
package = {loaded = {["xray.failure"] = "injected"}}
|
||||||
|
ok, err = pcall(require, "xray.failure")
|
||||||
|
assert(not ok and string.find(err, "loop or previous error loading module", 1, true))
|
||||||
|
for i = 1, 2 do
|
||||||
|
ok, err = pcall(require, "xray.cycle")
|
||||||
|
assert(not ok and string.find(err, "loop or previous error loading module", 1, true))
|
||||||
|
end
|
||||||
|
package = nil
|
||||||
|
`)
|
||||||
|
var failedLoads, cycleLoads int
|
||||||
|
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||||
|
L.PreloadModule("xray.failure", func(L *glua.LState) int {
|
||||||
|
failedLoads++
|
||||||
|
L.RaiseError("module initialization failed")
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
L.PreloadModule("xray.cycle", func(L *glua.LState) int {
|
||||||
|
cycleLoads++
|
||||||
|
L.Push(L.GetGlobal("require"))
|
||||||
|
L.Push(glua.LString("xray.cycle"))
|
||||||
|
L.Call(1, 1)
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
if failedLoads != 1 || cycleLoads != 1 {
|
||||||
|
t.Fatalf("module initialization calls = (%d, %d), want (1, 1)", failedLoads, cycleLoads)
|
||||||
|
}
|
||||||
|
if L.GetTop() != 0 {
|
||||||
|
t.Fatalf("stack top = %d, want 0", L.GetTop())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramSandboxModulesAreCollected(t *testing.T) {
|
||||||
|
for _, fail := range []bool{false, true} {
|
||||||
|
name := "success"
|
||||||
|
if fail {
|
||||||
|
name = "failure"
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ref := func() weak.Pointer[glua.LTable] {
|
||||||
|
program := compileTestProgram(t, `payload = string.rep("x", 1024 * 1024); assert(pcall(require, "xray.test") == expectedSuccess)`)
|
||||||
|
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||||
|
L.SetGlobal("expectedSuccess", glua.LBool(!fail))
|
||||||
|
L.PreloadModule("xray.test", func(L *glua.LState) int {
|
||||||
|
if fail {
|
||||||
|
L.RaiseError("module initialization failed")
|
||||||
|
}
|
||||||
|
L.Push(L.NewTable())
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ref := weak.Make(L.G.Global)
|
||||||
|
L.Close()
|
||||||
|
return ref
|
||||||
|
}()
|
||||||
|
runtime.GC()
|
||||||
|
runtime.GC()
|
||||||
|
if ref.Value() != nil {
|
||||||
|
t.Fatal("closed sandbox's global table was not collected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramSandboxClosesFailedInitialization(t *testing.T) {
|
||||||
|
program := compileTestProgram(t, `require("xray.failure")`)
|
||||||
|
var partial *glua.LState
|
||||||
|
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||||
|
partial = L
|
||||||
|
L.PreloadModule("xray.failure", func(L *glua.LState) int {
|
||||||
|
L.RaiseError("module initialization failed")
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}, func(*glua.LState) error {
|
||||||
|
t.Error("validation ran after failed initialization")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if L != nil {
|
||||||
|
L.Close()
|
||||||
|
t.Fatal("sandbox initialization returned a state")
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "module initialization failed") || partial == nil || !partial.IsClosed() {
|
||||||
|
t.Fatalf("error = %v, partial state closed = %t", err, partial != nil && partial.IsClosed())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramPrivilegedLibraries(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
const body = `assert(require("io") == io and require("os") == os)
|
||||||
|
assert(require("debug") == debug and require("channel") == channel)
|
||||||
|
assert(type(coroutine.create) == "function" and require("coroutine") == coroutine)
|
||||||
|
assert(type(dofile) == "function" and type(loadfile) == "function")
|
||||||
|
assert(type(getfenv) == "function" and type(setfenv) == "function")
|
||||||
|
assert(type(collectgarbage) == "function" and type(print) == "function")
|
||||||
|
assert(type(getmetatable) == "function" and type(setmetatable) == "function")
|
||||||
|
assert(type(rawget) == "function" and type(rawset) == "function")
|
||||||
|
assert(tostring(42) == "42" and type(tostring({})) == "string")
|
||||||
|
assert(tostring(setmetatable({}, {__tostring = function() return "custom" end})) == "custom")
|
||||||
|
assert(type(string.dump) == "function" and type(math.randomseed) == "function")
|
||||||
|
assert(type(package.loaders[2]) == "function" and type(package.path) == "string")
|
||||||
|
assert(type(debug.traceback()) == "string" and type(os.time()) == "number")
|
||||||
|
assert(assert(loadstring("return 42"))() == 42)
|
||||||
|
local file = assert(io.open(testPath, "w"))
|
||||||
|
file:write("privileged")
|
||||||
|
file:close()`
|
||||||
|
code := privilegedSignatureHeader + testSignature(t, body) + "\n" + body
|
||||||
|
path := filepath.Join(t.TempDir(), "privileged.lua")
|
||||||
|
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, script string
|
||||||
|
privileged bool
|
||||||
|
}{
|
||||||
|
{"file", path, true},
|
||||||
|
// TODO: Re-enable these cases when Compile supports inline scripts again.
|
||||||
|
// {"base64", xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)), false},
|
||||||
|
// {"gzip", xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, code)), false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
program, err := Compile(tc.script)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output := filepath.Join(t.TempDir(), "privileged.txt")
|
||||||
|
L, err := program.NewState(context.Background(), func(L *glua.LState) {
|
||||||
|
L.SetGlobal("testPath", glua.LString(output))
|
||||||
|
}, nil)
|
||||||
|
if !tc.privileged {
|
||||||
|
if L != nil {
|
||||||
|
L.Close()
|
||||||
|
t.Fatal("inline script with a valid signature accessed privileged libraries")
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "no preloaded module: io") {
|
||||||
|
t.Fatalf("NewState error = %v, want io module unavailable", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(output); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("inline script output stat error = %v, want file not found", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
data, err := os.ReadFile(output)
|
||||||
|
if err != nil || string(data) != "privileged" {
|
||||||
|
t.Fatalf("privileged file contents = %q, error = %v", data, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgramPrivilegesAreCompileTimeSnapshot(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
for _, privileged := range []bool{false, true} {
|
||||||
|
name := "sandboxed"
|
||||||
|
if privileged {
|
||||||
|
name = "privileged"
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
const body = "value = 1"
|
||||||
|
code := body
|
||||||
|
if privileged {
|
||||||
|
code = privilegedSignatureHeader + testSignature(t, body) + "\n" + body
|
||||||
|
}
|
||||||
|
path := filepath.Join(t.TempDir(), "snapshot.lua")
|
||||||
|
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
program, err := Compile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Source edits cannot change an already compiled Program's authorization.
|
||||||
|
changed := privilegedSignatureHeader + testSignature(t, body) + "\n" + body
|
||||||
|
if privileged {
|
||||||
|
changed = body
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(changed), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
check := func(L *glua.LState) {
|
||||||
|
t.Helper()
|
||||||
|
if got := L.GetGlobal("os").Type() == glua.LTTable; got != privileged {
|
||||||
|
t.Fatalf("os available = %t, want compile-time value %t", got, privileged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
factory := program.NewStateFactory(time.Second, nil, nil)
|
||||||
|
for range 2 {
|
||||||
|
L, err := factory(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
check(L)
|
||||||
|
}
|
||||||
|
recompiled, err := Compile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if recompiled.privileged == privileged {
|
||||||
|
t.Fatal("recompilation did not observe the changed source")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
+8
-3
@@ -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.
|
||||||
@@ -58,6 +58,7 @@ var (
|
|||||||
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