From 1ae3f155da7ebe9c1c69e798cc14b86c69cb8926 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:51 +0330 Subject: [PATCH] Import source: internal/checker/checker Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- internal/checker/checker.go | 259 ++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 internal/checker/checker.go diff --git a/internal/checker/checker.go b/internal/checker/checker.go new file mode 100644 index 0000000..ba0cf7c --- /dev/null +++ b/internal/checker/checker.go @@ -0,0 +1,259 @@ +// Package checker implements the four probe types: http, tcp, tls, icmp. +package checker + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "strconv" + "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + + "dash/internal/config" +) + +// Result is the outcome of a single probe. +type Result struct { + TS time.Time + OK bool + Degraded bool // ok but worrying (slow, cert nearing expiry) + LatencyMS float64 // round-trip time + Detail string // machine-readable extra: http status, cert days left + Err string +} + +func fail(start time.Time, format string, args ...any) Result { + return Result{ + TS: start, + LatencyMS: float64(time.Since(start).Microseconds()) / 1000.0, + Err: fmt.Sprintf(format, args...), + } +} + +// Run executes the probe described by t once. +func Run(ctx context.Context, t config.Target) Result { + ctx, cancel := context.WithTimeout(ctx, t.Timeout.Duration) + defer cancel() + + var res Result + switch t.Type { + case config.CheckHTTP: + res = checkHTTP(ctx, t) + case config.CheckTCP: + res = checkTCP(ctx, t) + case config.CheckTLS: + res = checkTLS(ctx, t) + case config.CheckICMP: + res = checkICMP(ctx, t) + case config.CheckDocker: + res = checkDocker(ctx, t) + default: + res = fail(time.Now(), "unknown check type %q", t.Type) + } + if res.OK && t.DegradedLatencyMS > 0 && res.LatencyMS > t.DegradedLatencyMS { + res.Degraded = true + } + return res +} + +func checkHTTP(ctx context.Context, t config.Target) Result { + start := time.Now() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, t.URL, nil) + if err != nil { + return fail(start, "bad url: %v", err) + } + req.Header.Set("User-Agent", "dash-monitor/1.0") + client := &http.Client{ + // Each probe measures a full fresh connection, not a pooled one. + Transport: &http.Transport{DisableKeepAlives: true}, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + resp, err := client.Do(req) + if err != nil { + return fail(start, "%v", err) + } + defer resp.Body.Close() + lat := float64(time.Since(start).Microseconds()) / 1000.0 + + ok := resp.StatusCode < 400 + if t.ExpectStatus != 0 { + ok = resp.StatusCode == t.ExpectStatus + } + res := Result{TS: start, OK: ok, LatencyMS: lat, Detail: strconv.Itoa(resp.StatusCode)} + if !ok { + res.Err = fmt.Sprintf("unexpected status %d", resp.StatusCode) + } + return res +} + +func checkTCP(ctx context.Context, t config.Target) Result { + start := time.Now() + var d net.Dialer + conn, err := d.DialContext(ctx, "tcp", t.Address) + if err != nil { + return fail(start, "%v", err) + } + conn.Close() + return Result{TS: start, OK: true, LatencyMS: float64(time.Since(start).Microseconds()) / 1000.0} +} + +func checkTLS(ctx context.Context, t config.Target) Result { + start := time.Now() + d := tls.Dialer{Config: &tls.Config{}} + conn, err := d.DialContext(ctx, "tcp", t.Address) + if err != nil { + return fail(start, "%v", err) + } + defer conn.Close() + lat := float64(time.Since(start).Microseconds()) / 1000.0 + + state := conn.(*tls.Conn).ConnectionState() + if len(state.PeerCertificates) == 0 { + return fail(start, "no peer certificates") + } + leaf := state.PeerCertificates[0] + days := int(time.Until(leaf.NotAfter).Hours() / 24) + res := Result{TS: start, OK: true, LatencyMS: lat, Detail: strconv.Itoa(days)} + switch { + case days < 0: + res.OK = false + res.Err = fmt.Sprintf("certificate expired %d days ago", -days) + case days < t.WarnDays: + res.Degraded = true + res.Err = fmt.Sprintf("certificate expires in %d days", days) + } + return res +} + +// dockerInspect is the subset of the container-inspect response we read. +type dockerInspect struct { + State struct { + Status string `json:"Status"` + Running bool `json:"Running"` + Health *struct { + Status string `json:"Status"` // starting | healthy | unhealthy + } `json:"Health"` + } `json:"State"` +} + +func checkDocker(ctx context.Context, t config.Target) Result { + start := time.Now() + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", t.Socket) + }, + }, + } + url := "http://docker/containers/" + t.Container + "/json" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fail(start, "bad request: %v", err) + } + resp, err := client.Do(req) + if err != nil { + return fail(start, "docker socket: %v", err) + } + defer resp.Body.Close() + lat := float64(time.Since(start).Microseconds()) / 1000.0 + + if resp.StatusCode == http.StatusNotFound { + return fail(start, "container %q not found", t.Container) + } + if resp.StatusCode != http.StatusOK { + return fail(start, "docker api status %d", resp.StatusCode) + } + var info dockerInspect + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + return fail(start, "decode: %v", err) + } + + res := Result{TS: start, LatencyMS: lat, Detail: info.State.Status} + if !info.State.Running { + res.Err = fmt.Sprintf("container %s", info.State.Status) + return res + } + res.OK = true + if h := info.State.Health; h != nil { + res.Detail = h.Status + switch h.Status { + case "unhealthy": + res.OK = false + res.Err = "healthcheck failing" + case "starting": + res.Degraded = true + res.Err = "healthcheck starting" + } + } + return res +} + +var icmpSeq uint32 + +func checkICMP(ctx context.Context, t config.Target) Result { + start := time.Now() + dst, err := net.ResolveIPAddr("ip4", t.Host) + if err != nil { + return fail(start, "resolve: %v", err) + } + + // Prefer unprivileged datagram ICMP; fall back to a raw socket (needs + // root or CAP_NET_RAW). + var peer net.Addr = &net.UDPAddr{IP: dst.IP} + conn, err := icmp.ListenPacket("udp4", "0.0.0.0") + if err != nil { + conn, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + return fail(start, "icmp socket: %v (needs CAP_NET_RAW or net.ipv4.ping_group_range)", err) + } + peer = dst + } + defer conn.Close() + + icmpSeq++ + msg := icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Body: &icmp.Echo{ + ID: os.Getpid() & 0xffff, + Seq: int(icmpSeq & 0xffff), + Data: []byte("dash-monitor-ping"), + }, + } + wire, err := msg.Marshal(nil) + if err != nil { + return fail(start, "marshal: %v", err) + } + + deadline, _ := ctx.Deadline() + conn.SetDeadline(deadline) + + sent := time.Now() + if _, err := conn.WriteTo(wire, peer); err != nil { + return fail(start, "send: %v", err) + } + + buf := make([]byte, 1500) + for { + n, _, err := conn.ReadFrom(buf) + if err != nil { + return fail(start, "recv: %v", err) + } + reply, err := icmp.ParseMessage(1, buf[:n]) // 1 = ICMPv4 protocol number + if err != nil { + continue + } + if reply.Type == ipv4.ICMPTypeEchoReply { + lat := float64(time.Since(sent).Microseconds()) / 1000.0 + return Result{TS: start, OK: true, LatencyMS: lat} + } + } +}