mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fc8efc0745
A subscription renders an inbound's stream and remark once per client and per external proxy, and every client holding its subId, all values a node chooses: an inbound with a megabyte stream and thousands of external proxies, or many clients sharing one subId, made a single unauthenticated request cost gigabytes of memory and minutes of CPU. A reported inbound whose streamSettings exceeds 64 KiB, sniffing 16 KiB or remark 1 KiB, or that lists more than 64 external proxies, is no longer taken in (an existing one keeps its stored values), and a node can no longer give a client a subId another client already holds, including another one in the same snapshot.
442 lines
18 KiB
Go
442 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func readClientUUID(t *testing.T, db *gorm.DB, email string) string {
|
|
t.Helper()
|
|
var row model.ClientRecord
|
|
if err := db.Where("email = ?", email).First(&row).Error; err != nil {
|
|
t.Fatalf("read client %q: %v", email, err)
|
|
}
|
|
return row.UUID
|
|
}
|
|
|
|
// Emails are globally unique, so a node reporting one that belongs to a master
|
|
// inbound would otherwise overwrite its credentials and lock the real user out.
|
|
func TestNodeCannotClaimClientOfAnotherInbound(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
clientSvc := &ClientService{}
|
|
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
|
|
const (
|
|
victim = "victim@x"
|
|
nodeLocal = "nodelocal@x"
|
|
legitUUID = "11111111-1111-1111-1111-111111111111"
|
|
attackUUID = "99999999-9999-9999-9999-999999999999"
|
|
)
|
|
|
|
master := &model.Inbound{
|
|
UserId: 1, Tag: "master-in", Enable: true, Port: 40001, Protocol: model.VLESS,
|
|
Settings: fmt.Sprintf(`{"clients":[{"email":%q,"id":%q,"enable":true}]}`, victim, legitUUID),
|
|
}
|
|
if err := db.Create(master).Error; err != nil {
|
|
t.Fatalf("create master inbound: %v", err)
|
|
}
|
|
masterClients, err := svc.GetClients(master)
|
|
if err != nil {
|
|
t.Fatalf("parse master clients: %v", err)
|
|
}
|
|
if err := clientSvc.SyncInbound(db, master.Id, masterClients); err != nil {
|
|
t.Fatalf("attach master client: %v", err)
|
|
}
|
|
if got := readClientUUID(t, db, victim); got != legitUUID {
|
|
t.Fatalf("setup: master client uuid = %q, want %q", got, legitUUID)
|
|
}
|
|
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
hostile := fmt.Sprintf(`{"clients":[{"email":%q,"id":%q,"enable":true},{"email":%q,"id":%q,"enable":true}]}`,
|
|
victim, attackUUID, nodeLocal, attackUUID)
|
|
syncNodeWithSettings(t, svc, 1, "n1-in", hostile,
|
|
xray.ClientTraffic{Email: victim, Enable: true},
|
|
xray.ClientTraffic{Email: nodeLocal, Enable: true})
|
|
|
|
if got := readClientUUID(t, db, victim); got != legitUUID {
|
|
t.Fatalf("node overwrote a master client's uuid: got %q, want %q", got, legitUUID)
|
|
}
|
|
nodeAttached, err := clientSvc.ListForInbound(db, nodeInboundID(t, db, "n1-in"))
|
|
if err != nil {
|
|
t.Fatalf("list node clients: %v", err)
|
|
}
|
|
for _, c := range nodeAttached {
|
|
if c.Email == victim {
|
|
t.Fatal("node inbound adopted a client that belongs to a master inbound")
|
|
}
|
|
}
|
|
|
|
// The node's own client must still be adopted, or the guard has replaced one
|
|
// bug with a worse one.
|
|
if got := readClientUUID(t, db, nodeLocal); got != attackUUID {
|
|
t.Fatalf("node-owned client not adopted: uuid = %q, want %q", got, attackUUID)
|
|
}
|
|
}
|
|
|
|
func nodeInboundID(t *testing.T, db *gorm.DB, tag string) int {
|
|
t.Helper()
|
|
var ib model.Inbound
|
|
if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
|
|
t.Fatalf("read inbound %q: %v", tag, err)
|
|
}
|
|
return ib.Id
|
|
}
|
|
|
|
func seedMasterInbound(t *testing.T, db *gorm.DB, tag string, port int, clientsJSON string) *model.Inbound {
|
|
t.Helper()
|
|
ib := &model.Inbound{
|
|
UserId: 1, Tag: tag, Enable: true, Port: port, Protocol: model.VLESS,
|
|
Settings: `{"clients":[` + clientsJSON + `]}`,
|
|
}
|
|
if err := db.Create(ib).Error; err != nil {
|
|
t.Fatalf("create inbound %q: %v", tag, err)
|
|
}
|
|
clients, err := (&InboundService{}).GetClients(ib)
|
|
if err != nil {
|
|
t.Fatalf("parse clients of %q: %v", tag, err)
|
|
}
|
|
if err := (&ClientService{}).SyncInbound(db, ib.Id, clients); err != nil {
|
|
t.Fatalf("attach clients of %q: %v", tag, err)
|
|
}
|
|
return ib
|
|
}
|
|
|
|
func readClientRecord(t *testing.T, db *gorm.DB, email string) model.ClientRecord {
|
|
t.Helper()
|
|
var row model.ClientRecord
|
|
if err := db.Where("email = ?", email).First(&row).Error; err != nil {
|
|
t.Fatalf("read client %q: %v", email, err)
|
|
}
|
|
return row
|
|
}
|
|
|
|
func countClientsWhere(t *testing.T, db *gorm.DB, query string, args ...any) int64 {
|
|
t.Helper()
|
|
var n int64
|
|
if err := db.Model(&model.ClientRecord{}).Where(query, args...).Count(&n).Error; err != nil {
|
|
t.Fatalf("count clients: %v", err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func storedInboundSettings(t *testing.T, db *gorm.DB, tag string) string {
|
|
t.Helper()
|
|
var ib model.Inbound
|
|
if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
|
|
t.Fatalf("read inbound %q: %v", tag, err)
|
|
}
|
|
return ib.Settings
|
|
}
|
|
|
|
func nodeHostsClient(t *testing.T, db *gorm.DB, tag, email string) bool {
|
|
t.Helper()
|
|
attached, err := (&ClientService{}).ListForInbound(db, nodeInboundID(t, db, tag))
|
|
if err != nil {
|
|
t.Fatalf("list clients of %q: %v", tag, err)
|
|
}
|
|
for _, c := range attached {
|
|
if c.Email == email {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// A client shared with a master inbound is rendered into the master's own Xray, so the
|
|
// node's copy must not move its credential, reverse tag or subscription id.
|
|
func TestNodeCannotRewriteClientSharedWithMaster(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
|
|
const legitUUID = "11111111-1111-4111-8111-111111111111"
|
|
shared := fmt.Sprintf(`{"email":"shared@x","id":%q,"subId":"shared-sub","enable":true}`, legitUUID)
|
|
seedMasterInbound(t, db, "master-in", 40001, shared)
|
|
nid := 1
|
|
nodeIb := &model.Inbound{
|
|
UserId: 1, Tag: "n1-in", Enable: true, Port: 41001, Protocol: model.VLESS, NodeID: &nid,
|
|
Settings: `{"clients":[` + shared + `]}`,
|
|
}
|
|
if err := db.Create(nodeIb).Error; err != nil {
|
|
t.Fatalf("create node inbound: %v", err)
|
|
}
|
|
clients, _ := svc.GetClients(nodeIb)
|
|
if err := (&ClientService{}).SyncInbound(db, nodeIb.Id, clients); err != nil {
|
|
t.Fatalf("attach node clients: %v", err)
|
|
}
|
|
|
|
syncNodeWithSettings(t, svc, 1, "n1-in",
|
|
`{"clients":[{"email":"shared@x","id":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz","subId":"picked-by-node","reverse":{"tag":"direct"},"enable":true}]}`,
|
|
xray.ClientTraffic{Email: "shared@x", Enable: true})
|
|
|
|
row := readClientRecord(t, db, "shared@x")
|
|
if row.UUID != legitUUID || row.SubID != "shared-sub" || row.Reverse != "" {
|
|
t.Fatalf("node rewrote a shared client: uuid=%q subId=%q reverse=%q", row.UUID, row.SubID, row.Reverse)
|
|
}
|
|
stored := storedInboundSettings(t, db, "n1-in")
|
|
if strings.Contains(stored, "zzzz") || strings.Contains(stored, "picked-by-node") || strings.Contains(stored, "direct") {
|
|
t.Fatalf("node inbound stored the node's values for a shared client: %s", stored)
|
|
}
|
|
if !nodeHostsClient(t, db, "n1-in", "shared@x") {
|
|
t.Fatal("the shared client lost its node link")
|
|
}
|
|
}
|
|
|
|
// A refused claim must leave no trace: the stored settings are re-synced later by
|
|
// auto-renew, and its stats would land on the victim's traffic row.
|
|
func TestNodeRefusedClaimIsNotStoredOrCounted(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
master := seedMasterInbound(t, db, "master-in", 40001, `{"email":"victim@x","id":"11111111-1111-4111-8111-111111111111","subId":"victim-sub","enable":true}`)
|
|
if err := db.Create(&xray.ClientTraffic{InboundId: master.Id, Email: "victim@x", Enable: true}).Error; err != nil {
|
|
t.Fatalf("seed victim traffic: %v", err)
|
|
}
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
|
|
hostile := `{"clients":[{"email":"victim@x","id":"","subId":"picked-by-node","enable":true},{"email":"victim@x ","subId":"padded","enable":true},{"email":"VICTIM@X","subId":"cased","enable":true},{"email":"own@x","enable":true}]}`
|
|
for _, up := range []int64{100, 600} {
|
|
syncNodeWithSettings(t, svc, 1, "n1-in", hostile,
|
|
xray.ClientTraffic{Email: "victim@x", Up: up, Enable: true},
|
|
xray.ClientTraffic{Email: "own@x", Enable: true})
|
|
}
|
|
|
|
if stored := storedInboundSettings(t, db, "n1-in"); strings.Contains(strings.ToLower(stored), "victim") {
|
|
t.Fatalf("refused claim stored on the node inbound: %s", stored)
|
|
}
|
|
if row := readClientRecord(t, db, "victim@x"); row.SubID != "victim-sub" {
|
|
t.Fatalf("victim subId = %q, want victim-sub", row.SubID)
|
|
}
|
|
if n := countClientsWhere(t, db, "LOWER(email) LIKE ?", "victim%"); n != 1 {
|
|
t.Fatalf("a padded or case-variant claim created a look-alike record: %d victim rows", n)
|
|
}
|
|
var counted int64
|
|
db.Model(xray.ClientTraffic{}).Where("LOWER(email) LIKE ? AND up > 0", "victim%").Count(&counted)
|
|
if counted != 0 {
|
|
t.Fatalf("refused claim's stats reached a victim traffic row: %d rows", counted)
|
|
}
|
|
if !nodeHostsClient(t, db, "n1-in", "own@x") {
|
|
t.Fatal("the node's own client was not adopted")
|
|
}
|
|
}
|
|
|
|
// A record with no inbound is the admin's (detached, or its inbound deleted), not a
|
|
// node's; only a node whose own merge orphaned it may take it back.
|
|
func TestNodeCannotAdoptDetachedClient(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
seedNodeRow(t, db, &model.Node{Id: 2, Name: "n2", Address: "127.0.0.2", Port: 2096, ApiToken: "tok2", Enable: true})
|
|
master := seedMasterInbound(t, db, "master-in", 40001,
|
|
`{"email":"detached@x","id":"11111111-1111-4111-8111-111111111111","subId":"d-sub","enable":true},`+
|
|
`{"email":"mine@x","id":"22222222-2222-4222-8222-222222222222","enable":true},`+
|
|
`{"email":"theirs@x","id":"33333333-3333-4333-8333-333333333333","enable":true}`)
|
|
if err := db.Exec("DELETE FROM client_inbounds WHERE inbound_id = ?", master.Id).Error; err != nil {
|
|
t.Fatalf("detach: %v", err)
|
|
}
|
|
for email, node := range map[string]int{"mine@x": 1, "theirs@x": 2} {
|
|
if err := db.Model(&model.ClientRecord{}).Where("email = ?", email).
|
|
Updates(map[string]any{"sync_orphaned_at": 1, "sync_orphaned_by": node}).Error; err != nil {
|
|
t.Fatalf("mark %s orphaned: %v", email, err)
|
|
}
|
|
}
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
|
|
syncNodeWithSettings(t, svc, 1, "n1-in",
|
|
`{"clients":[{"email":"detached@x","subId":"picked","enable":true},{"email":"mine@x","enable":true},{"email":"theirs@x","subId":"picked2","enable":true}]}`)
|
|
|
|
if row := readClientRecord(t, db, "detached@x"); row.SubID != "d-sub" || nodeHostsClient(t, db, "n1-in", "detached@x") {
|
|
t.Fatalf("node adopted a detached client: subId=%q", row.SubID)
|
|
}
|
|
if nodeHostsClient(t, db, "n1-in", "theirs@x") {
|
|
t.Fatal("node adopted a client another node's merge orphaned")
|
|
}
|
|
if !nodeHostsClient(t, db, "n1-in", "mine@x") {
|
|
t.Fatal("node could not take back a client its own merge orphaned")
|
|
}
|
|
}
|
|
|
|
// The subscription server selects by subId alone, so a node's client must not join
|
|
// the subscription of a customer it does not host.
|
|
func TestNodeCannotClaimForeignSubID(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
seedMasterInbound(t, db, "master-in", 40001, `{"email":"customer@x","id":"11111111-1111-4111-8111-111111111111","subId":"cust-sub","enable":true}`)
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
|
|
syncNodeWithSettings(t, svc, 1, "n1-in",
|
|
`{"clients":[{"email":"node-own@x","id":"66666666-6666-4666-8666-666666666666","subId":"cust-sub","enable":true}]}`)
|
|
|
|
if n := countClientsWhere(t, db, "sub_id = ?", "cust-sub"); n != 1 {
|
|
t.Fatalf("node joined a customer's subscription: %d records hold cust-sub", n)
|
|
}
|
|
}
|
|
|
|
// A shared client's tunnel keys are rendered into the master's own WireGuard inbound, so a
|
|
// node tunnel must not own them even when its inbound id is the lower one.
|
|
func TestNodeTunnelDoesNotOwnSharedPeerKeys(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
|
|
const masterKey = "mJ3F8Yb3Kx6cdx6Sx4WbJ3qQDXB5cN3d0pH6m4kq0Ug="
|
|
const nodeKey = "nK9Pq1Zr6s8Lw2Vb4Xc7Dm0Fh5Jt3Ny9Ru1Ea6Go2Is="
|
|
entry := func(key, ip string) string {
|
|
return fmt.Sprintf(`{"email":"shared@x","publicKey":%q,"allowedIPs":[%q],"enable":true}`, key, ip)
|
|
}
|
|
nid := 1
|
|
nodeIb := &model.Inbound{
|
|
UserId: 1, Tag: "n1-wg", Enable: true, Port: 41002, Protocol: model.WireGuard, NodeID: &nid,
|
|
Settings: `{"clients":[` + entry(masterKey, "10.1.0.2/32") + `]}`,
|
|
}
|
|
masterIb := &model.Inbound{
|
|
UserId: 1, Tag: "master-wg", Enable: true, Port: 40002, Protocol: model.WireGuard,
|
|
Settings: `{"clients":[` + entry(masterKey, "10.0.0.2/32") + `]}`,
|
|
}
|
|
for _, ib := range []*model.Inbound{nodeIb, masterIb} {
|
|
if err := db.Create(ib).Error; err != nil {
|
|
t.Fatalf("create %q: %v", ib.Tag, err)
|
|
}
|
|
clients, err := svc.GetClients(ib)
|
|
if err != nil {
|
|
t.Fatalf("parse %q: %v", ib.Tag, err)
|
|
}
|
|
if err := (&ClientService{}).SyncInbound(db, ib.Id, clients); err != nil {
|
|
t.Fatalf("attach %q: %v", ib.Tag, err)
|
|
}
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{
|
|
Tag: "n1-wg", Enable: true, Port: 41002, Protocol: model.WireGuard,
|
|
Settings: `{"clients":[` + entry(nodeKey, "10.1.0.2/32") + `]}`,
|
|
ClientStats: []xray.ClientTraffic{{Email: "shared@x", Enable: true}},
|
|
}}}
|
|
if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
|
|
t.Fatalf("sync: %v", err)
|
|
}
|
|
if !nodeHostsClient(t, db, "n1-wg", "shared@x") {
|
|
t.Fatal("the shared client lost its node tunnel link")
|
|
}
|
|
if got := readClientRecord(t, db, "shared@x").PublicKey; got != masterKey {
|
|
t.Fatalf("node tunnel replaced a shared client's public key: %q", got)
|
|
}
|
|
}
|
|
|
|
// An honest node's copy of a shared client differs only in timestamps and comment; treating
|
|
// that as a refused claim would mark the node dirty and re-push it on every tick.
|
|
func TestNodeSharedClientInAgreementIsNotRewritten(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
|
|
shared := `{"email":"shared@x","id":"11111111-1111-4111-8111-111111111111","subId":"shared-sub","comment":"vip","enable":true}`
|
|
seedMasterInbound(t, db, "master-in", 40001, shared)
|
|
nid := 1
|
|
nodeIb := &model.Inbound{
|
|
UserId: 1, Tag: "n1-in", Enable: true, Port: 41001, Protocol: model.VLESS, NodeID: &nid,
|
|
Settings: `{"clients":[` + shared + `]}`,
|
|
}
|
|
if err := db.Create(nodeIb).Error; err != nil {
|
|
t.Fatalf("create node inbound: %v", err)
|
|
}
|
|
clients, _ := svc.GetClients(nodeIb)
|
|
if err := (&ClientService{}).SyncInbound(db, nodeIb.Id, clients); err != nil {
|
|
t.Fatalf("attach node clients: %v", err)
|
|
}
|
|
|
|
syncNodeWithSettings(t, svc, 1, "n1-in",
|
|
`{"clients":[{"email":"shared@x","id":"11111111-1111-4111-8111-111111111111","subId":"shared-sub","enable":true,"created_at":1,"updated_at":2}]}`,
|
|
xray.ClientTraffic{Email: "shared@x", Enable: true})
|
|
|
|
var node model.Node
|
|
if err := db.Select("config_dirty").Where("id = ?", 1).First(&node).Error; err != nil {
|
|
t.Fatalf("read node: %v", err)
|
|
}
|
|
if node.ConfigDirty {
|
|
t.Fatal("a node in agreement with the master was marked dirty for a re-push")
|
|
}
|
|
}
|
|
|
|
// The refused emails are the node's choice, so the state that keeps their warning to one
|
|
// line must not keep every name a node ever sent.
|
|
func TestNodeRefusedClaimLogStateIsBounded(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
|
|
for i := range 50 {
|
|
syncNodeWithSettings(t, svc, 1, "n1-in", fmt.Sprintf(`{"clients":[{"email":"rotating-%d@x ","enable":true}]}`, i))
|
|
}
|
|
held, _ := reportedRefusedClaims.Load(1)
|
|
if got := len(held.(map[string]struct{})); got != 1 {
|
|
t.Fatalf("refused-claim log state holds %d emails for one node, want 1", got)
|
|
}
|
|
}
|
|
|
|
// A subscription renders every client holding its subId, so a node creating many clients
|
|
// under one subId multiplied the cost of a single request without bound.
|
|
func TestNodeClientsCannotShareASubID(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
|
|
syncNodeWithSettings(t, svc, 1, "n1-in",
|
|
`{"clients":[{"email":"a@x","subId":"one-sub","enable":true},{"email":"b@x","subId":"one-sub","enable":true},{"email":"c@x","subId":"c-sub","enable":true}]}`)
|
|
syncNodeWithSettings(t, svc, 1, "n1-in",
|
|
`{"clients":[{"email":"a@x","subId":"one-sub","enable":true},{"email":"c@x","subId":"c-sub","enable":true},{"email":"d@x","subId":"one-sub","enable":true}]}`)
|
|
|
|
if n := countClientsWhere(t, db, "sub_id = ?", "one-sub"); n != 1 {
|
|
t.Fatalf("%d records hold one subId, want 1", n)
|
|
}
|
|
if !nodeHostsClient(t, db, "n1-in", "c@x") {
|
|
t.Fatal("a client with its own subId was not adopted")
|
|
}
|
|
}
|
|
|
|
// Every reported stream, sniffing and remark is rendered once per client and external
|
|
// proxy of a subscription, so an oversized one let a node make one request cost gigabytes.
|
|
func TestNodeOversizeInboundIsNotTakenIn(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
svc := &InboundService{}
|
|
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
|
|
|
proxies := make([]string, maxNodeExternalProxies+1)
|
|
for i := range proxies {
|
|
proxies[i] = fmt.Sprintf(`{"dest":"h%d.example","port":443,"forceTls":"same"}`, i)
|
|
}
|
|
stream := `{"network":"tcp","externalProxy":[` + strings.Join(proxies, ",") + `]}`
|
|
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
|
|
{Tag: "n1-in", Enable: true, Port: 41001, Protocol: model.VLESS, Settings: `{"clients":[]}`, StreamSettings: stream},
|
|
{Tag: "n1-new", Enable: true, Port: 41002, Protocol: model.VLESS, Settings: `{"clients":[]}`, Remark: strings.Repeat("r", maxNodeRemarkBytes+1)},
|
|
}}
|
|
if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
|
|
t.Fatalf("sync: %v", err)
|
|
}
|
|
|
|
var stored model.Inbound
|
|
if err := db.Where("tag = ?", "n1-in").First(&stored).Error; err != nil {
|
|
t.Fatalf("the oversized report removed the existing inbound: %v", err)
|
|
}
|
|
if strings.Contains(stored.StreamSettings, "externalProxy") {
|
|
t.Fatal("an inbound with too many external proxies was taken in")
|
|
}
|
|
var created int64
|
|
db.Model(&model.Inbound{}).Where("tag = ?", "n1-new").Count(&created)
|
|
if created != 0 {
|
|
t.Fatal("an inbound with an oversized remark was adopted")
|
|
}
|
|
}
|