feat(healthcheck/icmp): use the same id and increasing sequence number

- easier debug logs flow
- MAY cost less conntrack-ed slots on the VPN server
- resets id and sequence to 1 when reconnecting VPN
- resets id and sequence to 1 after 5 minutes
- resets id and sequence to 1 if sequence is complete (65535)
This commit is contained in:
Quentin McGaw
2025-11-26 13:38:53 +00:00
parent 08a7aae5f1
commit c25c9f6f0e
2 changed files with 53 additions and 22 deletions
+1
View File
@@ -74,6 +74,7 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
// restore forced check type to dns if icmp was found to be not permitted // restore forced check type to dns if icmp was found to be not permitted
c.smallCheckType = smallCheckDNS c.smallCheckType = smallCheckDNS
} }
c.echoer.Reset()
err = c.startupCheck(ctx) err = c.startupCheck(ctx)
if err != nil { if err != nil {
+52 -22
View File
@@ -12,6 +12,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"strings" "strings"
"time"
"golang.org/x/net/icmp" "golang.org/x/net/icmp"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
@@ -27,6 +28,9 @@ type Echoer struct {
buffer []byte buffer []byte
randomSource io.Reader randomSource io.Reader
logger Logger logger Logger
seqStart time.Time
id int
seq int
} }
func NewEchoer(logger Logger) *Echoer { func NewEchoer(logger Logger) *Echoer {
@@ -42,12 +46,26 @@ func NewEchoer(logger Logger) *Echoer {
} }
} }
// Reset resets the [Echoer] icmp echo parameters:
// - ID is assigned a new random value
// - sequence is reset to 1
// - sequence start time is set to now
// It is used when the sequence is complete or when the VPN reconnects.
func (e *Echoer) Reset() {
const uint16Bytes = 2
idBytes := make([]byte, uint16Bytes)
_, _ = e.randomSource.Read(idBytes)
e.id = int(binary.BigEndian.Uint16(idBytes))
e.seq = 1
e.seqStart = time.Now()
}
var ( var (
ErrTimedOut = errors.New("timed out waiting for ICMP echo reply") ErrTimedOut = errors.New("timed out waiting for ICMP echo reply")
ErrNotPermitted = errors.New("not permitted") ErrNotPermitted = errors.New("not permitted")
) )
func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) { func (e *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
var ipVersion string var ipVersion string
var conn net.PacketConn var conn net.PacketConn
if ip.Is4() { if ip.Is4() {
@@ -69,8 +87,13 @@ func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
conn.Close() conn.Close()
}() }()
const echoDataSize = 32 const maxSeq = 1<<16 - 1
id, message := buildMessageToSend(ipVersion, echoDataSize, i.randomSource) const refreshIDInterval = 5 * time.Minute
if e.seq > maxSeq && time.Since(e.seqStart) >= refreshIDInterval {
e.Reset()
}
message := buildMessageToSend(ipVersion, e.id, e.seq, e.randomSource)
encodedMessage, err := message.Marshal(nil) encodedMessage, err := message.Marshal(nil)
if err != nil { if err != nil {
@@ -84,8 +107,11 @@ func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
} }
return fmt.Errorf("writing ICMP message to %s: %w", ip, err) return fmt.Errorf("writing ICMP message to %s: %w", ip, err)
} }
defer func() {
e.seq++
}()
receivedData, err := receiveEchoReply(conn, id, i.buffer, ipVersion, i.logger) receivedData, err := receiveEchoReply(conn, e.id, e.seq, e.buffer, ipVersion, e.logger)
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) && ctx.Err() != nil { if errors.Is(err, net.ErrClosed) && ctx.Err() != nil {
return fmt.Errorf("%w from %s", ErrTimedOut, ip) return fmt.Errorf("%w from %s", ErrTimedOut, ip)
@@ -101,12 +127,7 @@ func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
return nil return nil
} }
func buildMessageToSend(ipVersion string, size uint, randomSource io.Reader) (id int, message *icmp.Message) { func buildMessageToSend(ipVersion string, id, seq int, randomSource io.Reader) (message *icmp.Message) {
const uint16Bytes = 2
idBytes := make([]byte, uint16Bytes)
_, _ = randomSource.Read(idBytes)
id = int(binary.BigEndian.Uint16(idBytes))
var icmpType icmp.Type var icmpType icmp.Type
switch ipVersion { switch ipVersion {
case "v4": case "v4":
@@ -116,6 +137,7 @@ func buildMessageToSend(ipVersion string, size uint, randomSource io.Reader) (id
default: default:
panic(fmt.Sprintf("IP version %q not supported", ipVersion)) panic(fmt.Sprintf("IP version %q not supported", ipVersion))
} }
const size = 32
messageBodyData := make([]byte, size) messageBodyData := make([]byte, size)
_, _ = randomSource.Read(messageBodyData) _, _ = randomSource.Read(messageBodyData)
@@ -126,14 +148,14 @@ func buildMessageToSend(ipVersion string, size uint, randomSource io.Reader) (id
Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6) Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6)
Body: &icmp.Echo{ Body: &icmp.Echo{
ID: id, ID: id,
Seq: 0, // only one packet Seq: seq,
Data: messageBodyData, Data: messageBodyData,
}, },
} }
return id, message return message
} }
func receiveEchoReply(conn net.PacketConn, id int, buffer []byte, ipVersion string, logger Logger, func receiveEchoReply(conn net.PacketConn, id, seq int, buffer []byte, ipVersion string, logger Logger,
) (data []byte, err error) { ) (data []byte, err error) {
var icmpProtocol int var icmpProtocol int
const ( const (
@@ -167,27 +189,35 @@ func receiveEchoReply(conn net.PacketConn, id int, buffer []byte, ipVersion stri
switch body := message.Body.(type) { switch body := message.Body.(type) {
case *icmp.Echo: case *icmp.Echo:
if id != body.ID { switch {
case id != body.ID:
logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+ logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+
"(id: %d, type: %d, code: %d, length: %d, return address %s)", "(id: %d, seq: %d, type: %d, code: %d, length: %d, return address %s)",
id, body.ID, message.Type, message.Code, len(packetBytes), returnAddr) id, body.Seq, body.ID, message.Type, message.Code, len(packetBytes), returnAddr)
continue // not the ID we are looking for continue // not the ID we are looking for
case seq != body.Seq:
logger.Warnf("ignoring ICMP echo reply mismatching expected sequence number %d "+
"(id: %d, seq: %d, type: %d, code: %d, length: %d, return address %s)",
seq, body.ID, body.Seq, message.Type, message.Code, len(packetBytes), returnAddr)
continue // not the seq we are looking for
} }
return body.Data, nil return body.Data, nil
case *icmp.DstUnreach: case *icmp.DstUnreach:
logger.Debugf("ignoring ICMP destination unreachable message (type: 3, code: %d, return address %s, expected-id %d)", logger.Debugf("ignoring ICMP destination unreachable message "+
message.Code, returnAddr, id) "(type: 3, code: %d, return address %s, expected id %d and seq %d)",
message.Code, returnAddr, id, seq)
// See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249 // See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249
// on why we ignore this message. If it is actually unreachable, the timeout on waiting for // on why we ignore this message. If it is actually unreachable, the timeout on waiting for
// the echo reply will do instead of returning an error error. // the echo reply will do instead of returning an error error.
continue continue
case *icmp.TimeExceeded: case *icmp.TimeExceeded:
logger.Debugf("ignoring ICMP time exceeded message (type: 11, code: %d, return address %s, expected-id %d)", logger.Debugf("ignoring ICMP time exceeded message "+
message.Code, returnAddr, id) "(type: 11, code: %d, return address %s, expected id %d and seq %d)",
message.Code, returnAddr, id, seq)
continue continue
default: default:
return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected-id %d)", return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected id %d and seq %d)",
ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id) ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id, seq)
} }
} }
} }