mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-16 01:26:07 +00:00
fbad71d620
Second-pass review of the 54-commit self-correcting audit. Each item below was confirmed by reading the surrounding source (and, where practical, the pre-fix code) before being changed; regression tests are included for every behavioral fix. Concurrency: - eventbus: Bus.Subscribe called wg.Add with no synchronization against a concurrent Bus.Stop's wg.Wait, a real "WaitGroup misuse" panic risk (e.g. a Telegram-bot settings save racing panel shutdown/restart). Stop now flips a mu-guarded `stopped` flag before waiting, and Subscribe checks it under the same lock, so Add and Wait can no longer race. Security: - login_limiter: evictForRoom's fallback eviction picked an arbitrary map key, including ones still under an active cooldown - an attacker flooding /login with fresh usernames could evict their own (or anyone's) blocked record and reset the lockout. The fallback now skips actively-blocked records, only falling back to an unconditional evict if the map is somehow entirely full of active blocks (preserves the hard memory cap). Subscription-endpoint panics (reachable by any client hitting /sub): - internal/sub/service.go: applyPathAndHostParams/Obj (ws/httpupgrade/xhttp with no path settings object) and the TLS alpn readers in three places used unchecked type assertions - exactly the bug classabab7cd0patched elsewhere in the same switch statements, just not these call sites. - internal/sub/json_service.go, clash_service.go: the externalProxy loops in the JSON and Clash generators used unchecked assertions on a legacy/admin-supplied field (missing "port", non-object entry, etc.). - internal/sub/json_service.go: realityData's shortId/serverName selection could assert a non-string array element. Other correctness: - client_traffic.go: ResetAllTraffics (touched by3eb214d0) still skipped clearing NodeClientTraffic node-sync baselines, unlike its sibling reset paths in the same file - a node's next sync would re-add pre-reset delta on top of the freshly-zeroed counter. - inbound_traffic.go: the traffic-tick tx's Commit/Rollback errors were silently discarded; now logged so a backend-level commit failure (e.g. an aborted Postgres tx from a best-effort helper) doesn't masquerade as a successful tick. - outbound_subscription.go: the new subscriptionFetchClient doc comment was wedged between fetchAndStore's existing comment and fetchAndStore itself, leaving fetchAndStore undocumented and the comment describing the wrong function. Convention cleanup: - Removed narrative // comments added by the audit that violate this repo's no-inline-comment rule (mostly narrating the specific bug/fix rather than a lasting contract, and mostly on new Test functions, which this repo's existing tests never comment) - calibrated against this exact codebase's own pre-existing comment style so legitimate godoc-style doc comments were left alone.
133 lines
4.7 KiB
Go
133 lines
4.7 KiB
Go
package sub
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
func TestGetSubsToleratesUnusualStreamSettings(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
cases := []struct {
|
|
name string
|
|
stream string
|
|
}{
|
|
{"reality empty arrays", `{"network":"tcp","security":"reality","realitySettings":{"serverNames":[],"shortIds":[],"settings":{"publicKey":"pk"}}}`},
|
|
{"tcp http missing request", `{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"http","response":{"headers":{}}}}}`},
|
|
{"tcp http empty path", `{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"http","request":{"path":[]}}}}`},
|
|
{"grpc missing keys", `{"network":"grpc","security":"none","grpcSettings":{}}`},
|
|
{"empty stream settings", `{}`},
|
|
{"ws missing wsSettings", `{"network":"ws","security":"none"}`},
|
|
{"httpupgrade missing settings", `{"network":"httpupgrade","security":"none"}`},
|
|
{"tls alpn non-string element", `{"network":"tcp","security":"tls","tlsSettings":{"alpn":[123]}}`},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
seedSubDB(t)
|
|
subId := fmt.Sprintf("s%d", i)
|
|
seedSubInbound(t, subId, fmt.Sprintf("t%d", i), 46000+i, 1, tc.stream)
|
|
|
|
links, _, _, _, err := NewSubService("").GetSubs(subId, "req.example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetSubs errored: %v", err)
|
|
}
|
|
if len(links) != 1 {
|
|
t.Fatalf("expected 1 share link, got %d", len(links))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetJsonToleratesHysteriaWithoutHysteriaSettings(t *testing.T) {
|
|
seedSubDB(t)
|
|
db := database.GetDB()
|
|
|
|
const subId = "hy1"
|
|
const email = "hy@e"
|
|
ib := &model.Inbound{
|
|
UserId: 1, Tag: "hy", Enable: true, Listen: "203.0.113.5", Port: 46200,
|
|
Protocol: model.Hysteria,
|
|
Remark: "hy",
|
|
Settings: fmt.Sprintf(`{"version":2,"clients":[{"auth":"hyauth","email":%q,"subId":%q,"enable":true}]}`, email, subId),
|
|
StreamSettings: `{"security":"tls","tlsSettings":{"serverName":"hy.sni"}}`,
|
|
}
|
|
if err := db.Create(ib).Error; err != nil {
|
|
t.Fatalf("seed inbound: %v", err)
|
|
}
|
|
client := &model.ClientRecord{Email: email, SubID: subId, Enable: true}
|
|
if err := db.Create(client).Error; err != nil {
|
|
t.Fatalf("seed client: %v", err)
|
|
}
|
|
if err := db.Create(&model.ClientInbound{ClientId: client.Id, InboundId: ib.Id}).Error; err != nil {
|
|
t.Fatalf("seed client_inbound: %v", err)
|
|
}
|
|
|
|
jsonService := NewSubJsonService("", "", "", NewSubService(""))
|
|
out, _, err := jsonService.GetJson(subId, "sub.example.com", true)
|
|
if err != nil {
|
|
t.Fatalf("GetJson: %v", err)
|
|
}
|
|
if out == "" {
|
|
t.Fatal("GetJson returned empty for a hysteria inbound without hysteriaSettings")
|
|
}
|
|
}
|
|
|
|
func TestGetJsonToleratesNonStringRealityShortId(t *testing.T) {
|
|
seedSubDB(t)
|
|
stream := `{"network":"tcp","security":"reality","realitySettings":{"serverNames":["sni.example.com"],"shortIds":[42],"settings":{"publicKey":"pk"}}}`
|
|
seedSubInbound(t, "rlty1", "rlty", 46400, 1, stream)
|
|
|
|
jsonService := NewSubJsonService("", "", "", NewSubService(""))
|
|
out, _, err := jsonService.GetJson("rlty1", "sub.example.com", true)
|
|
if err != nil {
|
|
t.Fatalf("GetJson: %v", err)
|
|
}
|
|
if out == "" {
|
|
t.Fatal("GetJson returned empty for a reality inbound with a non-string shortId element")
|
|
}
|
|
}
|
|
|
|
func TestGetClashEmitsPinnedCertSha256(t *testing.T) {
|
|
seedSubDB(t)
|
|
const pin = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
|
stream := `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"pin.sni","settings":{"pinnedPeerCertSha256":["` + pin + `"]}}}`
|
|
seedSubInbound(t, "pin1", "pin", 46300, 1, stream)
|
|
|
|
out, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("pin1", "sub.example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetClash: %v", err)
|
|
}
|
|
if !strings.Contains(out, "pin-sha256") {
|
|
t.Fatalf("Clash proxy dropped the pinned cert sha256:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestJsonAndClashTolerateExternalProxyMissingPort(t *testing.T) {
|
|
seedSubDB(t)
|
|
stream := `{"network":"tcp","security":"none","externalProxy":[{"forceTls":"same","dest":"cdn.example.com"}]}`
|
|
seedSubInbound(t, "extp1", "extp", 46500, 1, stream)
|
|
|
|
jsonService := NewSubJsonService("", "", "", NewSubService(""))
|
|
jsonOut, _, err := jsonService.GetJson("extp1", "sub.example.com", true)
|
|
if err != nil {
|
|
t.Fatalf("GetJson: %v", err)
|
|
}
|
|
if jsonOut == "" {
|
|
t.Fatal("GetJson returned empty for an externalProxy entry missing port")
|
|
}
|
|
|
|
clashOut, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("extp1", "sub.example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetClash: %v", err)
|
|
}
|
|
if clashOut == "" {
|
|
t.Fatal("GetClash returned empty for an externalProxy entry missing port")
|
|
}
|
|
}
|