-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathparental_io.go
More file actions
120 lines (104 loc) · 3.79 KB
/
parental_io.go
File metadata and controls
120 lines (104 loc) · 3.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
File: parental_io.go
Version: 1.3.0
Updated: 11-May-2026 08:37 CEST
Description:
Snapshot persistence for the sdproxy parental subsystem.
Extracted from parental.go to isolate Disk I/O operations.
Changes:
1.3.0 - [LOGGING] Safely guarded snapshot loading debug events natively
through the `logParental` parameter flag.
1.2.0 - [SECURITY/FIX] Addressed a fatal deadlock vulnerability entirely halting
the global DNS pipeline on first-contact queries. Removed the nested
`gs.mu.Lock()` natively within `loadSnapshot`. Conforms explicitly to the
isolated lock boundaries instantiated by `CheckParental`.
1.1.0 - [RELIABILITY] Hardened `saveSnapshot` persistence logic to utilize
atomic `.tmp` file renames. This natively safeguards the system against
JSON corruption in the event of abrupt power loss or kernel panics
during active I/O writes.
*/
package main
import (
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// ---------------------------------------------------------------------------
// Snapshot persistence
// ---------------------------------------------------------------------------
type snapshotData struct {
Remaining map[string]int64 `json:"remaining"`
}
// saveSnapshot writes current budget counters for sk to disk safely via atomic rename.
func saveSnapshot(sk string, gs *groupState) {
path := snapshotPathForKey(sk)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return
}
gs.mu.Lock()
data := snapshotData{Remaining: make(map[string]int64, len(gs.remaining))}
for k, v := range gs.remaining {
data.Remaining[k] = v
}
gs.mu.Unlock()
b, err := json.Marshal(data)
if err != nil {
return
}
// Write out securely to a temporary file first, then atomically rename
// to prevent partial JSON corruption during abrupt power loss.
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, b, 0o644); err == nil {
_ = os.Rename(tmpPath, path)
}
}
// loadSnapshot restores budget counters for sk from today's snapshot file.
// Silent no-op when no snapshot exists for today.
//
// [SECURITY NOTE]
// The caller is strictly responsible for acquiring `gs.mu.Lock()` prior
// to invoking this function to prevent redundant internal deadlock loops.
func loadSnapshot(sk string, gs *groupState) {
path := snapshotPathForKey(sk)
b, err := os.ReadFile(path)
if err != nil {
return // no snapshot today — start fresh
}
var data snapshotData
if err := json.Unmarshal(b, &data); err != nil {
return
}
// [SECURITY/FIX] Do NOT call gs.mu.Lock() here.
// CheckParental explicitly acquires the structural mutex prior to invoking Disk I/O.
// Nested locks inherently instigate fatal deadlocks on identical channels natively.
for k, v := range data.Remaining {
gs.remaining[k] = v
}
if logParental {
log.Printf("[PARENTAL] [%s] Restored budget snapshot from %s", sk, path)
}
}
// ---------------------------------------------------------------------------
// Path & Directory helpers
// ---------------------------------------------------------------------------
// snapshotPathForKey returns a filesystem-safe snapshot path for a state key.
// "/" → "_" and ":" → "-" so device-mode keys are valid filename components.
func snapshotPathForKey(sk string) string {
safe := strings.ReplaceAll(sk, "/", "_")
safe = strings.ReplaceAll(safe, ":", "-")
return filepath.Join(snapshotDir(), "parental-"+safe+"-"+today()+".json")
}
// snapshotDir returns the configured snapshot directory, defaulting to /tmp/sdproxy.
func snapshotDir() string {
if cfg.Parental.SnapshotDir != "" {
return cfg.Parental.SnapshotDir
}
return "/tmp/sdproxy"
}
// today returns today's date as "YYYY-MM-DD" for use in snapshot filenames.
func today() string {
return time.Now().Format("2006-01-02")
}