On this page

Interface declaration:

dns = service("dns",
    interface("main", "udp", 53),
    image = "coredns/coredns:1.11",
)

statsd = service("statsd",
    interface("main", "udp", 8125),
    image = "statsd/statsd:latest",
)

UDP is connectionless and datagram-based. Faultbox speaks the transport directly — there is no higher-level wire format to parse, so fault rules apply uniformly to all datagrams on the interface.

Methods

send(data="", hex="", timeout_ms=5000)

Send one datagram and wait for a response. Response is returned as hex (for binary protocols like DNS).

# StatsD text metric
resp = statsd.main.send(data="api.requests:1|c")

# Binary DNS query
resp = dns.main.send(hex="...", timeout_ms=2000)
# resp.data = {"raw": "...", "size": 64}
ParameterTypeDefaultDescription
datastringUTF-8 string payload (exclusive with hex=)
hexstringHex-encoded binary payload (exclusive with data=)
timeout_msint5000Read timeout for the response

send_no_reply(data="", hex="")

Fire-and-forget. Does not wait for a response — returns immediately after the OS accepts the datagram for send.

statsd.main.send_no_reply(data="api.requests:1|c")

Response Object

For send():

FieldTypeDescription
.data.rawstringResponse payload, hex-encoded
.data.sizeintResponse size in bytes
.okboolTrue if a response was received
.duration_msintRoundtrip time

For send_no_reply():

FieldTypeDescription
.data.sizeintBytes sent
.okboolTrue if the send succeeded locally (does NOT confirm delivery)

Fault Rules

drop(probability=)

Silently discard a fraction of datagrams.

lossy = fault_assumption("lossy",
    target = dns.main,
    rules = [drop(probability="30%")],
)

total_loss = fault_assumption("dns_down",
    target = dns.main,
    rules = [drop()],  # 100% loss
)

delay(delay=, probability=)

Delay datagram forwarding.

slow = fault_assumption("slow_metrics",
    target = statsd.main,
    rules = [delay(delay="2s")],
)

Future: corrupt and reorder

RFC-016 proposes corrupt() (bit-flip) and reorder() (buffer+swap) fault actions unique to UDP. These are NOT yet implemented — they need new Action variants in the proxy engine and corresponding builtins. Tracked as open questions on RFC-016.

Recipes

See recipes/udp.star:

  • packet_loss — probabilistic drops
  • dns_flap — 50% drop for flappy DNS tests
  • metrics_slow — delay for metrics pipelines
  • jitter — fixed delay for congestion simulation
  • blackhole — total loss
load("@faultbox/recipes/udp.star", "udp")

broken_dns = fault_assumption("broken_dns",
    target = dns.main,
    rules  = [udp.dns_flap()],
)

Implementation notes

  • Proxy listens on a random local UDP port and forwards datagrams to the target. Response routing uses per-client upstream sockets so replies reach the original sender.
  • UDP has no connection state. “Drop” and “delay” apply per-datagram.
  • Healthcheck is best-effort: dial the target and return on success. UDP has no handshake, so “port open” detection is OS-dependent.