mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-18 16:17:16 +00:00
17e89db979
The inbound TLS form offered cipherSuites as a closed single-choice list, but xray reads the value as a colon-separated list and accepts any name Go knows, so several suites or one missing from the list could not be set. Both the inbound and the new host field now use a tag picker that keeps the stored value as the colon-joined string xray expects; old single values open unchanged. A host's cipher suites replace the inbound's in the JSON subscription stream, and a blank field inherits them. Share links and Clash carry no cipher suite parameter, so their output is unchanged.
392 lines
12 KiB
Go
392 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
|
)
|
|
|
|
func mkHost(t *testing.T, svc *HostService, inboundId int, remark string, order int) *entity.HostGroup {
|
|
t.Helper()
|
|
created, err := svc.AddHostGroup(&entity.HostGroup{
|
|
InboundIds: []int{inboundId},
|
|
Remark: remark,
|
|
SortOrder: order,
|
|
Hosts: []string{remark + ".example.com"},
|
|
Port: 8443,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddHostGroup %s: %v", remark, err)
|
|
}
|
|
g, err := svc.GetHostGroup(created[0].GroupId)
|
|
if err != nil {
|
|
t.Fatalf("GetHostGroup %s: %v", remark, err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
func TestAddHost_GetHostsByInbound(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
h1 := mkHost(t, svc, ib.Id, "b", 2)
|
|
h2 := mkHost(t, svc, ib.Id, "a", 1)
|
|
|
|
got, err := svc.GetHostsByInbound(ib.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetHostsByInbound: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("len = %d, want 2", len(got))
|
|
}
|
|
if got[0].GroupId != h2.GroupId || got[1].GroupId != h1.GroupId {
|
|
t.Fatalf("order = [%s,%s], want [%s,%s] (sort_order asc)", got[0].GroupId, got[1].GroupId, h2.GroupId, h1.GroupId)
|
|
}
|
|
if got[0].Hosts[0] != "a.example.com:8443" {
|
|
t.Fatalf("address not persisted: %q", got[0].Hosts[0])
|
|
}
|
|
}
|
|
|
|
func TestAddHost_RejectsUnknownInbound(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
if _, err := svc.AddHostGroup(&entity.HostGroup{InboundIds: []int{99999}, Remark: "x", Hosts: []string{"test.com"}}); err == nil {
|
|
t.Fatalf("expected error adding host to unknown inbound")
|
|
}
|
|
}
|
|
|
|
func TestReorderHosts(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
h2 := mkHost(t, svc, ib.Id, "h2", 0)
|
|
h3 := mkHost(t, svc, ib.Id, "h3", 0)
|
|
|
|
want := []string{h3.GroupId, h1.GroupId, h2.GroupId}
|
|
if err := svc.ReorderHostGroups(want); err != nil {
|
|
t.Fatalf("ReorderHostGroups: %v", err)
|
|
}
|
|
got, _ := svc.GetHostsByInbound(ib.Id)
|
|
for i, g := range got {
|
|
if g.GroupId != want[i] {
|
|
t.Fatalf("position %d = %s, want %s", i, g.GroupId, want[i])
|
|
}
|
|
if g.SortOrder != i {
|
|
t.Fatalf("host %s sort_order = %d, want %d", g.GroupId, g.SortOrder, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetHostEnableAndBulk(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
h2 := mkHost(t, svc, ib.Id, "h2", 1)
|
|
|
|
if err := svc.SetHostGroupEnable(h1.GroupId, false); err != nil {
|
|
t.Fatalf("SetHostGroupEnable: %v", err)
|
|
}
|
|
if g, _ := svc.GetHostGroup(h1.GroupId); g == nil || !g.IsDisabled {
|
|
t.Fatalf("h1 should be disabled after SetHostGroupEnable(false)")
|
|
}
|
|
|
|
if err := svc.SetHostsGroupEnable([]string{h1.GroupId, h2.GroupId}, true); err != nil {
|
|
t.Fatalf("SetHostsGroupEnable(true): %v", err)
|
|
}
|
|
for _, gid := range []string{h1.GroupId, h2.GroupId} {
|
|
if g, _ := svc.GetHostGroup(gid); g == nil || g.IsDisabled {
|
|
t.Fatalf("host %s should be enabled", gid)
|
|
}
|
|
}
|
|
if err := svc.SetHostsGroupEnable([]string{h1.GroupId, h2.GroupId}, false); err != nil {
|
|
t.Fatalf("SetHostsGroupEnable(false): %v", err)
|
|
}
|
|
for _, gid := range []string{h1.GroupId, h2.GroupId} {
|
|
if g, _ := svc.GetHostGroup(gid); g == nil || !g.IsDisabled {
|
|
t.Fatalf("host %s should be disabled", gid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeleteHosts(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
h2 := mkHost(t, svc, ib.Id, "h2", 1)
|
|
h3 := mkHost(t, svc, ib.Id, "h3", 2)
|
|
|
|
if err := svc.DeleteHostsGroup([]string{h1.GroupId, h3.GroupId}); err != nil {
|
|
t.Fatalf("DeleteHostsGroup: %v", err)
|
|
}
|
|
got, _ := svc.GetHostsByInbound(ib.Id)
|
|
if len(got) != 1 || got[0].GroupId != h2.GroupId {
|
|
t.Fatalf("remaining = %v, want only h2 (%s)", got, h2.GroupId)
|
|
}
|
|
}
|
|
|
|
func TestDeleteInboundCascadesHosts(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
inboundSvc := &InboundService{}
|
|
ib := &model.Inbound{Tag: "casc", Enable: false, Port: 4443, Protocol: model.VLESS, Settings: `{"clients":[]}`}
|
|
if err := database.GetDB().Create(ib).Error; err != nil {
|
|
t.Fatalf("create inbound: %v", err)
|
|
}
|
|
h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
|
|
if _, err := inboundSvc.DelInbound(ib.Id); err != nil {
|
|
t.Fatalf("DelInbound: %v", err)
|
|
}
|
|
got, _ := svc.GetHostsByInbound(ib.Id)
|
|
if len(got) != 0 {
|
|
t.Fatalf("hosts not cascaded on inbound delete, len = %d", len(got))
|
|
}
|
|
if _, err := svc.GetHostGroup(h1.GroupId); err == nil {
|
|
t.Fatalf("expected group to be deleted after cascading")
|
|
}
|
|
}
|
|
|
|
func TestGetAllTags(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
if _, err := svc.AddHostGroup(&entity.HostGroup{InboundIds: []int{ib.Id}, Remark: "h1", Hosts: []string{"h1.com"}, Tags: []string{"EU", "CDN"}}); err != nil {
|
|
t.Fatalf("AddHostGroup: %v", err)
|
|
}
|
|
if _, err := svc.AddHostGroup(&entity.HostGroup{InboundIds: []int{ib.Id}, Remark: "h2", Hosts: []string{"h2.com"}, Tags: []string{"CDN", "FAST"}}); err != nil {
|
|
t.Fatalf("AddHostGroup: %v", err)
|
|
}
|
|
tags, err := svc.GetAllTags()
|
|
if err != nil {
|
|
t.Fatalf("GetAllTags: %v", err)
|
|
}
|
|
want := []string{"CDN", "EU", "FAST"}
|
|
if len(tags) != len(want) {
|
|
t.Fatalf("tags = %v, want %v", tags, want)
|
|
}
|
|
for i := range want {
|
|
if tags[i] != want[i] {
|
|
t.Fatalf("tags = %v, want %v", tags, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddHostsGroup(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib1 := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
ib2 := mkInbound(t, 80, model.VLESS, `{"clients":[]}`)
|
|
|
|
req := &entity.HostGroup{
|
|
InboundIds: []int{ib1.Id, ib2.Id},
|
|
Hosts: []string{"h1.com", "h2.com:443", "[2001:db8::1]:80"},
|
|
Remark: "BulkRemark",
|
|
Port: 8443,
|
|
Security: "same",
|
|
}
|
|
|
|
created, err := svc.AddHostGroup(req)
|
|
if err != nil {
|
|
t.Fatalf("AddHostGroup: %v", err)
|
|
}
|
|
|
|
if len(created) != 6 {
|
|
t.Fatalf("expected 6 created hosts, got %d", len(created))
|
|
}
|
|
|
|
got1, _ := svc.GetHostsByInbound(ib1.Id)
|
|
if len(got1) != 1 {
|
|
t.Fatalf("expected 1 group for inbound 1, got %d", len(got1))
|
|
}
|
|
|
|
g := got1[0]
|
|
if g.Remark != "BulkRemark" {
|
|
t.Errorf("expected remark BulkRemark, got %s", g.Remark)
|
|
}
|
|
|
|
var foundH2Port443 bool
|
|
var foundIPv6Port80 bool
|
|
var foundH1DefaultPort8443 bool
|
|
|
|
for _, hostStr := range g.Hosts {
|
|
if hostStr == "h2.com:443" {
|
|
foundH2Port443 = true
|
|
}
|
|
if hostStr == "[2001:db8::1]:80" {
|
|
foundIPv6Port80 = true
|
|
}
|
|
if hostStr == "h1.com:8443" {
|
|
foundH1DefaultPort8443 = true
|
|
}
|
|
}
|
|
|
|
if !foundH2Port443 {
|
|
t.Error("missing custom port override host h2.com:443")
|
|
}
|
|
if !foundIPv6Port80 {
|
|
t.Error("missing IPv6 host with port override [2001:db8::1]:80")
|
|
}
|
|
if !foundH1DefaultPort8443 {
|
|
t.Error("missing default port fallback host h1.com:8443")
|
|
}
|
|
}
|
|
|
|
func TestParseHostAndPort_IPv6EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
defaultPort int
|
|
wantAddr string
|
|
wantPort int
|
|
}{
|
|
{"2001:db8::1", 8443, "2001:db8::1", 8443},
|
|
{"[2001:db8::1]:80", 8443, "2001:db8::1", 80},
|
|
{"h1.com:443", 8443, "h1.com", 443},
|
|
{"h1.com", 8443, "h1.com", 8443},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
addr, port := parseHostAndPort(tc.input, tc.defaultPort)
|
|
if addr != tc.wantAddr || port != tc.wantPort {
|
|
t.Errorf("parseHostAndPort(%q, %d) = (%q, %d); want (%q, %d)",
|
|
tc.input, tc.defaultPort, addr, port, tc.wantAddr, tc.wantPort)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseHostAndPort_AdversarialStressCases(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
defaultPort int
|
|
wantAddr string
|
|
wantPort int
|
|
}{
|
|
{"", 8443, "", 8443},
|
|
{" ", 8443, "", 8443},
|
|
{"h1.com: ", 8443, "h1.com:", 8443},
|
|
{"h1.com: -1", 8443, "h1.com: -1", 8443},
|
|
{"h1.com:-1", 8443, "h1.com:-1", 8443},
|
|
{"h1.com:0", 8443, "h1.com", 0},
|
|
{"h1.com:65535", 8443, "h1.com", 65535},
|
|
{"h1.com:65536", 8443, "h1.com:65536", 8443},
|
|
{"h1.com:80a", 8443, "h1.com:80a", 8443},
|
|
{"h1.com:123:456", 8443, "h1.com:123:456", 8443},
|
|
{"[2001:db8::1]", 8443, "2001:db8::1", 8443},
|
|
{"[2001:db8::1]:80", 8443, "2001:db8::1", 80},
|
|
{"2001:db8::1", 8443, "2001:db8::1", 8443},
|
|
{"[2001:db8::1]:65536", 8443, "[2001:db8::1]:65536", 8443},
|
|
{"[]:80", 8443, "", 80},
|
|
{"[:]::80", 8443, "[:]:", 80},
|
|
{"h1.com:", 8443, "h1.com:", 8443},
|
|
{"h1.com:123:", 8443, "h1.com:123:", 8443},
|
|
{" h1.com : 80 ", 8443, "h1.com : 80", 8443},
|
|
{" [2001:db8::1]:80 ", 8443, "2001:db8::1", 80},
|
|
{"[2001:db8::1]:+80", 8443, "2001:db8::1", 80},
|
|
{"[2001:db8::1]:080", 8443, "2001:db8::1", 80},
|
|
{"[2001:db8::1]80", 8443, "[2001:db8::1]80", 8443},
|
|
{"[::1]", 8443, "::1", 8443},
|
|
{"[2001:db8::1", 8443, "[2001:db8:", 1},
|
|
{"[2001:db8::1]:-80", 8443, "[2001:db8::1]:-80", 8443},
|
|
{"h1.com:443:80", 8443, "h1.com:443:80", 8443},
|
|
{"[2001:db8::1]::80", 8443, "[2001:db8::1]:", 80},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
addr, port := parseHostAndPort(tc.input, tc.defaultPort)
|
|
if addr != tc.wantAddr || port != tc.wantPort {
|
|
t.Errorf("parseHostAndPort(%q, %d) = (%q, %d); want (%q, %d)",
|
|
tc.input, tc.defaultPort, addr, port, tc.wantAddr, tc.wantPort)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddHostGroup_OptionalAddress(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
|
|
created, err := svc.AddHostGroup(&entity.HostGroup{
|
|
InboundIds: []int{ib.Id},
|
|
Remark: "OptionalAddressHost",
|
|
Hosts: nil,
|
|
Port: 8443,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddHostGroup with nil Hosts failed: %v", err)
|
|
}
|
|
|
|
if len(created) != 1 {
|
|
t.Fatalf("expected 1 host created, got %d", len(created))
|
|
}
|
|
|
|
g, err := svc.GetHostGroup(created[0].GroupId)
|
|
if err != nil {
|
|
t.Fatalf("GetHostGroup failed: %v", err)
|
|
}
|
|
|
|
if len(g.Hosts) != 1 || g.Hosts[0] != ":8443" {
|
|
t.Fatalf("expected Hosts list to contain default port fallback ':8443', got %v", g.Hosts)
|
|
}
|
|
}
|
|
|
|
func TestUpdateHostGroup_ValidateBeforeDelete(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
|
|
req := &entity.HostGroup{
|
|
InboundIds: []int{99999},
|
|
Remark: "h1-updated",
|
|
Hosts: []string{"h1.com"},
|
|
}
|
|
if _, err := svc.UpdateHostGroup(h1.GroupId, req); err == nil {
|
|
t.Fatalf("expected error updating host group with invalid inbound")
|
|
}
|
|
|
|
got, err := svc.GetHostGroup(h1.GroupId)
|
|
if err != nil {
|
|
t.Fatalf("original host group should not be deleted: %v", err)
|
|
}
|
|
if got.Remark != "h1" {
|
|
t.Fatalf("original host group remark changed: %s", got.Remark)
|
|
}
|
|
|
|
req.InboundIds = []int{ib.Id}
|
|
if _, err := svc.UpdateHostGroup(h1.GroupId, req); err != nil {
|
|
t.Fatalf("valid update failed: %v", err)
|
|
}
|
|
got2, _ := svc.GetHostGroup(h1.GroupId)
|
|
if got2.Remark != "h1-updated" {
|
|
t.Fatalf("remark not updated: %s", got2.Remark)
|
|
}
|
|
}
|
|
|
|
// Host fields are copied by hand in buildHostRows and newHostGroup; a missed
|
|
// copy on either side silently blanks the value on the next edit-and-save.
|
|
func TestHostGroup_CipherSuitesRoundTrip(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &HostService{}
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
const suites = "TLS_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
|
|
created, err := svc.AddHostGroup(&entity.HostGroup{
|
|
InboundIds: []int{ib.Id}, Remark: "cs", Hosts: []string{"cs.example.com"},
|
|
Security: "tls", CipherSuites: suites,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddHostGroup: %v", err)
|
|
}
|
|
g, err := svc.GetHostGroup(created[0].GroupId)
|
|
if err != nil {
|
|
t.Fatalf("GetHostGroup: %v", err)
|
|
}
|
|
if g.CipherSuites != suites {
|
|
t.Fatalf("CipherSuites = %q, want %q", g.CipherSuites, suites)
|
|
}
|
|
}
|