ui+api: dedup per-app marks by app_key; auto-refresh runtime
This commit is contained in:
@@ -48,6 +48,9 @@ type appMarkItem struct {
|
||||
Cgroup string `json:"cgroup"` // absolute path ("/user.slice/..."), informational
|
||||
CgroupRel string `json:"cgroup_rel"`
|
||||
Level int `json:"level"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
AppKey string `json:"app_key,omitempty"`
|
||||
AddedAt string `json:"added_at"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
@@ -74,6 +77,9 @@ func handleTrafficAppMarks(w http.ResponseWriter, r *http.Request) {
|
||||
op := TrafficAppMarksOp(strings.ToLower(strings.TrimSpace(string(body.Op))))
|
||||
target := strings.ToLower(strings.TrimSpace(body.Target))
|
||||
cgroup := strings.TrimSpace(body.Cgroup)
|
||||
unit := strings.TrimSpace(body.Unit)
|
||||
command := strings.TrimSpace(body.Command)
|
||||
appKey := strings.TrimSpace(body.AppKey)
|
||||
timeoutSec := body.TimeoutSec
|
||||
|
||||
if op == "" {
|
||||
@@ -165,7 +171,7 @@ func handleTrafficAppMarks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := appMarksAdd(target, inodeID, cgAbs, rel, level, ttl); err != nil {
|
||||
if err := appMarksAdd(target, inodeID, cgAbs, rel, level, unit, command, appKey, ttl); err != nil {
|
||||
writeJSON(w, http.StatusOK, TrafficAppMarksResponse{
|
||||
OK: false,
|
||||
Op: string(op),
|
||||
@@ -250,7 +256,7 @@ func appMarksGetStatus() (vpnCount int, directCount int) {
|
||||
return vpnCount, directCount
|
||||
}
|
||||
|
||||
func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int, ttlSec int) error {
|
||||
func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int, unit string, command string, appKey string, ttlSec int) error {
|
||||
target = strings.ToLower(strings.TrimSpace(target))
|
||||
if target != "vpn" && target != "direct" {
|
||||
return fmt.Errorf("invalid target")
|
||||
@@ -271,6 +277,27 @@ func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int,
|
||||
st := loadAppMarksState()
|
||||
changed := pruneExpiredAppMarksLocked(&st, time.Now().UTC())
|
||||
|
||||
unit = strings.TrimSpace(unit)
|
||||
command = strings.TrimSpace(command)
|
||||
appKey = normalizeAppKey(appKey, command)
|
||||
|
||||
// EN: Avoid unbounded growth of marks for the same app.
|
||||
// RU: Не даём бесконечно плодить метки на одно и то же приложение.
|
||||
if appKey != "" {
|
||||
kept := st.Items[:0]
|
||||
for _, it := range st.Items {
|
||||
if strings.ToLower(strings.TrimSpace(it.Target)) == target &&
|
||||
strings.TrimSpace(it.AppKey) == appKey &&
|
||||
it.ID != id {
|
||||
_ = nftDeleteAppMarkRule(target, it.ID)
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, it)
|
||||
}
|
||||
st.Items = kept
|
||||
}
|
||||
|
||||
// Replace any existing rule/state for this (target,id).
|
||||
_ = nftDeleteAppMarkRule(target, id)
|
||||
if err := nftInsertAppMarkRule(target, rel, level, id); err != nil {
|
||||
@@ -284,6 +311,9 @@ func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int,
|
||||
Cgroup: cgAbs,
|
||||
CgroupRel: rel,
|
||||
Level: level,
|
||||
Unit: unit,
|
||||
Command: command,
|
||||
AppKey: appKey,
|
||||
AddedAt: now.Format(time.RFC3339),
|
||||
ExpiresAt: now.Add(time.Duration(ttlSec) * time.Second).Format(time.RFC3339),
|
||||
}
|
||||
@@ -613,6 +643,22 @@ func saveAppMarksState(st appMarksState) error {
|
||||
return os.Rename(tmp, trafficAppMarksPath)
|
||||
}
|
||||
|
||||
func normalizeAppKey(appKey string, command string) string {
|
||||
key := strings.TrimSpace(appKey)
|
||||
if key != "" {
|
||||
return key
|
||||
}
|
||||
cmd := strings.TrimSpace(command)
|
||||
if cmd == "" {
|
||||
return ""
|
||||
}
|
||||
fields := strings.Fields(cmd)
|
||||
if len(fields) > 0 {
|
||||
return strings.TrimSpace(fields[0])
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func isAllDigits(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
|
||||
@@ -200,10 +200,15 @@ const (
|
||||
// EN: Runtime app marking request. Used by per-app launcher wrappers.
|
||||
// RU: Runtime app marking запрос. Используется wrapper-лаунчером per-app.
|
||||
type TrafficAppMarksRequest struct {
|
||||
Op TrafficAppMarksOp `json:"op"`
|
||||
Target string `json:"target"` // vpn|direct
|
||||
Cgroup string `json:"cgroup,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"` // only for add
|
||||
Op TrafficAppMarksOp `json:"op"`
|
||||
Target string `json:"target"` // vpn|direct
|
||||
Cgroup string `json:"cgroup,omitempty"`
|
||||
// EN: Optional metadata to deduplicate marks per-app across restarts / re-runs.
|
||||
// RU: Опциональные метаданные, чтобы не плодить метки на одно и то же приложение.
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
AppKey string `json:"app_key,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"` // only for add
|
||||
}
|
||||
|
||||
type TrafficAppMarksResponse struct {
|
||||
|
||||
Reference in New Issue
Block a user