-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhook.go
More file actions
155 lines (142 loc) · 3.86 KB
/
hook.go
File metadata and controls
155 lines (142 loc) · 3.86 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package main
import (
"context"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"sync/atomic"
"time"
"github.com/kkkgo/mini-ppdns/mlog"
"github.com/kkkgo/mini-ppdns/pkg/cache"
"codeberg.org/miekg/dns"
)
const (
hookCheckTimeout = 30 * time.Second
hookCmdTimeout = 60 * time.Second
)
type hookMonitor struct {
cfg *HookConfig
failed *atomic.Bool
logger *mlog.Logger
dnsCache *cache.Cache[CacheKey, *dns.Msg]
}
func (hm *hookMonitor) run(ctx context.Context) {
sleepTime := time.Duration(hm.cfg.SleepTime) * time.Second
retryTime := time.Duration(hm.cfg.RetryTime) * time.Second
failCount := 0
wasDown := false
for {
err := hm.check(ctx)
if err == nil {
failCount = 0
if wasDown {
hm.failed.Store(false)
wasDown = false
hm.logger.Infow("[hook] main DNS recovered, switching back to main DNS")
runOptionalCmd(hm.logger, hm.cfg.SwitchMainExec)
}
select {
case <-ctx.Done():
return
case <-time.After(sleepTime):
}
} else {
failCount++
hm.logger.DebugEventw("[hook] check failed",
mlog.Int("count", failCount),
mlog.Int("threshold", hm.cfg.Count),
mlog.Err(err))
if failCount >= hm.cfg.Count && !wasDown {
hm.failed.Store(true)
wasDown = true
// Clear DNS cache so stale main-DNS results are purged immediately
if hm.dnsCache != nil {
hm.dnsCache.Flush()
hm.logger.Infow("[hook] DNS cache flushed")
}
hm.logger.Warnw("[hook] main DNS marked DOWN, switching to fallback",
mlog.Int("failures", failCount))
// Wait retryTime before executing switch_fall_exec so fallback DNS
// is active and available for the script (e.g. sending notifications)
if hm.cfg.SwitchFallExec != "" {
go func(cmd string) {
select {
case <-ctx.Done():
return
case <-time.After(retryTime / 2):
if !hm.failed.Load() {
return
}
runOptionalCmd(hm.logger, cmd)
}
}(hm.cfg.SwitchFallExec)
}
}
select {
case <-ctx.Done():
return
case <-time.After(retryTime):
}
}
}
}
// shellCommand creates an exec.Cmd that runs cmdStr through the system shell.
// Uses SHELL env var on Unix (fallback /bin/sh), cmd.exe on Windows.
func shellCommand(ctx context.Context, cmdStr string) *exec.Cmd {
if runtime.GOOS == "windows" {
return exec.CommandContext(ctx, "cmd", "/C", cmdStr)
}
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}
return exec.CommandContext(ctx, shell, "-c", cmdStr)
}
func (hm *hookMonitor) check(ctx context.Context) error {
checkCtx, cancel := context.WithTimeout(ctx, hookCheckTimeout)
defer cancel()
cmd := shellCommand(checkCtx, hm.cfg.Exec)
output, err := cmd.CombinedOutput()
exitCode := 0
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
// Command failed to run (not found, permission denied, timeout, etc.)
return fmt.Errorf("execution failed: %v", err)
}
}
// If neither exit_code nor keyword is configured, default: success = exit code 0
if !hm.cfg.ExitCodeSet && hm.cfg.Keyword == "" {
if exitCode == 0 {
return nil
}
return fmt.Errorf("exit code %d (expected 0)", exitCode)
}
if hm.cfg.ExitCodeSet && exitCode != hm.cfg.ExitCode {
return fmt.Errorf("exit code %d (expected %d)", exitCode, hm.cfg.ExitCode)
}
if hm.cfg.Keyword != "" && !strings.Contains(string(output), hm.cfg.Keyword) {
return fmt.Errorf("output does not contain keyword %q", hm.cfg.Keyword)
}
return nil
}
func runOptionalCmd(logger *mlog.Logger, cmdStr string) {
if cmdStr == "" {
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), hookCmdTimeout)
defer cancel()
cmd := shellCommand(ctx, cmdStr)
out, err := cmd.CombinedOutput()
if err != nil {
logger.Warnw("[hook] switch exec failed",
mlog.String("cmd", cmdStr),
mlog.Err(err),
mlog.String("output", string(out)))
}
}()
}