103 lines
2.9 KiB
Go
103 lines
2.9 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int, unit string, command string, appKey string, ttlSec int, vpnIface string) error {
|
|
target = strings.ToLower(strings.TrimSpace(target))
|
|
if target != "vpn" && target != "direct" {
|
|
return fmt.Errorf("invalid target")
|
|
}
|
|
if id == 0 {
|
|
return fmt.Errorf("invalid cgroup id")
|
|
}
|
|
if strings.TrimSpace(rel) == "" || level <= 0 {
|
|
return fmt.Errorf("invalid cgroup path")
|
|
}
|
|
if ttlSec <= 0 {
|
|
ttlSec = defaultAppMarkTTLSeconds
|
|
}
|
|
|
|
appMarksMu.Lock()
|
|
defer appMarksMu.Unlock()
|
|
|
|
st := loadAppMarksState()
|
|
changed := pruneExpiredAppMarksLocked(&st, time.Now().UTC())
|
|
|
|
unit = strings.TrimSpace(unit)
|
|
command = strings.TrimSpace(command)
|
|
appKey = canonicalizeAppKey(appKey, command)
|
|
|
|
// EN: Keep only one effective mark per app and avoid cross-target conflicts.
|
|
// EN: If the same app_key is re-marked with another target, old mark is removed first.
|
|
// RU: Держим только одну эффективную метку на приложение и убираем конфликты между target.
|
|
// RU: Если тот же app_key перемечается на другой target — старая метка удаляется.
|
|
kept := st.Items[:0]
|
|
for _, it := range st.Items {
|
|
itTarget := strings.ToLower(strings.TrimSpace(it.Target))
|
|
itKey := strings.TrimSpace(it.AppKey)
|
|
remove := false
|
|
|
|
// Same cgroup id but different target => conflicting rules (mark+guard).
|
|
if it.ID == id && it.ID != 0 && itTarget != target {
|
|
remove = true
|
|
}
|
|
// Same app_key (if known) should not keep multiple active runtime routes.
|
|
if !remove && appKey != "" && itKey != "" && itKey == appKey {
|
|
if it.ID != id || itTarget != target {
|
|
remove = true
|
|
}
|
|
}
|
|
|
|
if remove {
|
|
_ = nftDeleteAppMarkRule(itTarget, 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, vpnIface); err != nil {
|
|
return err
|
|
}
|
|
if !nftHasAppMarkRule(target, id) {
|
|
_ = nftDeleteAppMarkRule(target, id)
|
|
return fmt.Errorf("appmark rule not active after insert (target=%s id=%d)", target, id)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
expiresAt := ""
|
|
if ttlSec > 0 {
|
|
expiresAt = now.Add(time.Duration(ttlSec) * time.Second).Format(time.RFC3339)
|
|
}
|
|
item := appMarkItem{
|
|
ID: id,
|
|
Target: target,
|
|
Cgroup: cgAbs,
|
|
CgroupRel: rel,
|
|
Level: level,
|
|
Unit: unit,
|
|
Command: command,
|
|
AppKey: appKey,
|
|
AddedAt: now.Format(time.RFC3339),
|
|
ExpiresAt: expiresAt,
|
|
}
|
|
st.Items = upsertAppMarkItem(st.Items, item)
|
|
changed = true
|
|
|
|
if changed {
|
|
if err := saveAppMarksState(st); err != nil {
|
|
// Keep runtime state and nft in sync on disk write errors.
|
|
_ = nftDeleteAppMarkRule(target, id)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|