package main import ( "context" "encoding/json" "flag" "fmt" "io" "log/slog" "os" "time" checkdns "internetpressure.io/probe-agent/internal/checks/dns" checkhttp "internetpressure.io/probe-agent/internal/checks/http" checkping "internetpressure.io/probe-agent/internal/checks/ping" checktrace "internetpressure.io/probe-agent/internal/checks/traceroute" "internetpressure.io/probe-agent/internal/client" "internetpressure.io/probe-agent/internal/config" "internetpressure.io/probe-agent/internal/protocol" ) // onceOutput is what `ip-probe once` prints: the batch payload shape without the envelope's probe identity. type onceOutput struct { AgentVersion string `json:"agent_version"` Target protocol.Target `json:"target"` Capabilities []string `json:"capabilities"` Measurements []protocol.Measurement `json:"measurements"` Traceroutes []protocol.Traceroute `json:"traceroutes"` DurationMs float64 `json:"duration_ms"` } // cmdOnce runs http, dns (every resolver), ping and traceroute once against an ad-hoc target. No server needed. func cmdOnce(args []string, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("once", flag.ContinueOnError) fs.SetOutput(stderr) target := fs.String("target", "", "hostname to check (required)") rawURL := fs.String("url", "", "URL for the http check (default https:///)") ip := fs.String("ip", "", "pin the connection to this IP (hostname kept for SNI/Host)") port := fs.Int("port", 443, "port for tcp checks") noTrace := fs.Bool("no-traceroute", false, "skip traceroute") noPing := fs.Bool("no-ping", false, "skip ping") path := fs.String("config", "", "config file (optional: resolvers_override is honoured)") if err := fs.Parse(args); err != nil { return 2 } if *target == "" { fmt.Fprintln(stderr, "once: --target is required") return 2 } cfg, err := config.Load(config.ResolvePath(*path), false) if err != nil { fmt.Fprintf(stderr, "once: %v\n", err) return 1 } log := newLogger("warn", stderr) slog.SetDefault(log) t := protocol.Target{TargetID: "adhoc", Name: *target, Hostname: *target, URL: *rawURL, Port: *port, Checks: []string{"http", "dns", "ping"}, Traceroute: !*noTrace, Tier: 1} if t.URL == "" { t.URL = "https://" + *target + "/" } if *ip != "" { t.IP = ip } resolvers := checkdns.DefaultResolvers if len(cfg.ResolversOverride) > 0 { resolvers = resolvers[:0:0] for _, r := range cfg.ResolversOverride { if id, addr, err := config.ParseResolver(r); err == nil { resolvers = append(resolvers, protocol.Resolver{ID: id, Address: addr}) } } } ua := client.UserAgent(version) httpC := &checkhttp.Checker{UserAgent: ua} dnsC := &checkdns.Checker{} pingC := &checkping.Checker{ICMP: checkping.Detect()} traceC := checktrace.Detect() caps := []string{"http", "dns"} if pingC.ICMP { caps = append(caps, "ping") } else { caps = append(caps, "tcp") } if traceC.Available() { caps = append(caps, "traceroute") } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() start := time.Now() out := onceOutput{AgentVersion: version, Target: t, Capabilities: caps, Traceroutes: []protocol.Traceroute{}} out.Measurements = append(out.Measurements, httpC.Run(ctx, t)) out.Measurements = append(out.Measurements, dnsC.Run(ctx, t, resolvers)...) if !*noPing { out.Measurements = append(out.Measurements, pingC.Run(ctx, t)) } if !*noTrace { if traceC.Available() { tr, err := traceC.Run(ctx, t) if err != nil { fmt.Fprintf(stderr, "traceroute: %v\n", err) } else { out.Traceroutes = append(out.Traceroutes, tr) } } else { fmt.Fprintln(stderr, "traceroute: binary not found, skipped") } } out.DurationMs = protocol.Ms(time.Since(start)) enc := json.NewEncoder(stdout) enc.SetIndent("", " ") if err := enc.Encode(out); err != nil { fmt.Fprintf(os.Stderr, "once: %v\n", err) return 1 } return 0 }