mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
61ee202750
The panel runs xray-core's conf.Build() in its own process on inbounds and on outbounds that come from pushes and subscriptions. Some values make it allocate or loop without bound (an XHTTP sessionIDLength, udphop port lists, any number of finalmask entries, each xmc one costing an RSA key), and certificate paths are read whole whatever they name. Refuse those before any build: sessionIDLength above 1024, more than one port range's worth of udphop ports per stream, more than 16 masks per stream, more than 64 xmc masks per outbound subscription, and certificate or key files that are not regular files of at most 1 MiB. Inbound saves, the TUIC readers and the certificate-pin helper apply the file rule too; the pin helper also stops looking at paths that name files on a node.
218 lines
12 KiB
Go
218 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
func TestValidateInboundTLSCertificates(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
streamSettings string
|
|
wantErr bool
|
|
}{
|
|
{"empty stream", "", false},
|
|
{"whitespace stream", " \t\n", false},
|
|
{"none ignores stale TLS settings", `{"security":"none","tlsSettings":{"certificates":[{}]}}`, false},
|
|
{"reality needs no TLS certificate", `{"security":"reality","realitySettings":{}}`, false},
|
|
{"missing TLS settings", `{"security":"tls"}`, true},
|
|
{"uppercase TLS security", `{"security":"TLS","tlsSettings":{}}`, true},
|
|
{"mixed-case TLS security", `{"security":"Tls","tlsSettings":{}}`, true},
|
|
{"null TLS settings", `{"security":"tls","tlsSettings":null}`, true},
|
|
{"missing certificates", `{"security":"tls","tlsSettings":{}}`, true},
|
|
{"null certificates", `{"security":"tls","tlsSettings":{"certificates":null}}`, true},
|
|
{"empty certificates", `{"security":"tls","tlsSettings":{"certificates":[]}}`, true},
|
|
{"null certificate row", `{"security":"tls","tlsSettings":{"certificates":[null]}}`, true},
|
|
{"empty default file fields", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":""}]}}`, true},
|
|
{"empty default inline fields", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":[],"key":[]}]}}`, true},
|
|
{"whitespace file fields", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":" \t","keyFile":" \n"}]}}`, true},
|
|
{"whitespace inline certificate", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":[" ","\t"],"key":["private key"]}]}}`, true},
|
|
{"whitespace inline key", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"key":[" ","\n"]}]}}`, true},
|
|
{"missing private key", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem"}]}}`, true},
|
|
{"missing certificate", `{"security":"tls","tlsSettings":{"certificates":[{"keyFile":"/node/key.pem"}]}}`, true},
|
|
{"verify only", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"verify","certificateFile":"/node/ca.pem"}]}}`, true},
|
|
{"verify with private key still needs server certificate", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"verify","certificateFile":"/node/ca.pem","keyFile":"/node/key.pem"}]}}`, true},
|
|
{"issue needs private key", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"issue","certificateFile":"/node/ca.pem"}]}}`, true},
|
|
{"file credentials with default usage", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
|
|
{"inline credentials", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"key":["private key"]}]}}`, false},
|
|
{"certificate file and inline key", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","key":["private key"]}]}}`, false},
|
|
{"inline certificate and key file", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"keyFile":"/node/key.pem"}]}}`, false},
|
|
{"encipherment usage", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"encipherment","certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
|
|
{"issue usage", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"issue","certificateFile":"/node/ca.pem","keyFile":"/node/ca-key.pem"}]}}`, false},
|
|
{"unknown usage defaults to encipherment like Xray", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"custom","certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
|
|
{"verify and server certificates", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"verify","certificateFile":"/node/ca.pem"},{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
|
|
{"verify usage is case insensitive", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"VERIFY","certificateFile":"/node/ca.pem"},{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
|
|
{"empty extra certificate row", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"},{}]}}`, true},
|
|
{"empty extra verify certificate", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"},{"usage":"verify"}]}}`, true},
|
|
{"whitespace certificate file overrides inline content", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":" ","certificate":["certificate"],"key":["private key"]}]}}`, true},
|
|
{"whitespace key file overrides inline content", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"keyFile":" ","key":["private key"]}]}}`, true},
|
|
{"malformed stream", `{"security":"tls"`, true},
|
|
{"malformed TLS settings", `{"security":"tls","tlsSettings":"invalid"}`, true},
|
|
{"malformed certificate list", `{"security":"tls","tlsSettings":{"certificates":{}}}`, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateInboundTLSCertificates(tt.streamSettings)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("validateInboundTLSCertificates() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateInboundTLSCertificatesIdentifiesIncompleteRow(t *testing.T) {
|
|
err := validateInboundTLSCertificates(`{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"},{"certificateFile":"/node/other.pem"}]}}`)
|
|
if err == nil || !strings.Contains(err.Error(), "TLS certificate 2") || !strings.Contains(err.Error(), "private key") {
|
|
t.Fatalf("expected actionable error for the second certificate's private key, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAddInboundRejectsMissingTLSCertificates(t *testing.T) {
|
|
setupConflictDB(t)
|
|
mgr := useTestRuntimeManager(t)
|
|
fake := &fakeNodeRuntime{}
|
|
mgr.SetLocalRuntimeOverride(fake)
|
|
|
|
inbound := &model.Inbound{
|
|
Tag: "tls-missing-44310",
|
|
Enable: true,
|
|
Listen: "0.0.0.0",
|
|
Port: 44310,
|
|
Protocol: model.VLESS,
|
|
StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":""}]}}`,
|
|
Settings: `{"clients":[]}`,
|
|
}
|
|
_, needRestart, err := (&InboundService{}).AddInbound(inbound)
|
|
if err == nil || !strings.Contains(err.Error(), "TLS") {
|
|
t.Fatalf("AddInbound: expected TLS validation error, got %v", err)
|
|
}
|
|
if needRestart {
|
|
t.Fatal("AddInbound: rejected TLS configuration requested a restart")
|
|
}
|
|
var count int64
|
|
if err := database.GetDB().Model(&model.Inbound{}).Count(&count).Error; err != nil {
|
|
t.Fatalf("count inbounds: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("AddInbound: rejected TLS configuration created %d rows", count)
|
|
}
|
|
if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
|
|
t.Fatal("AddInbound: rejected TLS configuration reached the runtime")
|
|
}
|
|
}
|
|
|
|
func TestUpdateInboundRejectsMissingTLSCertificates(t *testing.T) {
|
|
setupConflictDB(t)
|
|
mgr := useTestRuntimeManager(t)
|
|
fake := &fakeNodeRuntime{}
|
|
mgr.SetLocalRuntimeOverride(fake)
|
|
|
|
seedInboundConflict(t, "tls-existing-44311", "0.0.0.0", 44311, model.VLESS,
|
|
`{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, `{"clients":[]}`)
|
|
var existing model.Inbound
|
|
if err := database.GetDB().Where("tag = ?", "tls-existing-44311").First(&existing).Error; err != nil {
|
|
t.Fatalf("load existing inbound: %v", err)
|
|
}
|
|
update := existing
|
|
update.Remark = "must not be saved"
|
|
update.Port = 44312
|
|
update.StreamSettings = `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem"}]}}`
|
|
_, needRestart, err := (&InboundService{}).UpdateInbound(&update)
|
|
if err == nil || !strings.Contains(err.Error(), "TLS") {
|
|
t.Fatalf("UpdateInbound: expected TLS validation error, got %v", err)
|
|
}
|
|
if needRestart {
|
|
t.Fatal("UpdateInbound: rejected TLS configuration requested a restart")
|
|
}
|
|
var reloaded model.Inbound
|
|
if err := database.GetDB().First(&reloaded, existing.Id).Error; err != nil {
|
|
t.Fatalf("reload existing inbound: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(reloaded, existing) {
|
|
t.Fatal("UpdateInbound: rejected TLS configuration changed the stored inbound")
|
|
}
|
|
if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
|
|
t.Fatal("UpdateInbound: rejected TLS configuration reached the runtime")
|
|
}
|
|
}
|
|
|
|
// The panel used to seed a TLS inbound with an all-empty certificate, so rows in
|
|
// that shape predate the guard and must stay editable — see UpdateInbound.
|
|
func TestUpdateInboundAllowsUntouchedLegacyTLSCertificates(t *testing.T) {
|
|
const legacyStream = `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":"","certificate":[],"key":[]}]}}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
streamSettings string
|
|
}{
|
|
{"remark-only edit resends the stored block", legacyStream},
|
|
{"node push re-encodes the same block", `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"key":[],"certificate":[],"keyFile":"","certificateFile":""}]}}`},
|
|
{"a partial fix to the stored credentials is tolerated", `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem"}]}}`},
|
|
{"completing the credentials is accepted", `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setupConflictDB(t)
|
|
mgr := useTestRuntimeManager(t)
|
|
mgr.SetLocalRuntimeOverride(&fakeNodeRuntime{})
|
|
|
|
seedInboundConflict(t, "tls-legacy-44321", "0.0.0.0", 44321, model.VLESS, legacyStream, `{"clients":[]}`)
|
|
var existing model.Inbound
|
|
if err := database.GetDB().Where("tag = ?", "tls-legacy-44321").First(&existing).Error; err != nil {
|
|
t.Fatalf("load legacy inbound: %v", err)
|
|
}
|
|
|
|
update := existing
|
|
update.Remark = "renamed"
|
|
update.StreamSettings = tt.streamSettings
|
|
if _, _, err := (&InboundService{}).UpdateInbound(&update); err != nil {
|
|
t.Fatalf("UpdateInbound: %v", err)
|
|
}
|
|
var reloaded model.Inbound
|
|
if err := database.GetDB().First(&reloaded, existing.Id).Error; err != nil {
|
|
t.Fatalf("reload inbound: %v", err)
|
|
}
|
|
if reloaded.Remark != "renamed" {
|
|
t.Fatalf("UpdateInbound: remark = %q, want %q", reloaded.Remark, "renamed")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A push names any path on this host, and xray-core reads it whole in the panel process
|
|
// (a device never ends, /dev/zero never stops growing), so it is refused before storing.
|
|
func TestAddInboundRefusesACertificatePathThatIsNoRegularFile(t *testing.T) {
|
|
setupConflictDB(t)
|
|
mgr := useTestRuntimeManager(t)
|
|
fake := &fakeNodeRuntime{}
|
|
mgr.SetLocalRuntimeOverride(fake)
|
|
|
|
dir := filepath.ToSlash(t.TempDir())
|
|
inbound := &model.Inbound{
|
|
Tag: "tls-device-44311",
|
|
Enable: true,
|
|
Listen: "0.0.0.0",
|
|
Port: 44311,
|
|
Protocol: model.VLESS,
|
|
StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"` + dir + `","KeyFile":"` + dir + `"}]}}`,
|
|
Settings: `{"clients":[]}`,
|
|
}
|
|
_, _, err := (&InboundService{FromNodeSync: true}).AddInbound(inbound)
|
|
if err == nil || !strings.Contains(err.Error(), "regular file") {
|
|
t.Fatalf("AddInbound = %v, want the certificate path refused", err)
|
|
}
|
|
var count int64
|
|
if err := database.GetDB().Model(&model.Inbound{}).Count(&count).Error; err != nil || count != 0 {
|
|
t.Fatalf("refused push stored %d row(s) (%v)", count, err)
|
|
}
|
|
if fake.addInbound.Load() != 0 {
|
|
t.Fatal("refused push reached the runtime")
|
|
}
|
|
}
|