fix ICMP ping in Docker by trying raw sockets before unprivileged UDP
The old code only tried unprivileged UDP ICMP (udp4), which relies on the ping_group_range sysctl — not CAP_NET_RAW. Docker doesn't set that sysctl, so pings always timed out and fell through to exec. Now tries raw ICMP sockets (ip4:icmp) first, which properly uses CAP_NET_RAW. Also sets cap_net_raw+ep on the agent binary in the Dockerfile.
This commit is contained in:
parent
cd6fb4e79e
commit
f35be66148
2 changed files with 49 additions and 21 deletions
|
|
@ -10,9 +10,11 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION}" -o towerops-agent .
|
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION}" -o towerops-agent .
|
||||||
|
|
||||||
FROM alpine:3.23
|
FROM alpine:3.23
|
||||||
RUN apk add --no-cache ca-certificates iputils libcap && \
|
RUN apk add --no-cache ca-certificates iputils libcap
|
||||||
setcap cap_net_raw+p /bin/ping
|
|
||||||
COPY --from=builder /app/towerops-agent /usr/local/bin/towerops-agent
|
COPY --from=builder /app/towerops-agent /usr/local/bin/towerops-agent
|
||||||
RUN adduser -D -u 1000 towerops && chown towerops /usr/local/bin/towerops-agent
|
RUN adduser -D -u 1000 towerops && \
|
||||||
|
chown towerops /usr/local/bin/towerops-agent && \
|
||||||
|
setcap cap_net_raw+ep /usr/local/bin/towerops-agent && \
|
||||||
|
setcap cap_net_raw+p /bin/ping
|
||||||
USER towerops
|
USER towerops
|
||||||
CMD ["towerops-agent"]
|
CMD ["towerops-agent"]
|
||||||
|
|
|
||||||
62
ping.go
62
ping.go
|
|
@ -16,7 +16,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// icmpPing sends a single ICMP echo request and returns the round-trip time in milliseconds.
|
// icmpPing sends a single ICMP echo request and returns the round-trip time in milliseconds.
|
||||||
// Uses unprivileged UDP sockets where available, falling back to raw sockets.
|
// Tries raw ICMP sockets first (requires CAP_NET_RAW or root), then falls back to
|
||||||
|
// unprivileged UDP-based ICMP (requires ping_group_range sysctl).
|
||||||
func icmpPing(ip string, timeoutMs int) (float64, error) {
|
func icmpPing(ip string, timeoutMs int) (float64, error) {
|
||||||
parsedIP := net.ParseIP(ip)
|
parsedIP := net.ParseIP(ip)
|
||||||
if parsedIP == nil {
|
if parsedIP == nil {
|
||||||
|
|
@ -24,26 +25,50 @@ func icmpPing(ip string, timeoutMs int) (float64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
isIPv4 := parsedIP.To4() != nil
|
isIPv4 := parsedIP.To4() != nil
|
||||||
var network string
|
|
||||||
var dst net.Addr
|
|
||||||
var msgType icmp.Type
|
|
||||||
|
|
||||||
|
// Try raw ICMP first (works with CAP_NET_RAW or as root)
|
||||||
|
var rawNet string
|
||||||
if isIPv4 {
|
if isIPv4 {
|
||||||
network = "udp4"
|
rawNet = "ip4:icmp"
|
||||||
dst = &net.UDPAddr{IP: parsedIP}
|
|
||||||
msgType = ipv4.ICMPTypeEcho
|
|
||||||
} else {
|
} else {
|
||||||
network = "udp6"
|
rawNet = "ip6:ipv6-icmp"
|
||||||
dst = &net.UDPAddr{IP: parsedIP}
|
}
|
||||||
msgType = ipv6.ICMPTypeEchoRequest
|
ms, err := doICMPPing(parsedIP, rawNet, isIPv4, timeoutMs)
|
||||||
|
if err == nil {
|
||||||
|
return ms, nil
|
||||||
|
}
|
||||||
|
if _, ok := err.(*errICMPUnavailable); !ok {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to unprivileged UDP ICMP (works with ping_group_range sysctl)
|
||||||
|
var udpNet string
|
||||||
|
if isIPv4 {
|
||||||
|
udpNet = "udp4"
|
||||||
|
} else {
|
||||||
|
udpNet = "udp6"
|
||||||
|
}
|
||||||
|
return doICMPPing(parsedIP, udpNet, isIPv4, timeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doICMPPing performs an ICMP ping over the given network type.
|
||||||
|
func doICMPPing(ip net.IP, network string, isIPv4 bool, timeoutMs int) (float64, error) {
|
||||||
conn, err := icmp.ListenPacket(network, "")
|
conn, err := icmp.ListenPacket(network, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, &errICMPUnavailable{err: fmt.Errorf("icmp listen: %w", err)}
|
return 0, &errICMPUnavailable{err: fmt.Errorf("icmp listen %s: %w", network, err)}
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
var msgType icmp.Type
|
||||||
|
var proto int
|
||||||
|
if isIPv4 {
|
||||||
|
msgType = ipv4.ICMPTypeEcho
|
||||||
|
proto = 1
|
||||||
|
} else {
|
||||||
|
msgType = ipv6.ICMPTypeEchoRequest
|
||||||
|
proto = 58
|
||||||
|
}
|
||||||
|
|
||||||
id := os.Getpid() & 0xffff
|
id := os.Getpid() & 0xffff
|
||||||
msg := icmp.Message{
|
msg := icmp.Message{
|
||||||
Type: msgType,
|
Type: msgType,
|
||||||
|
|
@ -55,18 +80,19 @@ func icmpPing(ip string, timeoutMs int) (float64, error) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var proto int
|
|
||||||
if isIPv4 {
|
|
||||||
proto = 1 // ICMP
|
|
||||||
} else {
|
|
||||||
proto = 58 // ICMPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
wb, err := msg.Marshal(nil)
|
wb, err := msg.Marshal(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("icmp marshal: %w", err)
|
return 0, fmt.Errorf("icmp marshal: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destination address type depends on network
|
||||||
|
var dst net.Addr
|
||||||
|
if strings.HasPrefix(network, "udp") {
|
||||||
|
dst = &net.UDPAddr{IP: ip}
|
||||||
|
} else {
|
||||||
|
dst = &net.IPAddr{IP: ip}
|
||||||
|
}
|
||||||
|
|
||||||
deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond)
|
deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond)
|
||||||
if err := conn.SetDeadline(deadline); err != nil {
|
if err := conn.SetDeadline(deadline); err != nil {
|
||||||
return 0, fmt.Errorf("set deadline: %w", err)
|
return 0, fmt.Errorf("set deadline: %w", err)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue