platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -1,10 +1,7 @@
package app
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
@@ -88,201 +85,3 @@ func handleTrafficAudit(w http.ResponseWriter, r *http.Request) {
"nft": nftSummary,
})
}
func findProfileDuplicates(profiles []TrafficAppProfile) []string {
seen := map[string]int{}
for _, p := range profiles {
tgt := strings.ToLower(strings.TrimSpace(p.Target))
key := strings.TrimSpace(p.AppKey)
if tgt == "" || key == "" {
continue
}
seen[tgt+"|"+key]++
}
var out []string
for k, n := range seen {
if n > 1 {
out = append(out, fmt.Sprintf("%s x%d", k, n))
}
}
sort.Strings(out)
return out
}
func findMarkDuplicates(items []appMarkItem) []string {
seen := map[string]int{}
for _, it := range items {
tgt := strings.ToLower(strings.TrimSpace(it.Target))
key := strings.TrimSpace(it.AppKey)
if tgt == "" || key == "" {
continue
}
seen[tgt+"|"+key]++
}
var out []string
for k, n := range seen {
if n > 1 {
out = append(out, fmt.Sprintf("%s x%d", k, n))
}
}
sort.Strings(out)
return out
}
func auditNftAppMarks(state []appMarkItem) (issues []string, summary map[string]any) {
summary = map[string]any{
"output_jump_ok": false,
"output_apps_ok": false,
"state_items": len(state),
"nft_rules": 0,
"missing_rules": 0,
"orphan_rules": 0,
"missing_rule_ids": []string{},
"orphan_rule_ids": []string{},
}
// Check output -> jump output_apps.
outOutput, _, codeOut, errOut := runCommandTimeout(3*time.Second, "nft", "list", "chain", "inet", appMarksTable, "output")
if errOut != nil || codeOut != 0 {
issues = append(issues, "nft_error: failed to read chain output")
} else {
ok := strings.Contains(outOutput, "jump "+appMarksChain)
summary["output_jump_ok"] = ok
if !ok {
issues = append(issues, "nft_missing_jump: output -> output_apps")
}
}
outApps, _, codeApps, errApps := runCommandTimeout(3*time.Second, "nft", "-a", "list", "chain", "inet", appMarksTable, appMarksChain)
if errApps != nil || codeApps != 0 {
issues = append(issues, "nft_error: failed to read chain output_apps")
return issues, summary
}
summary["output_apps_ok"] = true
rules := parseAppMarkRules(outApps)
summary["nft_rules"] = len(rules)
stateIDs := map[string]struct{}{}
for _, it := range state {
tgt := strings.ToLower(strings.TrimSpace(it.Target))
if tgt != "vpn" && tgt != "direct" {
continue
}
if it.ID == 0 {
continue
}
stateIDs[fmt.Sprintf("%s:%d", tgt, it.ID)] = struct{}{}
}
ruleIDs := map[string]struct{}{}
for _, k := range rules {
ruleIDs[k] = struct{}{}
}
missing := []string{}
for k := range stateIDs {
if _, ok := ruleIDs[k]; !ok {
missing = append(missing, k)
}
}
orphan := []string{}
for k := range ruleIDs {
if _, ok := stateIDs[k]; !ok {
orphan = append(orphan, k)
}
}
sort.Strings(missing)
sort.Strings(orphan)
summary["missing_rules"] = len(missing)
summary["orphan_rules"] = len(orphan)
summary["missing_rule_ids"] = missing
summary["orphan_rule_ids"] = orphan
for _, k := range missing {
issues = append(issues, "nft_missing_rule: "+k)
}
for _, k := range orphan {
issues = append(issues, "nft_orphan_rule: "+k)
}
return issues, summary
}
// parseAppMarkRules extracts "target:id" keys from output_apps chain dump.
func parseAppMarkRules(out string) []string {
var keys []string
for _, line := range strings.Split(out, "\n") {
// comment "svpn_appmark:vpn:123"
i := strings.Index(line, appMarkCommentPrefix+":")
if i < 0 {
continue
}
rest := line[i:]
end := len(rest)
for j := 0; j < len(rest); j++ {
ch := rest[j]
if ch == '"' || ch == ' ' || ch == '\t' {
end = j
break
}
}
tag := rest[:end]
parts := strings.Split(tag, ":")
if len(parts) != 3 {
continue
}
tgt := strings.ToLower(strings.TrimSpace(parts[1]))
idRaw := strings.TrimSpace(parts[2])
if tgt != "vpn" && tgt != "direct" {
continue
}
id, err := strconv.ParseUint(idRaw, 10, 64)
if err != nil || id == 0 {
continue
}
keys = append(keys, fmt.Sprintf("%s:%d", tgt, id))
}
sort.Strings(keys)
// Dedup.
outKeys := keys[:0]
var last string
for _, k := range keys {
if k == last {
continue
}
outKeys = append(outKeys, k)
last = k
}
return outKeys
}
func buildTrafficAuditPretty(now string, traffic TrafficModeStatusResponse, profiles []TrafficAppProfile, marks []appMarkItem, issues []string, nft map[string]any) string {
var b strings.Builder
b.WriteString("Traffic audit\n")
b.WriteString("now=" + now + "\n\n")
b.WriteString("traffic: desired=" + string(traffic.DesiredMode) + " applied=" + string(traffic.AppliedMode) + " iface=" + strings.TrimSpace(traffic.ActiveIface) + " healthy=" + strconv.FormatBool(traffic.Healthy) + "\n")
if strings.TrimSpace(traffic.Message) != "" {
b.WriteString("traffic_message: " + strings.TrimSpace(traffic.Message) + "\n")
}
b.WriteString("\n")
b.WriteString(fmt.Sprintf("profiles=%d marks=%d\n", len(profiles), len(marks)))
if nft != nil {
b.WriteString(fmt.Sprintf("nft: rules=%v missing=%v orphan=%v jump_ok=%v\n",
nft["nft_rules"], nft["missing_rules"], nft["orphan_rules"], nft["output_jump_ok"]))
}
b.WriteString("\n")
if len(issues) == 0 {
b.WriteString("issues: none\n")
return b.String()
}
b.WriteString("issues:\n")
for _, it := range issues {
b.WriteString("- " + it + "\n")
}
return b.String()
}