mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-19 15:54:26 +00:00
Lua: Sandbox by default (whitelisted functions); Disable inline scripts for now; Add more checks for security reasons (#7136)
https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6158632073 https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6170831601 https://github.com/XTLS/Xray-core/pull/7136#issuecomment-6184362904
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"github.com/xtls/xray-core/main/commands/all/api"
|
||||
"github.com/xtls/xray-core/main/commands/all/base64"
|
||||
"github.com/xtls/xray-core/main/commands/all/convert"
|
||||
// TODO: Restore this import and lua.CmdLua when Lua privileged signature authorization is re-enabled.
|
||||
// "github.com/xtls/xray-core/main/commands/all/lua"
|
||||
"github.com/xtls/xray-core/main/commands/all/tls"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
@@ -14,9 +16,11 @@ func init() {
|
||||
api.CmdAPI,
|
||||
base64.CmdBase64,
|
||||
convert.CmdConvert,
|
||||
// lua.CmdLua,
|
||||
tls.CmdTLS,
|
||||
cmdUUID,
|
||||
cmdX25519,
|
||||
// cmdEd25519,
|
||||
cmdWG,
|
||||
cmdMLDSA65,
|
||||
cmdMLKEM768,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package all
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdEd25519 = &base.Command{
|
||||
UsageLine: `{{.Exec}} ed25519 [-i "private key seed (Base64URL)"]`,
|
||||
Short: "Generate an Ed25519 key pair",
|
||||
Long: `
|
||||
Generate an Ed25519 key pair, or restore it from a 32-byte private key seed
|
||||
with -i. PrivateKey (seed) and PublicKey use unpadded Base64URL.
|
||||
`,
|
||||
}
|
||||
|
||||
var inputEd25519 = cmdEd25519.Flag.String("i", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||
|
||||
func init() {
|
||||
cmdEd25519.Run = executeEd25519
|
||||
}
|
||||
|
||||
func executeEd25519(cmd *base.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
}
|
||||
|
||||
var privateKey ed25519.PrivateKey
|
||||
if *inputEd25519 == "" {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to generate Ed25519 key: %s", err)
|
||||
}
|
||||
privateKey = key
|
||||
} else {
|
||||
encoding := base64.RawURLEncoding.Strict()
|
||||
seed, err := encoding.DecodeString(*inputEd25519)
|
||||
if err != nil || len(seed) != ed25519.SeedSize || encoding.EncodeToString(seed) != *inputEd25519 {
|
||||
base.Fatalf("invalid Ed25519 private key seed: expected 32 bytes in unpadded Base64URL")
|
||||
}
|
||||
privateKey = ed25519.NewKeyFromSeed(seed)
|
||||
}
|
||||
|
||||
if _, err := fmt.Printf("PrivateKey: %s\nPublicKey: %s\n",
|
||||
base64.RawURLEncoding.EncodeToString(privateKey.Seed()),
|
||||
base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))); err != nil {
|
||||
base.Fatalf("failed to write Ed25519 keys: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package lua
|
||||
|
||||
import "github.com/xtls/xray-core/main/commands/base"
|
||||
|
||||
// CmdLua holds the Lua script commands.
|
||||
var CmdLua = &base.Command{
|
||||
UsageLine: "{{.Exec}} lua",
|
||||
Short: "Manage Lua scripts",
|
||||
Long: "{{.Exec}} {{.LongName}} provides tools for local Lua scripts.",
|
||||
Commands: []*base.Command{
|
||||
cmdSign,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
)
|
||||
|
||||
var cmdSign = &base.Command{
|
||||
UsageLine: "{{.Exec}} lua sign [-key seed] [-o output-file] <input-file>",
|
||||
Short: "Sign a local Lua script for privileged execution",
|
||||
Long: `
|
||||
Sign a local Lua file using a 32-byte Ed25519 private key seed in unpadded
|
||||
Base64URL. Use {{.Exec}} ed25519 to generate a key pair.
|
||||
Write back to the input file, or use -o to write another file (- for stdout).
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
signKey = cmdSign.Flag.String("key", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||
signOutput = cmdSign.Flag.String("o", "", "Output file, or - for stdout. Default: input file.")
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdSign.Run = executeSign
|
||||
}
|
||||
|
||||
func executeSign(cmd *base.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
}
|
||||
|
||||
info, err := os.Stat(args[0])
|
||||
if err != nil {
|
||||
base.Fatalf("failed to inspect Lua file: %s", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
base.Fatalf("Lua input is not a regular file: %s", args[0])
|
||||
}
|
||||
|
||||
code, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
base.Fatalf("failed to read Lua file: %s", err)
|
||||
}
|
||||
|
||||
signed, err := xlua.SignPrivilegedScript(code, *signKey)
|
||||
if err != nil {
|
||||
base.Fatalf("failed to sign Lua file: %s", err)
|
||||
}
|
||||
|
||||
if *signOutput == "-" {
|
||||
if _, err := os.Stdout.Write(signed); err != nil {
|
||||
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
output := *signOutput
|
||||
if output == "" {
|
||||
output = args[0]
|
||||
}
|
||||
if err := writeSignedFile(output, signed, info.Mode().Perm()); err != nil {
|
||||
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSignedFile(path string, code []byte, mode os.FileMode) error {
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
if !info.Mode().IsRegular() {
|
||||
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
|
||||
}
|
||||
mode = info.Mode().Perm()
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp(filepath.Dir(path), ".xray-lua-sign-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer file.Close()
|
||||
if _, err := file.Write(code); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Chmod(mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(file.Name(), path)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ package base
|
||||
type CommandEnvHolder struct {
|
||||
// Executable name of current binary
|
||||
Exec string
|
||||
// Full name of the command being executed
|
||||
CommandName string
|
||||
// commands column width of current command
|
||||
CommandsWidth int
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ BigCmdLoop:
|
||||
}
|
||||
|
||||
buildCommandText(cmd)
|
||||
CommandEnv.CommandName = cmdName
|
||||
cmd.Run(cmd, args)
|
||||
Exit()
|
||||
return
|
||||
|
||||
Vendored
+35
-1
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
"github.com/xtls/xray-core/main/confloader"
|
||||
)
|
||||
|
||||
@@ -31,7 +33,12 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
|
||||
default:
|
||||
data, err = os.ReadFile(arg)
|
||||
file, openErr := os.Open(arg)
|
||||
if openErr != nil {
|
||||
return nil, openErr
|
||||
}
|
||||
defer file.Close()
|
||||
data, err = readConfigFile(file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -41,6 +48,33 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func readConfigFile(file *os.File) ([]byte, error) {
|
||||
if base.CommandEnv.CommandName != "run" {
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
path, err := filepath.Abs(file.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf":
|
||||
default:
|
||||
return nil, errors.New("unsupported config file extension: ", path)
|
||||
}
|
||||
}
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL
|
||||
// or a Unix socket HTTP endpoint.
|
||||
//
|
||||
|
||||
+10
-4
@@ -36,10 +36,6 @@ The -confdir=dir flag sets a dir with multiple json config
|
||||
The -format=json flag sets the format of config files.
|
||||
Default "auto".
|
||||
|
||||
The config root env object sets process environment variables after all config
|
||||
files are parsed. Variables needed to locate or parse config files must be set
|
||||
in the process environment before Xray starts.
|
||||
|
||||
The -test flag tells Xray to test config files only,
|
||||
without launching the server.
|
||||
|
||||
@@ -47,6 +43,9 @@ The -dump flag tells Xray to print the merged config.
|
||||
`,
|
||||
}
|
||||
|
||||
// The --lua-privileged-pubkey=key flag sets the trusted Ed25519 public key
|
||||
// for privileged local Lua scripts, encoded as unpadded Base64URL.
|
||||
|
||||
func init() {
|
||||
cmdRun.Run = executeRun // break init loop
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
|
||||
@@ -58,6 +57,8 @@ var (
|
||||
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
|
||||
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
|
||||
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
|
||||
// TODO: Restore this flag and its help text when Lua privileged signature authorization is re-enabled.
|
||||
// luaPrivilegedPublicKey = cmdRun.Flag.String("lua-privileged-pubkey", "", "Unpadded Base64URL Ed25519 public key for privileged local Lua scripts.")
|
||||
|
||||
/* We have to do this here because Golang's Test will also need to parse flag, before
|
||||
* main func in this file is run.
|
||||
@@ -214,6 +215,11 @@ func getConfigFormat() string {
|
||||
}
|
||||
|
||||
func startXray() (core.Server, error) {
|
||||
// TODO: Restore public key initialization when Lua privileged signature authorization is re-enabled.
|
||||
// if err := lua.SetPrivilegedPublicKey(*luaPrivilegedPublicKey); err != nil {
|
||||
// return nil, errors.New("failed to configure Lua privileged authorization").Base(err)
|
||||
// }
|
||||
|
||||
configFiles := getConfigFilePath(true)
|
||||
|
||||
c, err := core.LoadConfig(getConfigFormat(), configFiles)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRunConfigCommands(t *testing.T) {
|
||||
if os.Getenv("XRAY_TEST_CONFIG_COMMAND") == "1" {
|
||||
os.Args = append([]string{os.Args[0]}, flag.Args()...)
|
||||
main()
|
||||
return
|
||||
}
|
||||
dir := t.TempDir()
|
||||
config := filepath.Join(dir, "config.yaml")
|
||||
script := filepath.Join(dir, "config.lua")
|
||||
binary := filepath.Join(dir, "config.pb")
|
||||
message := filepath.Join(dir, "user.tmsg")
|
||||
for path, body := range map[string]string{
|
||||
config: "{}",
|
||||
script: "{}",
|
||||
binary: "",
|
||||
message: `{"type":"xray.app.dispatcher.Config","value":""}`,
|
||||
} {
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
file, err := os.Open(script)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
code int
|
||||
input io.Reader
|
||||
}{
|
||||
{"format override", []string{"run", "-test", "-format", "json", "-c", config}, 0, nil},
|
||||
{"invalid extension", []string{"run", "-test", "-format", "json", "-c", script}, 23, nil},
|
||||
{"default command", []string{"-test", "-format", "json", "-c", script}, 23, nil},
|
||||
{"protobuf config", []string{"run", "-test", "-c", binary}, 0, nil},
|
||||
{"protobuf error", []string{"run", "-test", "-format", "protobuf", "-c", script}, 23, nil},
|
||||
{"merged source", []string{"run", "-test", "-format", "json", "-c", config, "-c", script}, 23, nil},
|
||||
{"dump", []string{"run", "-dump", "-c", config}, 0, nil},
|
||||
{"stdin pipe", []string{"run", "-test", "-c", "stdin:"}, 0, nil},
|
||||
{"stdin file", []string{"run", "-test", "-c", "stdin:"}, 0, file},
|
||||
{"convert json", []string{"convert", "json", message}, 0, nil},
|
||||
{"convert pb", []string{"convert", "pb", "-debug", config}, 0, nil},
|
||||
{"convert pb stdin", []string{"convert", "pb", "-debug", "stdin:"}, 0, file},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
args := append([]string{"-test.run=^TestRunConfigCommands$", "--"}, tc.args...)
|
||||
cmd := exec.CommandContext(ctx, os.Args[0], args...)
|
||||
cmd.Env = append(os.Environ(), "XRAY_TEST_CONFIG_COMMAND=1")
|
||||
cmd.Stdin = strings.NewReader("{}")
|
||||
if tc.input != nil {
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Stdin = tc.input
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
if cmd.ProcessState == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code := cmd.ProcessState.ExitCode(); code != tc.code {
|
||||
t.Fatalf("exit code = %d, want %d: %v\n%s", code, tc.code, err, output)
|
||||
}
|
||||
if tc.code == 23 && !strings.Contains(string(output), "failed to load config files") {
|
||||
t.Fatalf("missing config error:\n%s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user