mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-15 20:13:59 +00:00
Compare commits
28
Commits
main
..
lua-sandbox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bba0ae39d1 | ||
|
|
03ee3f872f | ||
|
|
65449d4f03 | ||
|
|
d5bfb0a5c7 | ||
|
|
afa28af4e0 | ||
|
|
ecd8719d98 | ||
|
|
74456ca5e9 | ||
|
|
b408f7f0c7 | ||
|
|
ef4f4888df | ||
|
|
c2a9b074f4 | ||
|
|
9726bbc9d7 | ||
|
|
6cd0c8a414 | ||
|
|
796bbd763a | ||
|
|
e04191e65b | ||
|
|
bd28a87f0b | ||
|
|
a04770587e | ||
|
|
eb9c6030ab | ||
|
|
e4124d3dbb | ||
|
|
4c0f3a94c5 | ||
|
|
19842ed0c2 | ||
|
|
34efc32ff0 | ||
|
|
4828ed8270 | ||
|
|
e84f3065dc | ||
|
|
fa9dc21a55 | ||
|
|
590a5236bb | ||
|
|
1aae252fc3 | ||
|
|
20c4c8f956 | ||
|
|
4c571f5e06 |
+1
-1
@@ -52,7 +52,7 @@ func luaServers(s *DNS) []luaDNSServer {
|
|||||||
|
|
||||||
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
|
func registerLua(L *lua.LState, servers []luaDNSServer, client featureDNS.Client) {
|
||||||
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
L.PreloadModule("xray.dns", func(L *lua.LState) int {
|
||||||
pushIPs := xlua.NewSlicePusher[net.IP](L)
|
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
|
||||||
|
|
||||||
serverList := L.CreateTable(len(servers), 0)
|
serverList := L.CreateTable(len(servers), 0)
|
||||||
for i, client := range servers {
|
for i, client := range servers {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func newScriptEngine(script string) (*scriptEngine, error) {
|
|||||||
func (e *scriptEngine) start(server *DNS) error {
|
func (e *scriptEngine) start(server *DNS) error {
|
||||||
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
|
pool, err := xlua.NewPool(server.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
|
||||||
scriptExecutionTimeout*20,
|
scriptExecutionTimeout*20,
|
||||||
|
256,
|
||||||
func(L *lua.LState) {
|
func(L *lua.LState) {
|
||||||
geodata.RegisterLua(L)
|
geodata.RegisterLua(L)
|
||||||
log.RegisterLua(L)
|
log.RegisterLua(L)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import (
|
|||||||
|
|
||||||
const idleTimeout = 30 * time.Second
|
const idleTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
// DO NOT EDIT. Ignored by geodata, rejected by Lua.
|
||||||
|
const luaGuardHeader = "\x12\x01\x00"
|
||||||
|
|
||||||
type stage struct {
|
type stage struct {
|
||||||
target string
|
target string
|
||||||
temp string
|
temp string
|
||||||
@@ -170,6 +173,10 @@ func (d *downloader) downloadOne(asset *Asset) (stage, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if _, err := temp.WriteString(luaGuardHeader); err != nil {
|
||||||
|
temp.Close()
|
||||||
|
return stage{}, err
|
||||||
|
}
|
||||||
if err := d.fetch(asset.Url, temp); err != nil {
|
if err := d.fetch(asset.Url, temp); err != nil {
|
||||||
temp.Close()
|
temp.Close()
|
||||||
return stage{}, err
|
return stage{}, err
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package geodata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commongeodata "github.com/xtls/xray-core/common/geodata"
|
||||||
|
xlua "github.com/xtls/xray-core/common/lua"
|
||||||
|
"github.com/yuin/gopher-lua/parse"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDownloadOneLuaGuard(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
code string
|
||||||
|
}{
|
||||||
|
{"geodata", "\x0a\x03\x0a\x01A", "A"},
|
||||||
|
{"prefixed geodata", luaGuardHeader + "\x0a\x03\x0a\x01A", "A"},
|
||||||
|
{"Lua", "function HandleRoute() end", ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.asset", dir)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(tc.body))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
d := &downloader{ctx: context.Background(), httpClient: server.Client()}
|
||||||
|
staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tx, err := swapAll([]stage{staged})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tx.commit(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(staged.target)
|
||||||
|
if err != nil || string(data) != luaGuardHeader+tc.body {
|
||||||
|
t.Fatalf("downloaded data = % x, %v", data, err)
|
||||||
|
}
|
||||||
|
program, err := xlua.Compile(staged.target)
|
||||||
|
var syntaxErr *parse.Error
|
||||||
|
if program != nil || !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
|
||||||
|
t.Fatalf("want Lua rejection at the first byte, got %v, %v", program, err)
|
||||||
|
}
|
||||||
|
if tc.code != "" {
|
||||||
|
var list commongeodata.GeoSiteList
|
||||||
|
if err := proto.Unmarshal(data, &list); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(list.Entry) != 1 || list.Entry[0].Code != tc.code {
|
||||||
|
t.Fatalf("header changed geodata: %v", &list)
|
||||||
|
}
|
||||||
|
if _, err := commongeodata.ParseDomainRules([]string{"ext:" + filepath.Base(staged.target) + ":" + tc.code}, commongeodata.Domain_Full); err != nil {
|
||||||
|
t.Fatalf("geodata loader rejected the header: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -62,7 +62,7 @@ func (r *Router) RegisterLua(L *lua.LState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func registerLuaContext(L *lua.LState) {
|
func registerLuaContext(L *lua.LState) {
|
||||||
pushIPs := xlua.NewSlicePusher[net.IP](L)
|
pushIPs := xlua.NewReadOnlySlicePusher[net.IP](L)
|
||||||
attributes := L.NewTypeMetatable(luaAttributesType)
|
attributes := L.NewTypeMetatable(luaAttributesType)
|
||||||
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
|
L.SetField(attributes, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||||
values := L.CheckUserData(1).Value.(map[string]string)
|
values := L.CheckUserData(1).Value.(map[string]string)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func newScriptEngine(script string) (*scriptEngine, error) {
|
|||||||
func (e *scriptEngine) start(router *Router) error {
|
func (e *scriptEngine) start(router *Router) error {
|
||||||
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
|
pool, err := xlua.NewPool(router.ctx, scriptExecutionTimeout, e.program.NewStateFactory(
|
||||||
scriptExecutionTimeout*20,
|
scriptExecutionTimeout*20,
|
||||||
|
256,
|
||||||
func(L *lua.LState) {
|
func(L *lua.LState) {
|
||||||
geodata.RegisterLua(L)
|
geodata.RegisterLua(L)
|
||||||
log.RegisterLua(L)
|
log.RegisterLua(L)
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func checkFile(file, code string) error {
|
func checkFile(file, code string) error {
|
||||||
r, err := filesystem.OpenAsset(file)
|
r, size, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to open ", file).Base(err)
|
return errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
if _, err := find(r, []byte(code), false); err != nil {
|
if _, err := find(r, []byte(code), false, size); err != nil {
|
||||||
return errors.New("failed to check code ", code, " from ", file).Base(err)
|
return errors.New("failed to check code ", code, " from ", file).Base(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -30,12 +30,12 @@ func checkFile(file, code string) error {
|
|||||||
|
|
||||||
func loadFile(file, code string) ([]byte, error) {
|
func loadFile(file, code string) ([]byte, error) {
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
r, err := filesystem.OpenAsset(file)
|
r, size, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to open ", file).Base(err)
|
return nil, errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
bs, err := find(r, []byte(code), true)
|
bs, err := find(r, []byte(code), true, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
|
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
|
||||||
}
|
}
|
||||||
@@ -60,13 +60,13 @@ func loadIP(file, code string) ([]*CIDR, error) {
|
|||||||
// unmarshalling it into a []*Domain, so value is only valid during fn.
|
// unmarshalling it into a []*Domain, so value is only valid during fn.
|
||||||
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
|
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
r, err := filesystem.OpenAsset(file)
|
r, size, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to open ", file).Base(err)
|
return errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
br := bufio.NewReaderSize(r, 64*1024)
|
br := bufio.NewReaderSize(r, 64*1024)
|
||||||
n, err := seek(br, []byte(code))
|
n, err := seek(br, []byte(code), size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to load code ", code, " from ", file).Base(err)
|
return errors.New("failed to load code ", code, " from ", file).Base(err)
|
||||||
}
|
}
|
||||||
@@ -114,6 +114,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if shift == 63 && b > 1 {
|
||||||
|
break // Only one value bit fits in the tenth byte.
|
||||||
|
}
|
||||||
x |= (uint64(b) & 0x7F) << shift
|
x |= (uint64(b) & 0x7F) << shift
|
||||||
if (b & 0x80) == 0 {
|
if (b & 0x80) == 0 {
|
||||||
return x, nil
|
return x, nil
|
||||||
@@ -123,9 +126,9 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
|
|||||||
return 0, errors.New("varint overflow")
|
return 0, errors.New("varint overflow")
|
||||||
}
|
}
|
||||||
|
|
||||||
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
|
func find(r io.Reader, code []byte, readBody bool, size int64) ([]byte, error) {
|
||||||
br := bufio.NewReaderSize(r, 64*1024)
|
br := bufio.NewReaderSize(r, 64*1024)
|
||||||
bodyL, err := seek(br, code)
|
bodyL, err := seek(br, code, size)
|
||||||
if err != nil || !readBody {
|
if err != nil || !readBody {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -137,7 +140,7 @@ func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// seek advances br to the body of the entry for code and returns the body length.
|
// seek advances br to the body of the entry for code and returns the body length.
|
||||||
func seek(br *bufio.Reader, code []byte) (int, error) {
|
func seek(br *bufio.Reader, code []byte, size int64) (int, error) {
|
||||||
codeL := len(code)
|
codeL := len(code)
|
||||||
if codeL == 0 {
|
if codeL == 0 {
|
||||||
return 0, errors.New("empty code")
|
return 0, errors.New("empty code")
|
||||||
@@ -145,7 +148,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
|||||||
need := 2 + codeL // TODO: if code too long
|
need := 2 + codeL // TODO: if code too long
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if _, err := br.ReadByte(); err != nil {
|
tag, err := br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +157,10 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
bodyL := int(x)
|
if x == 0 || x > uint64(size) || x > uint64(^uint(0)>>1) {
|
||||||
if bodyL <= 0 {
|
return 0, errors.New("invalid body length: ", x)
|
||||||
return 0, errors.New("invalid body length: ", bodyL)
|
|
||||||
}
|
}
|
||||||
|
bodyL := int(x)
|
||||||
|
|
||||||
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single
|
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single
|
||||||
// length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does.
|
// length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does.
|
||||||
@@ -167,7 +171,8 @@ func seek(br *bufio.Reader, code []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if bodyL >= need && len(prefix) >= need && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
|
if tag == 0x0a && bodyL >= need && len(prefix) >= need &&
|
||||||
|
prefix[0] == 0x0a && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
|
||||||
return bodyL, nil
|
return bodyL, nil
|
||||||
}
|
}
|
||||||
if _, err := br.Discard(bodyL); err != nil {
|
if _, err := br.Discard(bodyL); err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package geodata
|
package geodata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,6 +13,58 @@ import (
|
|||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFindChecksEntryAndCodeTags(t *testing.T) {
|
||||||
|
for _, payload := range [][]byte{
|
||||||
|
{0x12, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as entry.
|
||||||
|
{0x08, 6, 0x0a, 4, 'T', 'E', 'S', 'T'}, // Wrong entry wire type.
|
||||||
|
{0x0a, 6, 0x1a, 4, 'T', 'E', 'S', 'T'}, // Unknown field posing as code.
|
||||||
|
{0x0a, 6, 0x08, 4, 'T', 'E', 'S', 'T'}, // Wrong code wire type.
|
||||||
|
} {
|
||||||
|
if _, err := find(bytes.NewReader(payload), []byte("TEST"), true, int64(len(payload))); err == nil {
|
||||||
|
t.Fatalf("matched incorrect entry/code tags in %x", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFileRejectsVarintOverflow(t *testing.T) {
|
||||||
|
payload := []byte{
|
||||||
|
0x0a,
|
||||||
|
0x86, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02, // 2^64 + 6
|
||||||
|
0x0a, 0x04, 'T', 'E', 'S', 'T',
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.asset", dir)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "overflow.dat"), payload, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := loadFile("overflow.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "varint overflow") {
|
||||||
|
t.Fatalf("want varint overflow, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFileRejectsOversizedEntry(t *testing.T) {
|
||||||
|
payload := []byte{
|
||||||
|
0x0a, 0x03, 0x0a, 0x01, 'A', // A valid entry before the oversized one.
|
||||||
|
0x0a, 0x80, 0x80, 0x80, 0x80, 0x04, // Entry length: 1 GiB.
|
||||||
|
0x0a, 0x04, 'T', 'E', 'S', 'T',
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.asset", dir)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "oversized.dat"), payload, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Check without allocating the body first, so a regression fails safely.
|
||||||
|
if err := checkFile("oversized.dat", "TEST"); err == nil {
|
||||||
|
t.Fatal("accepted an entry length larger than the file")
|
||||||
|
}
|
||||||
|
if _, err := loadFile("oversized.dat", "TEST"); err == nil || !strings.Contains(err.Error(), "invalid body length") {
|
||||||
|
t.Fatalf("want invalid body length, got %v", err)
|
||||||
|
}
|
||||||
|
if err := loadSite("oversized.dat", "TEST", "", func(Domain_Type, []byte) {}); err == nil || !strings.Contains(err.Error(), "invalid body length") {
|
||||||
|
t.Fatalf("want invalid body length, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type siteEntry struct {
|
type siteEntry struct {
|
||||||
Type Domain_Type
|
Type Domain_Type
|
||||||
Value string
|
Value string
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ func RegisterLua(L *lua.LState) {
|
|||||||
"Match": luaIPMatch,
|
"Match": luaIPMatch,
|
||||||
"AnyMatch": luaIPAnyMatch,
|
"AnyMatch": luaIPAnyMatch,
|
||||||
"Matches": luaIPMatches,
|
"Matches": luaIPMatches,
|
||||||
"FilterIPs": newLuaIPFilterIPs(xlua.NewSlicePusher[net.IP](L)),
|
"FilterIPs": newLuaIPFilterIPs(xlua.NewReadOnlySlicePusher[net.IP](L)),
|
||||||
})
|
}, "SetReverse", "ToggleReverse")
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ local geodata = require("xray.geodata")
|
|||||||
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
|
local matcher = geodata.BuildIPMatcher("127.0.0.0/8")
|
||||||
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
|
assert(matcher.Match == matcher.match and matcher.AnyMatch == matcher.anyMatch)
|
||||||
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
|
assert(matcher.Matches == matcher.matches and matcher.FilterIPs == matcher.filterIPs)
|
||||||
|
assert(matcher.Reload == nil and matcher.reload == nil)
|
||||||
assert(matcher:match(ip))
|
assert(matcher:match(ip))
|
||||||
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
|
assert(matcher:anyMatch({ip}) and matcher:matches({ip}))
|
||||||
assert(not matcher:AnyMatch(nil))
|
assert(not matcher:AnyMatch(nil))
|
||||||
@@ -97,6 +98,11 @@ assert(matcher.missing == nil)
|
|||||||
|
|
||||||
local domain = geodata.BuildDomainMatcher("full:example.com")
|
local domain = geodata.BuildDomainMatcher("full:example.com")
|
||||||
assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny)
|
assert(domain.Match == domain.match and domain.MatchAny == domain.matchAny)
|
||||||
|
assert(domain.Reload == nil and domain.reload == nil)
|
||||||
|
for _, value in ipairs({matcher, domain}) do
|
||||||
|
assert(not pcall(function() return -value end))
|
||||||
|
assert(not pcall(function() return value ^ {} end))
|
||||||
|
end
|
||||||
assert(domain:matchAny("example.com"))
|
assert(domain:matchAny("example.com"))
|
||||||
assert(#domain:Match("example.com") == 1)
|
assert(#domain:Match("example.com") == 1)
|
||||||
assert(domain:match("example.com")[1] == 0)
|
assert(domain:match("example.com")[1] == 0)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// DO NOT EDIT. Regression test for app/geodata's luaGuardHeader.
|
||||||
|
|
||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yuin/gopher-lua/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeodataLuaGuard(t *testing.T) {
|
||||||
|
_, err := parse.Parse(strings.NewReader("\x12\x01\x00function HandleRoute() end"), "geodata")
|
||||||
|
var syntaxErr *parse.Error
|
||||||
|
if !errors.As(err, &syntaxErr) || syntaxErr.Pos.Line != 1 || syntaxErr.Pos.Column != 1 {
|
||||||
|
t.Fatalf("want Lua rejection at the first byte, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
-4
@@ -1,6 +1,9 @@
|
|||||||
package lua
|
package lua
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
luar "layeh.com/gopher-luar"
|
luar "layeh.com/gopher-luar"
|
||||||
)
|
)
|
||||||
@@ -9,7 +12,7 @@ import (
|
|||||||
// The returned function wraps slices without reflection or metatable lookup,
|
// The returned function wraps slices without reflection or metatable lookup,
|
||||||
// and pushes nil for nil slices. Use it with this state or its coroutines.
|
// and pushes nil for nil slices. Use it with this state or its coroutines.
|
||||||
func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
||||||
metatable := luar.New(L, []T{}).(*glua.LUserData).Metatable
|
metatable := luar.MT(L, []T(nil)).LTable
|
||||||
return func(L *glua.LState, values []T) {
|
return func(L *glua.LState, values []T) {
|
||||||
if values == nil {
|
if values == nil {
|
||||||
L.Push(glua.LNil)
|
L.Push(glua.LNil)
|
||||||
@@ -22,21 +25,105 @@ func NewSlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewReadOnlySlicePusher borrows slices without copying and recursively makes
|
||||||
|
// nested slice types read-only through their shared metatables. IP byte views
|
||||||
|
// and masks are also read-only. Other referenced elements retain their mutability.
|
||||||
|
// Element bindings are initialized on first indexing or iteration.
|
||||||
|
func NewReadOnlySlicePusher[T any](L *glua.LState) func(*glua.LState, []T) {
|
||||||
|
sliceMetatable := luar.MT(L, []T{})
|
||||||
|
if sliceMetatable.RawGetString("__readonly") != glua.LTrue {
|
||||||
|
readOnly := L.NewFunction(func(L *glua.LState) int {
|
||||||
|
L.RaiseError("slice is read-only")
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
setSliceReadOnly[[]T](L, readOnly)
|
||||||
|
}
|
||||||
|
return NewSlicePusher[T](L)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSliceReadOnly[S ~[]E, E any](L *glua.LState, readOnly *glua.LFunction) {
|
||||||
|
var visit func(reflect.Type)
|
||||||
|
visit = func(typ reflect.Type) {
|
||||||
|
if typ.Kind() != reflect.Slice {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metatable := luar.MT(L, reflect.Zero(typ).Interface())
|
||||||
|
if metatable.RawGetString("__readonly") == glua.LTrue {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Mark before descending because named slice types can be recursive.
|
||||||
|
metatable.RawSetString("__readonly", glua.LTrue)
|
||||||
|
metatable.RawSetString("__newindex", readOnly)
|
||||||
|
// Reject explicitly so Lua cannot fall back to the other operand.
|
||||||
|
metatable.RawSetString("__add", readOnly)
|
||||||
|
if typ == reflect.TypeFor[net.IP]() {
|
||||||
|
// A nil receiver lets AppendText return its argument as a writable []byte.
|
||||||
|
methods := metatable.RawGetString("methods").(*glua.LTable)
|
||||||
|
appendText := methods.RawGetString("AppendText")
|
||||||
|
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
|
||||||
|
if method == appendText {
|
||||||
|
methods.RawSet(key, glua.LNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(reflect.TypeFor[net.IPMask]())
|
||||||
|
}
|
||||||
|
if element := typ.Elem(); element.Kind() == reflect.Slice {
|
||||||
|
index := metatable.RawGetString("__index").(*glua.LFunction)
|
||||||
|
call := metatable.RawGetString("__call").(*glua.LFunction)
|
||||||
|
initialize := func() {
|
||||||
|
visit(element)
|
||||||
|
metatable.RawSetString("__index", index)
|
||||||
|
metatable.RawSetString("__call", call)
|
||||||
|
}
|
||||||
|
metatable.RawSetString("__index", L.NewFunction(func(L *glua.LState) int {
|
||||||
|
initialize()
|
||||||
|
return index.GFunction(L)
|
||||||
|
}))
|
||||||
|
metatable.RawSetString("__call", L.NewFunction(func(L *glua.LState) int {
|
||||||
|
initialize()
|
||||||
|
return call.GFunction(L)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(reflect.TypeFor[S]())
|
||||||
|
}
|
||||||
|
|
||||||
// DirectMethod handles a Lua call without luar's reflected method invocation.
|
// DirectMethod handles a Lua call without luar's reflected method invocation.
|
||||||
// It returns the result count and whether it handled the arguments. On false,
|
// It returns the result count and whether it handled the arguments. On false,
|
||||||
// it must leave the stack unchanged for the original luar wrapper.
|
// it must leave the stack unchanged for the original luar wrapper.
|
||||||
type DirectMethod func(L *glua.LState) (nresults int, handled bool)
|
type DirectMethod func(L *glua.LState) (nresults int, handled bool)
|
||||||
|
|
||||||
|
// PushMethods pushes a luar userdata exposing only the named Go methods and
|
||||||
|
// their aliases through reflected calls. The method list is fixed by the first
|
||||||
|
// binding of that Go type in each LState.
|
||||||
|
func PushMethods(L *glua.LState, value any, methods ...string) {
|
||||||
|
PushWithDirectMethods(L, value, nil, methods...)
|
||||||
|
}
|
||||||
|
|
||||||
// PushWithDirectMethods pushes a luar userdata with typed Go method bindings.
|
// PushWithDirectMethods pushes a luar userdata with typed Go method bindings.
|
||||||
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
|
// Handled calls bypass luar's argument conversion and reflect.Call; method lookup
|
||||||
// uses the methods table directly instead of luar's reflected __index handler.
|
// uses the methods table directly instead of luar's reflected __index handler.
|
||||||
// value must expose methods only. Bindings and their closures are installed once
|
// Only directMethods and additionalMethods are exposed, including their aliases.
|
||||||
// per Go type per LState, outside the method-call hot path.
|
// Bindings and their closures are installed once per Go type per LState, outside
|
||||||
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod) {
|
// the method-call hot path.
|
||||||
|
func PushWithDirectMethods(L *glua.LState, value any, directMethods map[string]DirectMethod, additionalMethods ...string) {
|
||||||
userdata := luar.New(L, value).(*glua.LUserData)
|
userdata := luar.New(L, value).(*glua.LUserData)
|
||||||
metatable := userdata.Metatable.(*glua.LTable)
|
metatable := userdata.Metatable.(*glua.LTable)
|
||||||
methods := metatable.RawGetString("methods").(*glua.LTable)
|
methods := metatable.RawGetString("methods").(*glua.LTable)
|
||||||
if metatable.RawGetString("__index") != methods {
|
if metatable.RawGetString("__index") != methods {
|
||||||
|
// Keep all aliases for the explicitly exposed methods.
|
||||||
|
exposed := make(map[glua.LValue]bool, len(directMethods)+len(additionalMethods))
|
||||||
|
for name := range directMethods {
|
||||||
|
exposed[methods.RawGetString(name)] = true
|
||||||
|
}
|
||||||
|
for _, name := range additionalMethods {
|
||||||
|
exposed[methods.RawGetString(name)] = true
|
||||||
|
}
|
||||||
|
for key, method := methods.Next(glua.LNil); key != glua.LNil; key, method = methods.Next(key) {
|
||||||
|
if !exposed[method] {
|
||||||
|
methods.RawSet(key, glua.LNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
for name, direct := range directMethods {
|
for name, direct := range directMethods {
|
||||||
original := methods.RawGetString(name)
|
original := methods.RawGetString(name)
|
||||||
fn := L.NewFunction(func(L *glua.LState) int {
|
fn := L.NewFunction(func(L *glua.LState) int {
|
||||||
@@ -52,6 +139,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)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package lua
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
@@ -47,6 +48,188 @@ assert(ok and result == true)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadOnlySlicePusher(t *testing.T) {
|
||||||
|
L := glua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
values := []int{3, 5}
|
||||||
|
NewReadOnlySlicePusher[int](L)(L, values)
|
||||||
|
L.SetGlobal("values", L.Get(-1))
|
||||||
|
L.Pop(1)
|
||||||
|
if err := L.DoString(`
|
||||||
|
assert(#values == 2 and values[1] == 3 and values[2] == 5)
|
||||||
|
assert(not pcall(function() values[1] = 7 end))
|
||||||
|
assert(not pcall(function() return values + 7 end))
|
||||||
|
local sum = 0
|
||||||
|
for _, value in values() do sum = sum + value end
|
||||||
|
assert(sum == 8)
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := L.GetGlobal("values").(*glua.LUserData).Value.([]int)
|
||||||
|
if &got[0] != &values[0] || values[0] != 3 || values[1] != 5 {
|
||||||
|
t.Fatal("read-only binding copied or modified the slice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOnlyIPSlicePusher(t *testing.T) {
|
||||||
|
L := glua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
ips := []net.IP{{192, 0, 2, 1}, net.ParseIP("2001:db8::1")}
|
||||||
|
NewReadOnlySlicePusher[net.IP](L)(L, ips)
|
||||||
|
L.SetGlobal("ips", L.Get(-1))
|
||||||
|
L.Pop(1)
|
||||||
|
readOnly := luar.MT(L, ips).RawGetString("__newindex")
|
||||||
|
NewReadOnlySlicePusher[net.IP](L)
|
||||||
|
if luar.MT(L, ips).RawGetString("__newindex") != readOnly {
|
||||||
|
t.Fatal("IP metatables were initialized more than once")
|
||||||
|
}
|
||||||
|
if err := L.DoString(`
|
||||||
|
local function readOnly(value)
|
||||||
|
assert(not pcall(function() value[1] = value[1] end))
|
||||||
|
assert(not pcall(function() return value + value[1] end))
|
||||||
|
end
|
||||||
|
assert(#ips == 2 and ips[1]:String() == "192.0.2.1")
|
||||||
|
readOnly(ips)
|
||||||
|
for _, ip in ips() do
|
||||||
|
readOnly(ip)
|
||||||
|
readOnly(ip:To16())
|
||||||
|
if ip:To4() then
|
||||||
|
readOnly(ip:To4())
|
||||||
|
readOnly(ip:DefaultMask())
|
||||||
|
end
|
||||||
|
assert(ip:Equal(ip:To16()) and type(ip:String()) == "string")
|
||||||
|
assert(ip.AppendText == nil and ip.appendText == nil)
|
||||||
|
local text, err = ip:MarshalText()
|
||||||
|
assert(not err)
|
||||||
|
text[1] = 88 -- Independently owned byte buffers remain writable.
|
||||||
|
assert(not pcall(function() return ips + text end))
|
||||||
|
end
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := L.GetGlobal("ips").(*glua.LUserData).Value.([]net.IP)
|
||||||
|
if &got[0] != &ips[0] || &got[0][0] != &ips[0][0] || &got[1][0] != &ips[1][0] {
|
||||||
|
t.Fatal("read-only binding copied the IP data")
|
||||||
|
}
|
||||||
|
for i, want := range []string{"192.0.2.1", "2001:db8::1"} {
|
||||||
|
if got[i].String() != want {
|
||||||
|
t.Fatalf("IP %d changed to %s; want %s", i, got[i], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOnlyIPSlicePusherLazy(t *testing.T) {
|
||||||
|
for _, tc := range []struct{ name, access string }{
|
||||||
|
{"index", `local ip = ips[1]`},
|
||||||
|
{"iterate", `local _, ip = ips()()`},
|
||||||
|
{"coroutine", `local co = coroutine.create(function() return ips[1] end); local ok, ip = coroutine.resume(co); assert(ok)`},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
L := glua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
methods := 0
|
||||||
|
luar.GetConfig(L).MethodNames = func(_ reflect.Type, method reflect.Method) []string {
|
||||||
|
methods++
|
||||||
|
return []string{method.Name}
|
||||||
|
}
|
||||||
|
NewReadOnlySlicePusher[net.IP](L)(L, []net.IP{{192, 0, 2, 1}})
|
||||||
|
L.SetGlobal("ips", L.Get(-1))
|
||||||
|
L.Pop(1)
|
||||||
|
if err := L.DoString(`assert(#ips == 1); assert(not pcall(function() ips[1] = nil end))`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if methods != 0 {
|
||||||
|
t.Fatal("IP methods initialized without reading an element")
|
||||||
|
}
|
||||||
|
source := tc.access + `
|
||||||
|
assert(ip:String() == "192.0.2.1" and ip.AppendText == nil)
|
||||||
|
assert(not pcall(function() ip[1] = 0 end))
|
||||||
|
assert(not pcall(function() ip:To4()[1] = 0 end))
|
||||||
|
assert(not pcall(function() ip:To16()[1] = 0 end))
|
||||||
|
assert(not pcall(function() ip:DefaultMask()[1] = 0 end))
|
||||||
|
`
|
||||||
|
if err := L.DoString(source); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
initialized := methods
|
||||||
|
if initialized == 0 {
|
||||||
|
t.Fatal("IP access did not initialize its methods")
|
||||||
|
}
|
||||||
|
if err := L.DoString(source); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if methods != initialized {
|
||||||
|
t.Fatal("IP methods initialized more than once")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOnlyNestedSlicePusher(t *testing.T) {
|
||||||
|
L := glua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
values := [][][]byte{{{3, 5}}}
|
||||||
|
NewReadOnlySlicePusher[[][]byte](L)(L, values)
|
||||||
|
L.SetGlobal("values", L.Get(-1))
|
||||||
|
L.Pop(1)
|
||||||
|
ips := [][]net.IP{{{192, 0, 2, 1}}}
|
||||||
|
NewReadOnlySlicePusher[[]net.IP](L)(L, ips)
|
||||||
|
L.SetGlobal("ips", L.Get(-1))
|
||||||
|
L.Pop(1)
|
||||||
|
if err := L.DoString(`
|
||||||
|
local function readOnly(value)
|
||||||
|
assert(not pcall(function() value[1] = value[1] end))
|
||||||
|
assert(not pcall(function() return value + value[1] end))
|
||||||
|
end
|
||||||
|
assert(values[1][1][1] == 3 and values[1][1][2] == 5)
|
||||||
|
readOnly(values)
|
||||||
|
for _, group in values() do
|
||||||
|
readOnly(group)
|
||||||
|
for _, bytes in group() do readOnly(bytes) end
|
||||||
|
end
|
||||||
|
readOnly(ips)
|
||||||
|
readOnly(ips[1])
|
||||||
|
local ip = ips[1][1]
|
||||||
|
assert(ip:String() == "192.0.2.1")
|
||||||
|
readOnly(ip)
|
||||||
|
readOnly(ip:To4())
|
||||||
|
readOnly(ip:To16())
|
||||||
|
readOnly(ip:DefaultMask())
|
||||||
|
assert(ip.AppendText == nil and ip.appendText == nil)
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
readOnly := luar.MT(L, ips[0]).RawGetString("__newindex")
|
||||||
|
NewReadOnlySlicePusher[net.IP](L)
|
||||||
|
if luar.MT(L, ips[0]).RawGetString("__newindex") != readOnly {
|
||||||
|
t.Fatal("nested slice metatable was initialized again")
|
||||||
|
}
|
||||||
|
got := L.GetGlobal("values").(*glua.LUserData).Value.([][][]byte)
|
||||||
|
if &got[0][0][0] != &values[0][0][0] || values[0][0][0] != 3 || values[0][0][1] != 5 {
|
||||||
|
t.Fatal("read-only binding copied or modified the nested slice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type recursiveReadOnlySlice []recursiveReadOnlySlice
|
||||||
|
|
||||||
|
func TestReadOnlyRecursiveSlicePusher(t *testing.T) {
|
||||||
|
L := glua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
values := make(recursiveReadOnlySlice, 1)
|
||||||
|
values[0] = values
|
||||||
|
NewReadOnlySlicePusher[recursiveReadOnlySlice](L)(L, values)
|
||||||
|
L.SetGlobal("values", L.Get(-1))
|
||||||
|
L.Pop(1)
|
||||||
|
if err := L.DoString(`
|
||||||
|
local nested = values[1][1][1]
|
||||||
|
assert(#nested == 1)
|
||||||
|
assert(not pcall(function() nested[1] = nested end))
|
||||||
|
assert(not pcall(function() return nested + nested end))
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSlicePusherMetatablePerState(t *testing.T) {
|
func TestSlicePusherMetatablePerState(t *testing.T) {
|
||||||
first := glua.NewState()
|
first := glua.NewState()
|
||||||
defer first.Close()
|
defer first.Close()
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var privilegedKey struct {
|
||||||
|
sync.Once
|
||||||
|
publicKey ed25519.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrivilegedPublicKey configures the trusted Ed25519 public key once at
|
||||||
|
// startup, before compiling scripts. An empty value disables authorization.
|
||||||
|
// Nonempty values must be unpadded Base64URL encodings of exactly 32 bytes.
|
||||||
|
func SetPrivilegedPublicKey(value string) error {
|
||||||
|
var publicKey ed25519.PublicKey
|
||||||
|
if value != "" {
|
||||||
|
decoded, err := decodePrivilegedValue(value, ed25519.PublicKeySize)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid Lua privileged public key").Base(err)
|
||||||
|
}
|
||||||
|
publicKey = ed25519.PublicKey(decoded)
|
||||||
|
}
|
||||||
|
privilegedKey.Do(func() {
|
||||||
|
privilegedKey.publicKey = publicKey
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePrivilegedValue(value string, size int) ([]byte, error) {
|
||||||
|
encoding := base64.RawURLEncoding.Strict()
|
||||||
|
if len(value) != encoding.EncodedLen(size) {
|
||||||
|
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
|
||||||
|
}
|
||||||
|
decoded, err := encoding.DecodeString(value)
|
||||||
|
if err != nil || len(decoded) != size || encoding.EncodeToString(decoded) != value {
|
||||||
|
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyPrivilegedSignature(code []byte) (bool, error) {
|
||||||
|
_, end, value, found := privilegedSignatureLine(code)
|
||||||
|
if !found {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := privilegedKey.publicKey
|
||||||
|
if len(publicKey) == 0 {
|
||||||
|
return false, errors.New("Lua privileged signature requires --lua-privileged-pubkey")
|
||||||
|
}
|
||||||
|
signature, err := decodePrivilegedValue(string(bytes.TrimSpace(value)), ed25519.SignatureSize)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("invalid Lua privileged signature").Base(err)
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(bytes.TrimSpace(code[end:]))
|
||||||
|
if !ed25519.Verify(publicKey, digest[:], signature) {
|
||||||
|
return false, errors.New("Lua privileged signature verification failed")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignPrivilegedScript signs the script body, then prepends or replaces the signature line.
|
||||||
|
func SignPrivilegedScript(code []byte, seed string) ([]byte, error) {
|
||||||
|
decoded, err := decodePrivilegedValue(seed, ed25519.SeedSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid Ed25519 private key seed").Base(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end, _, found := privilegedSignatureLine(code)
|
||||||
|
body := code
|
||||||
|
if found {
|
||||||
|
body = code[end:]
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := sha256.Sum256(bytes.TrimSpace(body))
|
||||||
|
signature := ed25519.Sign(ed25519.NewKeyFromSeed(decoded), digest[:])
|
||||||
|
header := []byte("-- @xray:privileged-signature=" + base64.RawURLEncoding.EncodeToString(signature))
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if newline := bytes.IndexByte(code, '\n'); newline > 0 && code[newline-1] == '\r' {
|
||||||
|
header = append(header, '\r')
|
||||||
|
}
|
||||||
|
header = append(header, '\n')
|
||||||
|
return append(header, code...), nil
|
||||||
|
}
|
||||||
|
signed := make([]byte, 0, start+len(header)+len(code)-end)
|
||||||
|
signed = append(signed, code[:start]...)
|
||||||
|
signed = append(signed, header...)
|
||||||
|
return append(signed, code[end:]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privilegedSignatureLine(code []byte) (start, end int, value []byte, found bool) {
|
||||||
|
for rest := code; len(rest) != 0; {
|
||||||
|
start = len(code) - len(rest)
|
||||||
|
var line []byte
|
||||||
|
line, rest, _ = bytes.Cut(rest, []byte{'\n'})
|
||||||
|
line = bytes.TrimSuffix(line, []byte{'\r'})
|
||||||
|
if len(bytes.TrimSpace(line)) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end = start + len(line)
|
||||||
|
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
|
||||||
|
if !found {
|
||||||
|
return 0, 0, nil, false
|
||||||
|
}
|
||||||
|
value, found = bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
|
||||||
|
return start, end, value, found
|
||||||
|
}
|
||||||
|
return 0, 0, nil, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
// TODO: Re-enable these tests and their helper when Compile supports privileged signature authorization again.
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func privilegedSeed(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignPrivilegedScriptPreservesSource(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
seed := privilegedSeed(t)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, prefix, header, body, newline string
|
||||||
|
}{
|
||||||
|
{name: "unsigned", body: privilegedFixtureBody, newline: "\n"},
|
||||||
|
{name: "unsigned boundary whitespace", body: " \t\n\n" + privilegedFixtureBody + " \t\n", newline: "\n"},
|
||||||
|
{name: "unsigned CRLF", body: strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n"), newline: "\r\n"},
|
||||||
|
{name: "replace invalid header", header: privilegedSignatureHeader + "%", body: "\n" + privilegedFixtureBody, newline: "\n"},
|
||||||
|
{name: "replace indented header", prefix: " \t\r\n\n", header: " \t--\t @xray:privileged-signature=% \t", body: "\r\n \t\r\n" + privilegedFixtureBody + "\r\n", newline: "\r\n"},
|
||||||
|
{name: "replace valid header", header: privilegedSignatureHeader + privilegedFixtureSignature, body: "\n" + privilegedFixtureBody, newline: "\n"},
|
||||||
|
{name: "empty script", newline: "\n"},
|
||||||
|
{name: "header without newline", header: privilegedSignatureHeader + "%"},
|
||||||
|
{name: "ordinary comment before marker", body: "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", newline: "\n"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
code := []byte(tc.prefix + tc.header + tc.body)
|
||||||
|
original := bytes.Clone(code)
|
||||||
|
signed, err := SignPrivilegedScript(code, seed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(code, original) {
|
||||||
|
t.Fatal("signing modified the input buffer")
|
||||||
|
}
|
||||||
|
body := tc.body
|
||||||
|
if tc.header != "" {
|
||||||
|
body = strings.TrimPrefix(body, tc.newline)
|
||||||
|
}
|
||||||
|
header := privilegedSignatureHeader + testSignature(t, body)
|
||||||
|
want := header + tc.newline + tc.body
|
||||||
|
if tc.header != "" {
|
||||||
|
want = tc.prefix + header + tc.body
|
||||||
|
}
|
||||||
|
if string(signed) != want {
|
||||||
|
t.Fatalf("signed source = %q, want %q", signed, want)
|
||||||
|
}
|
||||||
|
program, err := Compile(writeTestScript(t, string(signed)))
|
||||||
|
if err != nil || program == nil || !program.privileged {
|
||||||
|
t.Fatalf("signed source Compile = %v, %v; want privileged program", program, err)
|
||||||
|
}
|
||||||
|
again, err := SignPrivilegedScript(signed, seed)
|
||||||
|
if err != nil || !bytes.Equal(again, signed) {
|
||||||
|
t.Fatalf("resigning changed the source: %q, %v", again, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignPrivilegedScriptRejectsInvalidSeed(t *testing.T) {
|
||||||
|
seed := privilegedSeed(t)
|
||||||
|
for _, value := range []string{
|
||||||
|
"", "%", seed + "=",
|
||||||
|
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SeedSize-1)),
|
||||||
|
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PrivateKeySize)),
|
||||||
|
} {
|
||||||
|
if signed, err := SignPrivilegedScript([]byte(privilegedFixtureBody), value); signed != nil || err == nil {
|
||||||
|
t.Fatalf("SignPrivilegedScript with invalid seed = %q, %v; want rejection", signed, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
+179
-6
@@ -3,11 +3,14 @@ package lua
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/base64"
|
"github.com/xtls/xray-core/common/base64"
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
"github.com/xtls/xray-core/common/platform"
|
||||||
glua "github.com/yuin/gopher-lua"
|
glua "github.com/yuin/gopher-lua"
|
||||||
"github.com/yuin/gopher-lua/parse"
|
"github.com/yuin/gopher-lua/parse"
|
||||||
@@ -15,7 +18,8 @@ import (
|
|||||||
|
|
||||||
// Program holds immutable bytecode that can be run by independent LStates.
|
// Program holds immutable bytecode that can be run by independent LStates.
|
||||||
type Program struct {
|
type Program struct {
|
||||||
proto *glua.FunctionProto
|
proto *glua.FunctionProto
|
||||||
|
privileged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LStateFactory returns a fully initialized state or nil and an error.
|
// LStateFactory returns a fully initialized state or nil and an error.
|
||||||
@@ -27,10 +31,17 @@ 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)
|
code, err = os.ReadFile(name)
|
||||||
|
// TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries.
|
||||||
|
// if err == nil {
|
||||||
|
// privileged, err = verifyPrivilegedSignature(code)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -44,14 +55,26 @@ 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) {
|
// Choose registrySize for the script type's expected stack usage, balancing
|
||||||
L := glua.NewState()
|
// per-state memory usage against the number of states retained in a pool.
|
||||||
|
func (p *Program) NewState(ctx context.Context, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) (*glua.LState, error) {
|
||||||
|
options := glua.Options{
|
||||||
|
RegistrySize: registrySize,
|
||||||
|
RegistryMaxSize: glua.RegistrySize,
|
||||||
|
RegistryGrowStep: registrySize,
|
||||||
|
}
|
||||||
|
var L *glua.LState
|
||||||
|
if p.privileged {
|
||||||
|
L = glua.NewState(options)
|
||||||
|
} else {
|
||||||
|
L = newSandboxedState(options)
|
||||||
|
}
|
||||||
valid := false
|
valid := false
|
||||||
defer func() {
|
defer func() {
|
||||||
if !valid {
|
if !valid {
|
||||||
@@ -63,6 +86,10 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
|
|||||||
if register != nil {
|
if register != nil {
|
||||||
register(L)
|
register(L)
|
||||||
}
|
}
|
||||||
|
if !p.privileged {
|
||||||
|
// Hide package internals to prevent data snooping and sandbox escape.
|
||||||
|
L.SetGlobal(glua.LoadLibName, glua.LNil)
|
||||||
|
}
|
||||||
L.Push(L.NewFunctionFromProto(p.proto))
|
L.Push(L.NewFunctionFromProto(p.proto))
|
||||||
// Execute the Lua script's top level.
|
// Execute the Lua script's top level.
|
||||||
if err := L.PCall(0, 0, nil); err != nil {
|
if err := L.PCall(0, 0, nil); err != nil {
|
||||||
@@ -77,11 +104,157 @@ func (p *Program) NewState(ctx context.Context, register func(*glua.LState), val
|
|||||||
return L, nil
|
return L, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSandboxedState(options glua.Options) *glua.LState {
|
||||||
|
options.SkipOpenLibs = true
|
||||||
|
L := glua.NewState(options)
|
||||||
|
|
||||||
|
for _, lib := range []struct {
|
||||||
|
name string
|
||||||
|
open glua.LGFunction
|
||||||
|
}{
|
||||||
|
{glua.LoadLibName, glua.OpenPackage},
|
||||||
|
{glua.BaseLibName, glua.OpenBase},
|
||||||
|
{glua.TabLibName, glua.OpenTable},
|
||||||
|
{glua.StringLibName, glua.OpenString},
|
||||||
|
{glua.MathLibName, glua.OpenMath},
|
||||||
|
{glua.OsLibName, glua.OpenOs},
|
||||||
|
} {
|
||||||
|
L.Push(L.NewFunction(lib.open))
|
||||||
|
L.Push(glua.LString(lib.name))
|
||||||
|
L.Call(1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{
|
||||||
|
"dofile", "loadfile", "load", "loadstring", "getfenv", "setfenv",
|
||||||
|
"getmetatable", "setmetatable", "rawget", "rawset",
|
||||||
|
"collectgarbage", "module", "newproxy", "print", "_printregs",
|
||||||
|
} {
|
||||||
|
L.SetGlobal(name, glua.LNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent printing memory addresses that reveal the memory layout.
|
||||||
|
toString := func(value glua.LValue) glua.LString {
|
||||||
|
result := "<opaque>"
|
||||||
|
switch value.Type() {
|
||||||
|
case glua.LTNil, glua.LTBool, glua.LTNumber, glua.LTString:
|
||||||
|
result = value.String()
|
||||||
|
case glua.LTUserData:
|
||||||
|
switch native := value.(*glua.LUserData).Value.(type) {
|
||||||
|
case error: // Errors can also implement fmt.Stringer; redact them first.
|
||||||
|
result = redactMemoryAddresses(native.Error())
|
||||||
|
case fmt.Stringer:
|
||||||
|
result = native.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return glua.LString(result)
|
||||||
|
}
|
||||||
|
L.SetGlobal("tostring", L.NewFunction(func(L *glua.LState) int {
|
||||||
|
L.Push(toString(L.CheckAny(1)))
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Prevent printing memory addresses that reveal the memory layout.
|
||||||
|
stringLib := L.GetGlobal(glua.StringLibName).(*glua.LTable)
|
||||||
|
stringLib.RawSetString("dump", glua.LNil)
|
||||||
|
format := stringLib.RawGetString("format")
|
||||||
|
stringLib.RawSetString("format", L.NewFunction(func(L *glua.LState) int {
|
||||||
|
nargs := L.GetTop()
|
||||||
|
for i := 2; i <= nargs; i++ {
|
||||||
|
value := L.Get(i)
|
||||||
|
switch value.Type() {
|
||||||
|
case glua.LTBool, glua.LTNumber, glua.LTString:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
L.Replace(i, toString(value))
|
||||||
|
}
|
||||||
|
L.Insert(format, 1)
|
||||||
|
L.Call(nargs, 1)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Redact addresses before caught errors become readable by the script.
|
||||||
|
redactError := func(value glua.LValue) glua.LValue {
|
||||||
|
if message, ok := value.(glua.LString); ok {
|
||||||
|
if redacted := redactMemoryAddresses(string(message)); redacted != string(message) {
|
||||||
|
return glua.LString(redacted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
for _, name := range []string{"pcall", "xpcall"} {
|
||||||
|
call := L.GetGlobal(name)
|
||||||
|
L.SetGlobal(name, L.NewFunction(func(L *glua.LState) int {
|
||||||
|
nargs := L.GetTop()
|
||||||
|
if name == "xpcall" {
|
||||||
|
L.CheckFunction(1)
|
||||||
|
handler := L.CheckFunction(2)
|
||||||
|
L.Replace(2, L.NewFunction(func(L *glua.LState) int {
|
||||||
|
L.Replace(1, redactError(L.Get(1)))
|
||||||
|
L.Insert(handler, 1)
|
||||||
|
L.Call(1, 1)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
L.Insert(call, 1)
|
||||||
|
L.Call(nargs, glua.MultRet)
|
||||||
|
if L.Get(1) == glua.LFalse {
|
||||||
|
L.Replace(2, redactError(L.Get(2)))
|
||||||
|
}
|
||||||
|
return L.GetTop()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent seed output from compromising cryptographic security.
|
||||||
|
L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil)
|
||||||
|
|
||||||
|
// Keep only time functions, including when os is accessed through require.
|
||||||
|
osLib := L.GetGlobal(glua.OsLibName).(*glua.LTable)
|
||||||
|
for key, _ := osLib.Next(glua.LNil); key != glua.LNil; key, _ = osLib.Next(key) {
|
||||||
|
switch key {
|
||||||
|
case glua.LString("clock"), glua.LString("date"), glua.LString("difftime"), glua.LString("time"):
|
||||||
|
default:
|
||||||
|
osLib.RawSet(key, glua.LNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent module injection through the global package table.
|
||||||
|
module := L.GetGlobal(glua.LoadLibName).(*glua.LTable)
|
||||||
|
preload := module.RawGetString("preload").(*glua.LTable)
|
||||||
|
loaders := L.CreateTable(1, 0)
|
||||||
|
loaders.RawSetInt(1, L.NewFunction(func(L *glua.LState) int {
|
||||||
|
name := L.CheckString(1)
|
||||||
|
loader := preload.RawGetString(name)
|
||||||
|
if loader == glua.LNil {
|
||||||
|
loader = glua.LString("no preloaded module: " + name)
|
||||||
|
}
|
||||||
|
L.Push(loader)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
module.RawSetString("loaders", loaders)
|
||||||
|
L.SetField(L.Get(glua.RegistryIndex), "_LOADERS", loaders)
|
||||||
|
for _, name := range []string{"loadlib", "seeall", "path", "cpath", "config"} {
|
||||||
|
module.RawSetString(name, glua.LNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return L
|
||||||
|
}
|
||||||
|
|
||||||
|
var memoryAddressPattern = regexp.MustCompile(`0[xX][0-9a-fA-F]+`)
|
||||||
|
|
||||||
|
func redactMemoryAddresses(message string) string {
|
||||||
|
if !strings.Contains(message, "0x") && !strings.Contains(message, "0X") {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return memoryAddressPattern.ReplaceAllString(message, "<address>")
|
||||||
|
}
|
||||||
|
|
||||||
// NewStateFactory returns a factory that gives each state an initialization timeout.
|
// 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 {
|
// Choose registrySize for the script type's expected stack usage, balancing
|
||||||
|
// per-state memory usage against the number of states retained in a pool.
|
||||||
|
func (p *Program) NewStateFactory(initTimeout time.Duration, registrySize int, register func(*glua.LState), validate func(*glua.LState) error) LStateFactory {
|
||||||
return func(ctx context.Context) (*glua.LState, error) {
|
return func(ctx context.Context) (*glua.LState, error) {
|
||||||
initCtx, cancel := context.WithTimeout(ctx, initTimeout)
|
initCtx, cancel := context.WithTimeout(ctx, initTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return p.NewState(initCtx, register, validate)
|
return p.NewState(initCtx, registrySize, register, validate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1001
-15
File diff suppressed because it is too large
Load Diff
@@ -34,12 +34,13 @@ func ReadAsset(file string) ([]byte, error) {
|
|||||||
return ReadFile(path)
|
return ReadFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenAsset(file string) (io.ReadCloser, error) {
|
func OpenAsset(file string) (io.ReadCloser, int64, error) {
|
||||||
path, _, err := getAssetFileLocation(file)
|
path, info, err := getAssetFileLocation(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return NewFileReader(path)
|
r, err := NewFileReader(path)
|
||||||
|
return r, info.Size(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatAsset(file string) (os.FileInfo, error) {
|
func StatAsset(file string) (os.FileInfo, error) {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func GetConfDirPath() string {
|
|||||||
return configPath
|
return configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveLuaFile finds a local Lua script and returns its absolute path.
|
// ResolveLuaFile finds a local .lua script and returns its resolved absolute path.
|
||||||
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
|
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
|
||||||
func ResolveLuaFile(path string) (string, error) {
|
func ResolveLuaFile(path string) (string, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
@@ -113,7 +113,22 @@ func ResolveLuaFile(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resolveFile(paths)
|
path, err := resolveFile(paths)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resolveLuaTarget(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLuaTarget(path string) (string, error) {
|
||||||
|
path, err := filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to resolve Lua file symlinks: %w", err)
|
||||||
|
}
|
||||||
|
if filepath.Ext(path) != ".lua" {
|
||||||
|
return "", errors.New("Lua file extension must be .lua")
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveFile(paths []string) (string, error) {
|
func resolveFile(paths []string) (string, error) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
@@ -88,6 +89,10 @@ func TestResolveLuaFile(t *testing.T) {
|
|||||||
for _, path := range paths[:3] {
|
for _, path := range paths[:3] {
|
||||||
common.Must(os.WriteFile(path, nil, 0o600))
|
common.Must(os.WriteFile(path, nil, 0o600))
|
||||||
}
|
}
|
||||||
|
for i, path := range paths {
|
||||||
|
paths[i], err = filepath.EvalSymlinks(path)
|
||||||
|
common.Must(err)
|
||||||
|
}
|
||||||
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
|
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
|
||||||
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
|
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
|
||||||
}
|
}
|
||||||
@@ -115,3 +120,35 @@ func TestResolveLuaFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveLuaFileSymlink(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
link, target string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"alias.lua", "data.dat", false},
|
||||||
|
{"alias.dat", "script.lua", true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.link, func(t *testing.T) {
|
||||||
|
dir, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
common.Must(err)
|
||||||
|
target := filepath.Join(dir, tc.target)
|
||||||
|
common.Must(os.WriteFile(target, nil, 0o600))
|
||||||
|
link := filepath.Join(dir, tc.link)
|
||||||
|
if err := os.Symlink(tc.target, link); err != nil {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skipf("cannot create symlinks: %v", err)
|
||||||
|
}
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := ResolveLuaFile(link)
|
||||||
|
if tc.valid {
|
||||||
|
if err != nil || got != target {
|
||||||
|
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target)
|
||||||
|
}
|
||||||
|
} else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
|
||||||
|
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-2
@@ -175,9 +175,13 @@ func init() {
|
|||||||
switch v := input.(type) {
|
switch v := input.(type) {
|
||||||
case cmdarg.Arg:
|
case cmdarg.Arg:
|
||||||
r, err := confloader.LoadConfig(v[0])
|
r, err := confloader.LoadConfig(v[0])
|
||||||
common.Must(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
data, err := buf.ReadAllToBytes(r)
|
data, err := buf.ReadAllToBytes(r)
|
||||||
common.Must(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return loadProtobufConfig(data)
|
return loadProtobufConfig(data)
|
||||||
case io.Reader:
|
case io.Reader:
|
||||||
data, err := buf.ReadAllToBytes(v)
|
data, err := buf.ReadAllToBytes(v)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ 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"
|
||||||
|
// 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/all/tls"
|
||||||
"github.com/xtls/xray-core/main/commands/base"
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
)
|
)
|
||||||
@@ -14,9 +16,11 @@ func init() {
|
|||||||
api.CmdAPI,
|
api.CmdAPI,
|
||||||
base64.CmdBase64,
|
base64.CmdBase64,
|
||||||
convert.CmdConvert,
|
convert.CmdConvert,
|
||||||
|
// lua.CmdLua,
|
||||||
tls.CmdTLS,
|
tls.CmdTLS,
|
||||||
cmdUUID,
|
cmdUUID,
|
||||||
cmdX25519,
|
cmdX25519,
|
||||||
|
// cmdEd25519,
|
||||||
cmdWG,
|
cmdWG,
|
||||||
cmdMLDSA65,
|
cmdMLDSA65,
|
||||||
cmdMLKEM768,
|
cmdMLKEM768,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package all
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdEd25519 = &base.Command{
|
||||||
|
UsageLine: `{{.Exec}} ed25519 [-i "private key seed (Base64URL)"]`,
|
||||||
|
Short: "Generate an Ed25519 key pair",
|
||||||
|
Long: `
|
||||||
|
Generate an Ed25519 key pair, or restore it from a 32-byte private key seed
|
||||||
|
with -i. PrivateKey (seed) and PublicKey use unpadded Base64URL.
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputEd25519 = cmdEd25519.Flag.String("i", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdEd25519.Run = executeEd25519
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeEd25519(cmd *base.Command, args []string) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
cmd.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKey ed25519.PrivateKey
|
||||||
|
if *inputEd25519 == "" {
|
||||||
|
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to generate Ed25519 key: %s", err)
|
||||||
|
}
|
||||||
|
privateKey = key
|
||||||
|
} else {
|
||||||
|
encoding := base64.RawURLEncoding.Strict()
|
||||||
|
seed, err := encoding.DecodeString(*inputEd25519)
|
||||||
|
if err != nil || len(seed) != ed25519.SeedSize || encoding.EncodeToString(seed) != *inputEd25519 {
|
||||||
|
base.Fatalf("invalid Ed25519 private key seed: expected 32 bytes in unpadded Base64URL")
|
||||||
|
}
|
||||||
|
privateKey = ed25519.NewKeyFromSeed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Printf("PrivateKey: %s\nPublicKey: %s\n",
|
||||||
|
base64.RawURLEncoding.EncodeToString(privateKey.Seed()),
|
||||||
|
base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))); err != nil {
|
||||||
|
base.Fatalf("failed to write Ed25519 keys: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import "github.com/xtls/xray-core/main/commands/base"
|
||||||
|
|
||||||
|
// CmdLua holds the Lua script commands.
|
||||||
|
var CmdLua = &base.Command{
|
||||||
|
UsageLine: "{{.Exec}} lua",
|
||||||
|
Short: "Manage Lua scripts",
|
||||||
|
Long: "{{.Exec}} {{.LongName}} provides tools for local Lua scripts.",
|
||||||
|
Commands: []*base.Command{
|
||||||
|
cmdSign,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
xlua "github.com/xtls/xray-core/common/lua"
|
||||||
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdSign = &base.Command{
|
||||||
|
UsageLine: "{{.Exec}} lua sign [-key seed] [-o output-file] <input-file>",
|
||||||
|
Short: "Sign a local Lua script for privileged execution",
|
||||||
|
Long: `
|
||||||
|
Sign a local Lua file using a 32-byte Ed25519 private key seed in unpadded
|
||||||
|
Base64URL. Use {{.Exec}} ed25519 to generate a key pair.
|
||||||
|
Write back to the input file, or use -o to write another file (- for stdout).
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
signKey = cmdSign.Flag.String("key", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||||
|
signOutput = cmdSign.Flag.String("o", "", "Output file, or - for stdout. Default: input file.")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdSign.Run = executeSign
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeSign(cmd *base.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
cmd.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(args[0])
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to inspect Lua file: %s", err)
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
base.Fatalf("Lua input is not a regular file: %s", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := os.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to read Lua file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := xlua.SignPrivilegedScript(code, *signKey)
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to sign Lua file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *signOutput == "-" {
|
||||||
|
if _, err := os.Stdout.Write(signed); err != nil {
|
||||||
|
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output := *signOutput
|
||||||
|
if output == "" {
|
||||||
|
output = args[0]
|
||||||
|
}
|
||||||
|
if err := writeSignedFile(output, signed, info.Mode().Perm()); err != nil {
|
||||||
|
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSignedFile(path string, code []byte, mode os.FileMode) error {
|
||||||
|
if info, err := os.Stat(path); err == nil {
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
|
||||||
|
}
|
||||||
|
mode = info.Mode().Perm()
|
||||||
|
path, err = filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.CreateTemp(filepath.Dir(path), ".xray-lua-sign-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
defer file.Close()
|
||||||
|
if _, err := file.Write(code); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Chmod(mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(file.Name(), path)
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ package base
|
|||||||
type CommandEnvHolder struct {
|
type CommandEnvHolder struct {
|
||||||
// Executable name of current binary
|
// Executable name of current binary
|
||||||
Exec string
|
Exec string
|
||||||
|
// Full name of the command being executed
|
||||||
|
CommandName string
|
||||||
// commands column width of current command
|
// commands column width of current command
|
||||||
CommandsWidth int
|
CommandsWidth int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ BigCmdLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildCommandText(cmd)
|
buildCommandText(cmd)
|
||||||
|
CommandEnv.CommandName = cmdName
|
||||||
cmd.Run(cmd, args)
|
cmd.Run(cmd, args)
|
||||||
Exit()
|
Exit()
|
||||||
return
|
return
|
||||||
|
|||||||
Vendored
+35
-1
@@ -8,12 +8,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/utils"
|
"github.com/xtls/xray-core/common/utils"
|
||||||
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
"github.com/xtls/xray-core/main/confloader"
|
"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)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
data, err = os.ReadFile(arg)
|
file, openErr := os.Open(arg)
|
||||||
|
if openErr != nil {
|
||||||
|
return nil, openErr
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
data, err = readConfigFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,6 +48,33 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
|||||||
return
|
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
|
// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL
|
||||||
// or a Unix socket HTTP endpoint.
|
// 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.
|
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
|
|
||||||
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,
|
The -test flag tells Xray to test config files only,
|
||||||
without launching the server.
|
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() {
|
func init() {
|
||||||
cmdRun.Run = executeRun // break init loop
|
cmdRun.Run = executeRun // break init loop
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
|
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.")
|
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.")
|
||||||
|
// 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
|
/* 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,11 @@ func getConfigFormat() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startXray() (core.Server, error) {
|
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)
|
configFiles := getConfigFilePath(true)
|
||||||
|
|
||||||
c, err := core.LoadConfig(getConfigFormat(), configFiles)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -169,17 +169,11 @@ 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 {
|
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
|
||||||
sessionItem.UpdateConn(conn)
|
})
|
||||||
} else {
|
if err != nil {
|
||||||
var err error
|
continue
|
||||||
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(dest net.Destination, payload []byte) ([]byte, error) {
|
|
||||||
return i.udpCodec.EncodeServerPacket(decoded.SessionID, dest, payload)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
payloadBuf := buf.New()
|
payloadBuf := buf.New()
|
||||||
|
|||||||
@@ -336,17 +336,11 @@ 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 {
|
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
|
||||||
sessionItem.UpdateConn(conn)
|
})
|
||||||
} else {
|
if err != nil {
|
||||||
var err error
|
continue
|
||||||
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
|
|
||||||
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pBuf := buf.New()
|
pBuf := buf.New()
|
||||||
|
|||||||
@@ -253,16 +253,10 @@ 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 {
|
if err != nil {
|
||||||
sessionItem.UpdateConn(conn)
|
b.Release()
|
||||||
} else {
|
continue
|
||||||
var err error
|
|
||||||
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
|
|
||||||
if err != nil {
|
|
||||||
b.Release()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
||||||
|
|||||||
@@ -169,8 +169,8 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
|
|||||||
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
|
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,8 +18,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
|
func (s *ServerUDPSession) UpdateConn(conn stat.Connection) {
|
||||||
s.currentConn.Store(conn)
|
if s.currentConn.Load() == nil {
|
||||||
s.timer.Update()
|
s.currentConn.Store(conn)
|
||||||
|
}
|
||||||
|
if s.timer != nil {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
s.timer.Update()
|
if s.timer != nil {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user