121 lines
2.5 KiB
Go
121 lines
2.5 KiB
Go
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
|
|
|
package prober
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"net"
|
|
"time"
|
|
|
|
"golang.org/x/net/icmp"
|
|
"golang.org/x/net/ipv4"
|
|
"golang.org/x/net/ipv6"
|
|
|
|
"git.ipng.ch/ipng/vpp-maglev/internal/health"
|
|
)
|
|
|
|
// ICMPProbe sends an ICMP echo request to cfg.Target inside the healthcheck
|
|
// netns and waits for a matching reply.
|
|
func ICMPProbe(ctx context.Context, cfg ProbeConfig) health.ProbeResult {
|
|
isV6 := cfg.Target.To4() == nil
|
|
|
|
var ok bool
|
|
err := inNetns(cfg.HealthCheckNetns, func() error {
|
|
var network string
|
|
var proto int
|
|
var msgType icmp.Type
|
|
if isV6 {
|
|
network = "ip6:ipv6-icmp"
|
|
proto = 58
|
|
msgType = ipv6.ICMPTypeEchoRequest
|
|
} else {
|
|
network = "ip4:icmp"
|
|
proto = 1
|
|
msgType = ipv4.ICMPTypeEcho
|
|
}
|
|
|
|
listenAddr := ""
|
|
if cfg.ProbeSrc != nil {
|
|
listenAddr = cfg.ProbeSrc.String()
|
|
}
|
|
pc, err := net.ListenPacket(network, listenAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("listen icmp (%s): %w", network, err)
|
|
}
|
|
defer pc.Close()
|
|
|
|
id := rand.IntN(0xffff) + 1
|
|
seq := rand.IntN(0xffff) + 1
|
|
|
|
msg := icmp.Message{
|
|
Type: msgType,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: id,
|
|
Seq: seq,
|
|
Data: []byte("maglev-hc"),
|
|
},
|
|
}
|
|
b, err := msg.Marshal(nil)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal icmp: %w", err)
|
|
}
|
|
|
|
dst := &net.IPAddr{IP: cfg.Target}
|
|
if _, err := pc.WriteTo(b, dst); err != nil {
|
|
return fmt.Errorf("write icmp: %w", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(cfg.Timeout)
|
|
pc.SetDeadline(deadline) //nolint:errcheck
|
|
|
|
buf := make([]byte, 1500)
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
n, _, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
if isTimeout(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("read icmp: %w", err)
|
|
}
|
|
|
|
reply, err := icmp.ParseMessage(proto, buf[:n])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
echo, ok2 := reply.Body.(*icmp.Echo)
|
|
if !ok2 {
|
|
continue
|
|
}
|
|
if echo.ID == id && echo.Seq == seq {
|
|
ok = true
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return health.ProbeResult{OK: false, Layer: health.LayerL7, Code: "L7TOUT", Detail: err.Error()}
|
|
}
|
|
if ok {
|
|
return health.ProbeResult{OK: true, Layer: health.LayerL7, Code: "L7OK"}
|
|
}
|
|
return health.ProbeResult{OK: false, Layer: health.LayerL7, Code: "L7TOUT", Detail: "no reply received"}
|
|
}
|
|
|
|
func isTimeout(err error) bool {
|
|
if netErr, ok := err.(net.Error); ok {
|
|
return netErr.Timeout()
|
|
}
|
|
return false
|
|
}
|