| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- 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
- }
|