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 }