diagnostics.go 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. package aerotun
  2. import (
  3. "context"
  4. "fmt"
  5. "os/exec"
  6. "regexp"
  7. "strconv"
  8. "strings"
  9. "time"
  10. )
  11. type HealthCheck struct {
  12. TunnelID string `json:"tunnel_id"`
  13. CheckedAt time.Time `json:"checked_at"`
  14. Reachability string `json:"reachability"`
  15. RTTMillis *float64 `json:"rtt_ms,omitempty"`
  16. RTTUpperBound bool `json:"rtt_upper_bound,omitempty"`
  17. MTU int `json:"mtu"`
  18. MTUProbe string `json:"mtu_probe"`
  19. Explanation string `json:"explanation"`
  20. }
  21. var pingTime = regexp.MustCompile(`time([=<])([0-9]+(?:\.[0-9]+)?)\s*ms`)
  22. // CheckHealth sends at most two ICMP echoes over the configured overlay.
  23. // A failed echo is inconclusive: peer firewalls may block ICMP. It never
  24. // changes link MTU or routing and does not probe the public underlay.
  25. func (m *Manager) CheckHealth(ctx context.Context, id string) (HealthCheck, error) {
  26. if m.health != nil {
  27. if !m.health.checkMu.TryLock() {
  28. return HealthCheck{}, fmt.Errorf("another health check is already running")
  29. }
  30. defer m.health.checkMu.Unlock()
  31. }
  32. t, ok := m.Get(id)
  33. if !ok {
  34. return HealthCheck{}, fmt.Errorf("tunnel not found")
  35. }
  36. if !t.Running || !t.Connected {
  37. return HealthCheck{}, fmt.Errorf("tunnel is not connected")
  38. }
  39. ip, err := peerIP(t.PeerAddress)
  40. if err != nil || ip == nil {
  41. return HealthCheck{}, fmt.Errorf("configure the peer tunnel IP before running a health check")
  42. }
  43. if !interfaceName.MatchString(t.Name) {
  44. return HealthCheck{}, fmt.Errorf("invalid interface name")
  45. }
  46. if _, err = exec.LookPath("ping"); err != nil {
  47. return HealthCheck{}, fmt.Errorf("ping is not installed")
  48. }
  49. family := "-4"
  50. overhead := 28
  51. if ip.To4() == nil {
  52. family = "-6"
  53. overhead = 48
  54. }
  55. 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."}
  56. probe := func(size int) ([]byte, error) {
  57. return exec.CommandContext(ctx, "ping", family, "-n", "-c", "1", "-W", "2", "-I", t.Name, "-M", "do", "-s", strconv.Itoa(size), ip.String()).CombinedOutput()
  58. }
  59. out, err := probe(56)
  60. if err == nil {
  61. result.Reachability = "reachable"
  62. if match := pingTime.FindStringSubmatch(string(out)); match != nil {
  63. v, e := strconv.ParseFloat(match[2], 64)
  64. if e == nil {
  65. result.RTTMillis = &v
  66. result.RTTUpperBound = match[1] == "<"
  67. }
  68. }
  69. }
  70. if ctx.Err() != nil {
  71. return HealthCheck{}, ctx.Err()
  72. }
  73. if t.MTU > overhead {
  74. if _, err = probe(t.MTU - overhead); err == nil {
  75. result.MTUProbe = "reachable"
  76. }
  77. }
  78. if ctx.Err() != nil {
  79. return HealthCheck{}, ctx.Err()
  80. }
  81. if m.health != nil {
  82. m.health.mu.Lock()
  83. 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))})
  84. if err := m.health.saveLocked(); err != nil {
  85. fmt.Printf("AeroTUN health history: %v\n", err)
  86. }
  87. m.health.mu.Unlock()
  88. }
  89. return result, nil
  90. }