package aerotun import ( "context" "fmt" "os/exec" "regexp" "strconv" "strings" "time" ) type HealthCheck struct { TunnelID string `json:"tunnel_id"` CheckedAt time.Time `json:"checked_at"` Reachability string `json:"reachability"` RTTMillis *float64 `json:"rtt_ms,omitempty"` RTTUpperBound bool `json:"rtt_upper_bound,omitempty"` MTU int `json:"mtu"` MTUProbe string `json:"mtu_probe"` Explanation string `json:"explanation"` } var pingTime = regexp.MustCompile(`time([=<])([0-9]+(?:\.[0-9]+)?)\s*ms`) // CheckHealth sends at most two ICMP echoes over the configured overlay. // A failed echo is inconclusive: peer firewalls may block ICMP. It never // changes link MTU or routing and does not probe the public underlay. func (m *Manager) CheckHealth(ctx context.Context, id string) (HealthCheck, error) { if m.health != nil { if !m.health.checkMu.TryLock() { return HealthCheck{}, fmt.Errorf("another health check is already running") } defer m.health.checkMu.Unlock() } t, ok := m.Get(id) if !ok { return HealthCheck{}, fmt.Errorf("tunnel not found") } if !t.Running || !t.Connected { return HealthCheck{}, fmt.Errorf("tunnel is not connected") } ip, err := peerIP(t.PeerAddress) if err != nil || ip == nil { return HealthCheck{}, fmt.Errorf("configure the peer tunnel IP before running a health check") } if !interfaceName.MatchString(t.Name) { return HealthCheck{}, fmt.Errorf("invalid interface name") } if _, err = exec.LookPath("ping"); err != nil { return HealthCheck{}, fmt.Errorf("ping is not installed") } family := "-4" overhead := 28 if ip.To4() == nil { family = "-6" overhead = 48 } result := HealthCheck{TunnelID: id, CheckedAt: time.Now(), Reachability: "inconclusive", MTU: t.MTU, MTUProbe: "inconclusive", Explanation: "ICMP may be filtered; a failed probe does not prove the tunnel is down. A successful overlay probe does not prove the outer path avoids fragmentation."} probe := func(size int) ([]byte, error) { return exec.CommandContext(ctx, "ping", family, "-n", "-c", "1", "-W", "2", "-I", t.Name, "-M", "do", "-s", strconv.Itoa(size), ip.String()).CombinedOutput() } out, err := probe(56) if err == nil { result.Reachability = "reachable" if match := pingTime.FindStringSubmatch(string(out)); match != nil { v, e := strconv.ParseFloat(match[2], 64) if e == nil { result.RTTMillis = &v result.RTTUpperBound = match[1] == "<" } } } if ctx.Err() != nil { return HealthCheck{}, ctx.Err() } if t.MTU > overhead { if _, err = probe(t.MTU - overhead); err == nil { result.MTUProbe = "reachable" } } if ctx.Err() != nil { return HealthCheck{}, ctx.Err() } if m.health != nil { m.health.mu.Lock() m.health.addLocked(HealthEvent{Time: result.CheckedAt, TunnelID: id, Name: t.Name, Type: "check", Status: result.Reachability, Detail: strings.TrimSpace(fmt.Sprintf("Overlay echo: %s; MTU-size echo: %s", result.Reachability, result.MTUProbe))}) if err := m.health.saveLocked(); err != nil { fmt.Printf("AeroTUN health history: %v\n", err) } m.health.mu.Unlock() } return result, nil }