Skip to content

Commit a0a7744

Browse files
SamNet-devclaude
andcommitted
Add enhanced TUI guidance: pre-flight checklist, step descriptions, live warnings, next-steps
- Step description banners before each scan step explain what's being tested - Pre-flight checklist validates setup (resolvers, workers, binaries, NS delegation reminder) - Live pass-rate warning during scan when rate drops below 5% - Inter-step summary shows pass rate and resolver count advancing - Post-scan next-steps guidance (test command, e2e suggestion, guide link) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aae1d09 commit a0a7744

2 files changed

Lines changed: 127 additions & 3 deletions

File tree

cmd/root.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,70 @@ func newProgressFactoryWithTotal(total int) scanner.ProgressFactory {
179179
}
180180
}
181181

182+
func newScanProgressFactory(totalSteps int, descriptions map[string]string) scanner.ProgressFactory {
183+
if !isTTY() {
184+
return nil
185+
}
186+
stepNum := 0
187+
return func(stepName string) scanner.ProgressFunc {
188+
stepNum++
189+
label := fmt.Sprintf("[%d/%d] %s", stepNum, totalSteps, stepName)
190+
191+
// Print step description banner
192+
w := os.Stderr
193+
if desc, ok := descriptions[stepName]; ok {
194+
fmt.Fprintf(w, " %s── %s ─────────────────────────────────%s\n", colorDim, desc, colorReset)
195+
}
196+
197+
start := time.Now()
198+
warnedLow := false
199+
200+
return func(done, total, passed, failed int) {
201+
if total == 0 {
202+
return
203+
}
204+
pct := done * 100 / total
205+
elapsed := time.Since(start).Truncate(time.Second)
206+
bar := progressBar(pct, 20)
207+
fmt.Fprintf(w, "\r\033[2K \033[1m%s\033[0m %s %d/%d \033[32m%d \u2714\033[0m \033[31m%d \u2718\033[0m \033[2m%s\033[0m",
208+
label, bar, done, total, passed, failed, elapsed)
209+
210+
// Live warning: after 25% done with enough samples, if pass rate < 5%
211+
if !warnedLow && done > 20 && done >= total/4 {
212+
rate := passed * 100 / done
213+
if rate < 5 {
214+
warnedLow = true
215+
fmt.Fprintf(w, "\n %s\u26a0 Very low pass rate (%d%%) — check configuration%s\n", colorYellow, rate, colorReset)
216+
}
217+
}
218+
219+
// Completion: inter-step summary
220+
if done == total {
221+
elapsed = time.Since(start).Truncate(time.Second)
222+
passRate := 0
223+
if total > 0 {
224+
passRate = passed * 100 / total
225+
}
226+
color := colorGreen
227+
if passRate < 50 {
228+
color = colorYellow
229+
}
230+
if passRate < 20 {
231+
color = colorRed
232+
}
233+
fmt.Fprintf(w, "\r\033[2K %s\u2714%s %-18s %s%d/%d passed (%d%%)%s %s%s%s\n",
234+
color, colorReset,
235+
stepName,
236+
color, passed, total, passRate, colorReset,
237+
colorDim, elapsed, colorReset)
238+
if stepNum < totalSteps && passed > 0 {
239+
fmt.Fprintf(w, " %s\u21b3 %d resolvers advancing to next step%s\n", colorDim, passed, colorReset)
240+
}
241+
}
242+
}
243+
}
244+
}
245+
182246
func progressBar(pct, width int) string {
183247
filled := pct * width / 100
184248
bar := make([]rune, width)

cmd/scan.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ const (
2424
colorWhite = "\033[37m"
2525
)
2626

27+
var stepDescriptions = map[string]string{
28+
"ping": "Testing ICMP reachability of resolvers",
29+
"resolve": "Checking if resolvers can resolve standard domains",
30+
"nxdomain": "Detecting DNS hijacking on non-existent domains",
31+
"edns": "Testing EDNS0 support and buffer sizes",
32+
"resolve/tunnel": "Verifying resolvers forward queries to your tunnel domain",
33+
"e2e/dnstt": "Full tunnel connectivity test via DNSTT",
34+
"e2e/slipstream": "Full tunnel connectivity test via Slipstream",
35+
"doh/resolve": "Checking DoH resolver connectivity",
36+
"doh/resolve/tunnel": "Verifying DoH resolvers forward to your tunnel domain",
37+
"doh/e2e": "Full DoH tunnel connectivity test via DNSTT",
38+
}
39+
2740
var scanCmd = &cobra.Command{
2841
Use: "scan",
2942
Short: "Full scan pipeline: ping -> resolve -> nxdomain -> tunnel -> e2e",
@@ -168,19 +181,20 @@ func runScan(cmd *cobra.Command, args []string) error {
168181
}
169182

170183
printBanner(len(ips), dohMode, domain, steps)
184+
printPreFlight(len(ips), domain, dnsttBin, slipstreamBin, steps)
171185

172186
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
173187
defer stop()
174188

175189
scanStart := time.Now()
176-
report := scanner.RunChainQuietCtx(ctx, ips, workers, steps, newProgressFactoryWithTotal(len(steps)))
190+
report := scanner.RunChainQuietCtx(ctx, ips, workers, steps, newScanProgressFactory(len(steps), stepDescriptions))
177191
totalTime := time.Since(scanStart)
178192

179193
if ctx.Err() != nil {
180194
fmt.Fprintf(os.Stderr, "\n\n %s⚠ Interrupted — saving partial results to %s%s\n", colorYellow, outputFile, colorReset)
181195
}
182196

183-
printSummary(report, topN, totalTime)
197+
printSummary(report, topN, totalTime, domain)
184198

185199
return scanner.WriteChainReport(report, outputFile)
186200
}
@@ -196,6 +210,29 @@ func hline(left, fill, right string, width int) string {
196210
return left + strings.Repeat(fill, width) + right
197211
}
198212

213+
func printPreFlight(ipCount int, domain, dnsttBin, slipstreamBin string, steps []scanner.Step) {
214+
if !isTTY() {
215+
return
216+
}
217+
w := os.Stderr
218+
fmt.Fprintf(w, " %sPre-flight:%s\n", colorBold, colorReset)
219+
fmt.Fprintf(w, " %s\u2714%s %d resolvers loaded\n", colorGreen, colorReset, ipCount)
220+
fmt.Fprintf(w, " %s\u2714%s %d workers\n", colorGreen, colorReset, workers)
221+
fmt.Fprintf(w, " %s\u2714%s %d scan steps configured\n", colorGreen, colorReset, len(steps))
222+
if dnsttBin != "" {
223+
fmt.Fprintf(w, " %s\u2714%s dnstt-client: %s%s%s\n", colorGreen, colorReset, colorDim, dnsttBin, colorReset)
224+
}
225+
if slipstreamBin != "" {
226+
fmt.Fprintf(w, " %s\u2714%s slipstream-client: %s%s%s\n", colorGreen, colorReset, colorDim, slipstreamBin, colorReset)
227+
}
228+
if domain != "" {
229+
fmt.Fprintf(w, " %s\u26a0%s Domain: %s%s%s — %sverify NS delegation before scanning%s\n",
230+
colorYellow, colorReset, colorCyan, domain, colorReset, colorDim, colorReset)
231+
fmt.Fprintf(w, " %snslookup -type=NS %s 8.8.8.8%s\n", colorDim, domain, colorReset)
232+
}
233+
fmt.Fprintf(w, "\n")
234+
}
235+
199236
func printBanner(count int, doh bool, domain string, steps []scanner.Step) {
200237
mode := "UDP"
201238
if doh {
@@ -232,7 +269,7 @@ func printBanner(count int, doh bool, domain string, steps []scanner.Step) {
232269
fmt.Fprintf(w, "\n\n")
233270
}
234271

235-
func printSummary(report scanner.ChainReport, topN int, totalTime time.Duration) {
272+
func printSummary(report scanner.ChainReport, topN int, totalTime time.Duration, domain string) {
236273
w := os.Stderr
237274

238275
fmt.Fprintf(w, "\n")
@@ -295,6 +332,7 @@ func printSummary(report scanner.ChainReport, topN int, totalTime time.Duration)
295332
break // Only show hint for the first failing step
296333
}
297334
}
335+
fmt.Fprintf(w, "\n %sSee full guide: https://github.com/SamNet-dev/findns/blob/main/GUIDE.md%s\n", colorDim, colorReset)
298336
fmt.Fprintln(w)
299337
return
300338
}
@@ -346,6 +384,28 @@ func printSummary(report scanner.ChainReport, topN int, totalTime time.Duration)
346384
fmt.Fprintf(w, " %s... and %d more in %s%s\n", colorDim, len(report.Passed)-limit, outputFile, colorReset)
347385
}
348386

387+
// Next steps guidance
388+
fmt.Fprintf(w, "\n %sNext steps:%s\n", colorBold, colorReset)
389+
fmt.Fprintf(w, " %s\u2022%s Results saved to %s%s%s\n", colorDim, colorReset, colorCyan, outputFile, colorReset)
390+
if domain != "" && len(report.Passed) > 0 {
391+
topIP := report.Passed[0].IP
392+
fmt.Fprintf(w, " %s\u2022%s Test top resolver: %snslookup %s %s%s\n",
393+
colorDim, colorReset, colorDim, domain, topIP, colorReset)
394+
}
395+
// Check if e2e was in pipeline
396+
hasE2E := false
397+
hasTunnel := false
398+
for _, s := range report.Steps {
399+
if strings.Contains(s.Name, "e2e") {
400+
hasE2E = true
401+
}
402+
if strings.Contains(s.Name, "tunnel") {
403+
hasTunnel = true
404+
}
405+
}
406+
if hasTunnel && !hasE2E && len(report.Passed) > 0 {
407+
fmt.Fprintf(w, " %s\u2022%s Run with --pubkey to test full tunnel connectivity (e2e)\n", colorDim, colorReset)
408+
}
349409
fmt.Fprintln(w)
350410
}
351411

0 commit comments

Comments
 (0)