Skip to content

RTFM · Load balancing

External healthchecks: the Saphira check library

Two packages, 34 checks, one interface: small scripts that perform the real transaction a service promises, a DICOM echo, a DNS query, a RADIUS Status-Server, and answer with an exit code any load balancer can read.

Saphira Linux dragon mascot

The idea in plain words

A normal health check is like checking whether a shop's door is unlocked. The Saphira checks walk in, buy something, and check the change: each one performs the exact transaction the service promises its customers. If that transaction works, the exit code is 0 and the balancer sends real traffic. If anything in the chain fails, the listener, the protocol, the database behind it, the check says so.

The package rule that makes this trustworthy: a named check is a named capability, with no silent fallbacks. https never degrades to "port open"; http3 never quietly becomes quic; dicom.echo never passes because a port answered. A pass verdict certifies exactly the promised transaction.

Install

Packages and paths
# Generic checks - pulls the full Saphira LB stack with it
# (haproxy, ipvsadm/LVS, ldirectord via resource-agents)
apk add saphira-lb-healthchecks

# Medical checks - depends on the generic package and dcmtk
apk add saphira-lb-medical-healthchecks

# Where things land
ls /var/lib/lb/saphira/                       # the checks themselves
ls /usr/share/saphira/lb-healthchecks/lib/    # shared shell library
ls /usr/share/saphira/lb-medical-healthchecks/lib/
saphira-lb-probe --help                       # the probe engine
/usr/share/doc/saphira-lb-healthchecks/README.md
What each package carries
PathContents
/var/lib/lb/saphira/lb.saphira.*The executable checks (both packages install here)
/usr/bin/saphira-lb-probeThe raw/protocol probe engine behind many checks
/usr/share/saphira/lb-healthchecks/lib/lb-saphira-common.shShared ABI, config loader, timeout handling
/usr/share/saphira/lb-medical-healthchecks/lib/lb-saphira-dicom-synth.pySynthetic DICOM object generator (no sample files)
/etc/saphira/lb-healthchecks.d/Your configuration layer (you create it)

The caller contract: $1–$5

ldirectord, HAProxy's external-check and the ipvsadm toolchain all share one habit: they pass a fixed set of arguments to an external check and read its exit status. The Saphira checks accept that ABI as gospel; nothing beyond these five is ever assumed from a caller:

The five standard arguments and exit codes
$1  virtual service / firewall mark     (e.g. 21, or the VIP)
$2  virtual port
$3  real server address                 ← the check target
$4  real server port
$5  virtual source where supplied       (optional)

# exit 0      healthy
# exit 1      unhealthy, the named capability failed
# exit 2      misconfiguration, fix the setup, do not flap the server

Everything beyond $5 is check-specific configuration, resolved in a fixed order so the same script works interactively, under ldirectord, and under HAProxy:

  1. 1. Positional $6 and beyond

    Manual use and adapter wrappers, where the caller permits richer arguments; a convenience, never required.

  2. 2. Environment variables

    LB_SAPHIRA_*; the highest-priority machine configuration, and the only place credentials may arrive.

  3. 3. Check/service config file

    /etc/saphira/lb-healthchecks.d/<check>.conf for all services, or <check>/<service>.conf for one virtual service (the service token is $1 sanitized).

  4. 4. Safe baked-in default

    What the check author chose when you have said nothing.

The configuration layer
# /etc/saphira/lb-healthchecks.d/dicom.find.conf
LB_SAPHIRA_DICOM_PATIENT_ID=SAPHIRA-SYNTHETIC-HEALTHCHECK
LB_SAPHIRA_DICOM_MODALITY=CT
LB_SAPHIRA_DICOM_QUERY_LEVEL=PATIENT
LB_SAPHIRA_DICOM_AET=SAPHIRA-HC
LB_SAPHIRA_DICOM_AEC=MODALITY-SCP

# Per-service override (service token = sanitized $1):
#   /etc/saphira/lb-healthchecks.d/dicom.find/21.conf
# Format: KEY=VALUE lines, keys must match LB_SAPHIRA_[A-Z0-9_]+,
# '#' comments. Caller environment always wins over conf files.

Credentials arrive via environment or admin-owned conf files only and are never echoed by any check. Exit 2 is deliberate: a misconfigured check refuses to vote, so a broken setup can never silently mark healthy servers down (or keep dead ones up).

Timeouts and dual-stack

Every check enforces a bounded timeout via LB_SAPHIRA_TIMEOUT (default 3 seconds); a check that hangs would otherwise hang the balancer's scheduler. All addresses resolve through getaddrinfo, so any check whose protocol permits IPv6 handles a v4 or v6 real server indifferently; raw-packet checks (gre, ipproto) are IPv4 in v1 and say so on their pages.

The check catalogue

Generic checks (saphira-lb-healthchecks)
CheckLayerProves
icmpL3ICMP echo round-trip
tcpL4TCP accept
udpL4UDP listener bound (ICMP-unreachable discrimination)
multiportL4all/any of a port list accepts TCP
httpL7HTTP status/body transaction
httpsL7HTTPS status/body transaction
tlsL7Full TLS handshake (+ optional cert-expiry floor)
sniL7TLS handshake asserting a specific SNI hostname
http3L7Strict real HTTP/3 transaction (no fallback)
quicL7QUIC listener via Version Negotiation handshake
dnsL7DNS query round-trip (ID/QR/rcode verified)
smtpL7SMTP greeting + EHLO + QUIT (stops before DATA)
imapL7IMAP greeting/CAPABILITY, no AUTH
pop3L7POP3 greeting, no AUTH
ldapL7LDAP anonymous bind + rootDSE response
sshL7SSH-2.0 banner handshake (credential-free)
radiusL7RADIUS Status-Server (RFC 5997) round-trip
sipL7SIP OPTIONS round-trip (UDP or TCP)
mysqlL7MySQL protocol handshake parse
postgresqlL7PostgreSQL SSLRequest negotiation
redisL7Redis PING → PONG (optional AUTH)
greL3GRE encapsulated-packet loopback (two-party contract)
ipprotoL3Raw IP protocol primitive (probe/accept modes)
sendexpect-tcpL4/L7Raw TCP send-expect: the custom-protocol escape hatch
sendexpect-udpL4/L7Raw UDP send-expect
Medical checks (saphira-lb-medical-healthchecks)
CheckClassProves
dicom-echocontinuous-safeC-ECHO association + verification SCP
dicom-findcontinuous-safeC-FIND cycle with synthetic non-patient keys
dicom-moveopt-inC-MOVE retrieve pipeline to a dedicated receiving SCP
dicom-storeopt-inC-STORE of a generated synthetic object
dicom-qidocontinuous-safeQIDO-RS query → 200 + application/dicom+json
dicom-wadocontinuous-safeWADO-RS retrieve → 200 + application/dicom
dicom-stowopt-inSTOW-RS store of a synthetic object
hl7-mllpcontinuous-safeMLLP QRY^A19 (synthetic, read-only) + valid ACK
fhircontinuous-safeFHIR CapabilityStatement served

Wiring: the same check under both directors

ldirectord
# ldirectord.cf
virtual = 21
    protocol = fwm
    scheduler = rr
    real = 192.168.20.21:0 gate 1
    checktype = external
    checkcommand = "/var/lib/lb/saphira/lb.saphira.dicom.echo"
HAProxy
# haproxy.cfg - external-check runs the same script with the
# same five arguments (drain-support requires HAProxy 2.2+ semantics)
global
    external-check

backend dicom_pool
    mode tcp
    option external-check
    external-check command "/var/lib/lb/saphira/lb.saphira.dicom.echo"
    server pacs1 192.168.20.21:104 check inter 5s fall 3 rise 2

Check-specific settings ride in the environment or the conf-file layer, not in the balancer config, which is exactly why one check serves both directors unchanged.

Run any check by hand first

Manual runs cost nothing and prove everything
# The five arguments: FWM/VIP vport real rport [vsource]
/var/lib/lb/saphira/lb.saphira.tcp 21 443 192.168.20.11 443
echo $?                          # 0 healthy, 1 unhealthy, 2 misconfig

# With settings, exactly as the balancer would see them:
LB_SAPHIRA_HTTP_PATH=/healthz \
  /var/lib/lb/saphira/lb.saphira.http web-pool 80 192.168.20.11 80
echo $?

Prove it works: Your first check passes

You picked a check from the catalogue, ran it by hand against a real server, saw exit 0, wired it into one director, and watched ipvsadm or the HAProxy stats page mark the server up; then down again when you stopped the service.

Did we miss something?

If this page left something unanswered, found an error, or there is another subject you would like documented, tell us. Saphira’s documentation grows from real problems people need to solve.

Send feedback or request a new section →

Prefer not to do it yourself?

Everything needed to do the work yourself is documented here and remains free; we charge for human time, not for withholding knowledge. Sometimes the missing resource is simply time. The same people who build Saphira can provide paid professional help with implementation, migration, troubleshooting and administration.

Ask about professional support →