Compare commits

..
10 Commits
Author SHA1 Message Date
Meo597 4c0f3a94c5 disable inline lua 2026-10-14 10:18:40 +08:00
Meo597 19842ed0c2 luar: whitelist 2026-10-14 09:49:58 +08:00
Meo597 34efc32ff0 sandbox: disable more func 2026-10-14 09:16:32 +08:00
Meo597 4828ed8270 cmd: add Ed25519 key generation and Lua script signing 2026-10-14 08:14:05 +08:00
Meo597 e84f3065dc fix format 2026-10-14 07:07:03 +08:00
Meo597 fa9dc21a55 Replace privileged PSK with Ed25519 signature verification 2026-10-14 06:57:32 +08:00
Meo597 590a5236bb Reduce memory usage 2026-10-13 12:31:04 +08:00
Meo597 1aae252fc3 Lua: Restrict PSK privileges to local Lua files 2026-10-13 11:38:06 +08:00
Meo597 20c4c8f956 Lua: Hide package from sandboxed scripts 2026-10-13 11:24:47 +08:00
Meo597 4c571f5e06 Lua: Add sandbox mechanism 2026-10-13 09:52:31 +08:00
23 changed files with 1263 additions and 124 deletions
+1 -1
View File
@@ -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
})) }))
+6
View File
@@ -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
View File
@@ -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)
+117
View File
@@ -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
}
+82
View File
@@ -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
View File
@@ -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
View File
@@ -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")
}
})
}
}
+10 -9
View File
@@ -4,6 +4,7 @@ go 1.27
require ( require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
@@ -19,15 +20,15 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
github.com/yuin/gopher-lua v1.1.2 github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.57.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.61.0 golang.org/x/net v0.60.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.23.0
golang.org/x/sys v0.49.0 golang.org/x/sys v0.48.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1 golang.zx2c4.com/wireguard/windows v1.1.1
@@ -41,13 +42,13 @@ require (
) )
require ( require (
github.com/andybalholm/brotli v1.2.3 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/gopacket v1.1.19 // indirect github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.20.0 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect github.com/pion/dtls/v3 v3.1.5 // indirect
@@ -57,9 +58,9 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.51.0 // indirect golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
+20 -20
View File
@@ -1,10 +1,12 @@
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
@@ -31,8 +33,8 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
@@ -79,10 +81,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs= github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY= github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
@@ -95,8 +95,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
@@ -107,12 +107,12 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,22 +121,22 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+3
View File
@@ -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,
+53
View File
@@ -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)
}
}
+13
View File
@@ -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,
},
}
+98
View File
@@ -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)
}
+3 -3
View File
@@ -1,11 +1,11 @@
package all package all
import ( import (
"crypto/mldsa"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/commands/base"
) )
@@ -39,8 +39,8 @@ func executeMLDSA65(cmd *base.Command, args []string) {
} else { } else {
rand.Read(seed[:]) rand.Read(seed[:])
} }
key, _ := mldsa.NewPrivateKey(mldsa.MLDSA65(), seed[:]) pub, _ := mldsa65.NewKeyFromSeed(&seed)
fmt.Printf("Seed: %v\nVerify: %v\n", fmt.Printf("Seed: %v\nVerify: %v\n",
base64.RawURLEncoding.EncodeToString(seed[:]), base64.RawURLEncoding.EncodeToString(seed[:]),
base64.RawURLEncoding.EncodeToString(key.PublicKey().Bytes())) base64.RawURLEncoding.EncodeToString(pub.Bytes()))
} }
+8 -3
View File
@@ -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)
+1 -7
View File
@@ -169,18 +169,12 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
} }
sessionItem.Unlock() sessionItem.Unlock()
} }
link := sessionItem.link.Load() link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload) return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
}) })
if err != nil { if err != nil {
continue continue
} }
}
payloadBuf := buf.New() payloadBuf := buf.New()
payloadBuf.Write(decoded.Payload) payloadBuf.Write(decoded.Payload)
+1 -7
View File
@@ -336,18 +336,12 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
sessionItem.Unlock() sessionItem.Unlock()
link := sessionItem.link.Load() link, err := sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload) return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
}) })
if err != nil { if err != nil {
continue continue
} }
}
pBuf := buf.New() pBuf := buf.New()
pBuf.Write(decoded.Payload) pBuf.Write(decoded.Payload)
+1 -7
View File
@@ -253,17 +253,11 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
} }
sessionItem.Unlock() sessionItem.Unlock()
} }
link := sessionItem.link.Load() link, err := sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if link != nil {
sessionItem.UpdateConn(conn)
} else {
var err error
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
if err != nil { if err != nil {
b.Release() b.Release()
continue continue
} }
}
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b}) _ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
} }
+4 -4
View File
@@ -169,8 +169,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer)) return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
} }
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer)) responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil { if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
return errors.New("connection ends").Base(err) return errors.New("connection ends").Base(err)
} }
@@ -212,8 +212,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
return nil return nil
} }
responseDonePost := task.OnSuccess(responseDone, task.Close(link.Writer)) responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDone, responseDonePost); err != nil { if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
return errors.New("connection ends").Base(err) return errors.New("connection ends").Base(err)
} }
+18 -5
View File
@@ -18,8 +18,12 @@ import (
) )
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) { func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
if s.currentConn.Load() == nil {
s.currentConn.Store(conn) s.currentConn.Store(conn)
}
if s.timer != nil {
s.timer.Update() s.timer.Update()
}
} }
func (s *ServerUDPSession) WriteToClient(b []byte) error { func (s *ServerUDPSession) WriteToClient(b []byte) error {
@@ -53,16 +57,19 @@ func (s *ServerUDPSession) EnsureLink(
policyManager policy.Manager, policyManager policy.Manager,
responseEncoder func(dest net.Destination, payload []byte) ([]byte, error), responseEncoder func(dest net.Destination, payload []byte) ([]byte, error),
) (*transport.Link, error) { ) (*transport.Link, error) {
s.UpdateConn(conn)
if link := s.link.Load(); link != nil {
return link, nil
}
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if link := s.link.Load(); link != nil { if link := s.link.Load(); link != nil {
s.UpdateConn(conn)
return link, nil return link, nil
} }
s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx) sessCtx, cancel := context.WithCancel(ctx)
inbound := session.InboundFromContext(sessCtx) inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil { if inbound != nil && s.User != nil {
@@ -87,6 +94,7 @@ func (s *ServerUDPSession) EnsureLink(
return nil, err return nil, err
} }
s.link.Store(link)
sessionPolicy := policyManager.ForLevel(level) sessionPolicy := policyManager.ForLevel(level)
s.timer = signal.CancelAfterInactivity(sessCtx, func() { s.timer = signal.CancelAfterInactivity(sessCtx, func() {
if s.manager != nil { if s.manager != nil {
@@ -95,7 +103,6 @@ func (s *ServerUDPSession) EnsureLink(
s.Close() s.Close()
cancel() cancel()
}, sessionPolicy.Timeouts.ConnectionIdle) }, sessionPolicy.Timeouts.ConnectionIdle)
s.link.Store(link)
go handleUDPResponse(s, link, dest, responseEncoder) go handleUDPResponse(s, link, dest, responseEncoder)
return link, nil return link, nil
@@ -111,13 +118,19 @@ func ResetTCPConn(conn net.Conn) {
} }
func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) { func handleUDPResponse(s *ServerUDPSession, link *transport.Link, fallbackDest net.Destination, encode func(dest net.Destination, payload []byte) ([]byte, error)) {
defer s.timer.SetTimeout(0) defer func() {
if s.timer != nil {
s.timer.SetTimeout(0)
}
}()
for { for {
resMb, err := link.Reader.ReadMultiBuffer() resMb, err := link.Reader.ReadMultiBuffer()
if err != nil { if err != nil {
return return
} }
if s.timer != nil {
s.timer.Update() s.timer.Update()
}
for i, rb := range resMb { for i, rb := range resMb {
b := rb.Bytes() b := rb.Bytes()
if encode != nil { if encode != nil {
+2 -10
View File
@@ -11,7 +11,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/antireplay" "github.com/xtls/xray-core/common/antireplay"
"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"
@@ -630,7 +629,6 @@ func InitServerStream(conn net.Conn, method *CipherMethod, psk, saltSlice []byte
func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.Reader, writer buf.Writer, link *transport.Link) error { func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.Reader, writer buf.Writer, link *transport.Link) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
defer timer.SetTimeout(0)
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
requestDone := func() error { requestDone := func() error {
@@ -646,12 +644,6 @@ func TransportTCP(ctx context.Context, sessionPolicy policy.Session, reader buf.
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)) return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
} }
requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer)) responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil { return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
return errors.New("connection ends").Base(err)
}
return nil
} }
+4 -9
View File
@@ -2,13 +2,12 @@ package reality
import ( import (
"context" "context"
"crypto/ecdh"
"crypto/mldsa"
"io" "io"
"net" "net"
"os" "os"
"time" "time"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/xtls/reality" "github.com/xtls/reality"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
@@ -24,6 +23,7 @@ func (c *Config) GetREALITYConfig() *reality.Config {
Dest: c.Dest, Dest: c.Dest,
Xver: byte(c.Xver), Xver: byte(c.Xver),
PrivateKey: c.PrivateKey,
MinClientVer: c.MinClientVer, MinClientVer: c.MinClientVer,
MaxClientVer: c.MaxClientVer, MaxClientVer: c.MaxClientVer,
MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond, MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond,
@@ -33,14 +33,9 @@ func (c *Config) GetREALITYConfig() *reality.Config {
KeyLogWriter: KeyLogWriterFromConfig(c), KeyLogWriter: KeyLogWriterFromConfig(c),
} }
var err error
if config.PrivateKey, err = ecdh.X25519().NewPrivateKey(c.PrivateKey); err != nil {
panic(err)
}
if c.Mldsa65Seed != nil { if c.Mldsa65Seed != nil {
if config.Mldsa65Key, err = mldsa.NewPrivateKey(mldsa.MLDSA65(), c.Mldsa65Seed); err != nil { _, key := mldsa65.NewKeyFromSeed((*[32]byte)(c.Mldsa65Seed))
panic(err) config.Mldsa65Key = key.Bytes()
}
} }
if c.LimitFallbackUpload != nil { if c.LimitFallbackUpload != nil {
config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes
+10 -9
View File
@@ -5,9 +5,7 @@ import (
"context" "context"
"crypto/ecdh" "crypto/ecdh"
"crypto/ed25519" "crypto/ed25519"
"crypto/hkdf"
"crypto/hmac" "crypto/hmac"
"crypto/mldsa"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
gotls "crypto/tls" gotls "crypto/tls"
@@ -24,6 +22,7 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
utls "github.com/refraction-networking/utls" utls "github.com/refraction-networking/utls"
"github.com/xtls/reality" "github.com/xtls/reality"
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
@@ -32,6 +31,7 @@ import (
"github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/crypto/hkdf"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@@ -114,8 +114,8 @@ func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x50
if len(certs[0].Extensions) > 0 { if len(certs[0].Extensions) > 0 {
h.Write(c.HandshakeState.Hello.Raw) h.Write(c.HandshakeState.Hello.Raw)
h.Write(c.HandshakeState.ServerHello.Raw) h.Write(c.HandshakeState.ServerHello.Raw)
verify, _ := mldsa.NewPublicKey(mldsa.MLDSA65(), c.Config.Mldsa65Verify) verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify)
if mldsa.Verify(verify, h.Sum(nil), certs[0].Extensions[0].Value, nil) == nil { if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) {
c.Verified = true c.Verified = true
return nil return nil
} }
@@ -176,7 +176,7 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
} }
publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey) publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey)
if err != nil { if err != nil {
return nil, errors.New("REALITY: nil publicKey").Base(err) return nil, errors.New("REALITY: publicKey == nil")
} }
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil { if ecdhe == nil {
@@ -185,11 +185,12 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
if ecdhe == nil { if ecdhe == nil {
return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.") return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.")
} }
if uConn.AuthKey, err = ecdhe.ECDH(publicKey); err != nil { uConn.AuthKey, _ = ecdhe.ECDH(publicKey)
return nil, errors.New("REALITY: nil sharedKey").Base(err) if uConn.AuthKey == nil {
return nil, errors.New("REALITY: SharedKey == nil")
} }
if uConn.AuthKey, err = hkdf.Key(sha256.New, uConn.AuthKey, hello.Random[:20], "REALITY", 32); err != nil { if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil {
return nil, errors.New("REALITY: nil authKey").Base(err) return nil, err
} }
aead := crypto.NewAesGcm(uConn.AuthKey) aead := crypto.NewAesGcm(uConn.AuthKey)
if config.Show { if config.Show {