Skip to content

TFTP Debugging FAQ

Answers to the questions that come up most often when a machine will not network boot and TFTP is suspected.

For a linear walk through the whole PXE boot sequence, start with PXEBoot and TFTP Troubleshooting instead. For the architecture, see How dr-provision Boots Machines.

Two things worth knowing up front

DRP serves TFTP from the same virtual filesystem as its HTTP and HTTPS static file servers, so a file that is reachable over HTTP is reachable over TFTP at the same path.

Every TFTP request DRP receives publishes an event, including requests that fail. That property is what makes How do I confirm a machine's TFTP request actually reached DRP? the fastest way to determine if TFTP is actually reaching DRP.

Confirming Requests Reach DRP

How do I confirm a machine's TFTP request actually reached DRP?

Watch the TFTP event stream, then power on the machine:

Bash
drpcli events watch "tftp.serve.*"

DRP publishes one tftp.serve event per request, keyed on the requested filename:

JSON
{
  "Time": "2026-07-31T14:02:11.412554192Z",
  "Type": "tftp",
  "Action": "serve",
  "Key": "lpxelinux.0",
  "Principal": "tftp",
  "Object": {
    "Start": "2026-07-31T14:02:11.402554192Z",
    "End": "2026-07-31T14:02:11.412554192Z",
    "RequestSize": 0,
    "ResponseSize": 45056,
    "Status": "SUCCESS",
    "Requestor": "10.10.20.76",
    "Url": "lpxelinux.0"
  },
  "Original": null
}

Requestor identifies the client, Status is SUCCESS, FAILED, or CRASHED, and Start/End bracket the transfer so you can spot slow or stalling fetches. RequestSize is always 0 for TFTP reads. See File Transfer Events for every field.

Read FAILED carefully, it is good news twice over

Events are published from a deferred handler, so one fires even when the file does not exist or the transfer aborts. Any tftp.serve event naming your client's IP proves the request reached DRP, which rules out firewalls, VLAN isolation, DHCP next-server misconfiguration, and routing. The problem is then in what DRP served, not in whether it was asked.

It also does not by itself mean a fault. A healthy boot produces a FAILED event for every file it fetches: clients issue a first request only to learn the file's size, abort it once they have the answer, then re-request the file for real. So FAILED only matters in the negative: a filename with a FAILED and no matching SUCCESS was never delivered. See What does a healthy transfer look like? and TFTP Error.

No special role claim is required to watch tftp events. Any authenticated principal will receive them.

Narrowing the stream

Add a fourth component to the specifier to filter on any Object field:

Bash
# Everything one machine asks for
drpcli events watch 'tftp.serve.*.Requestor=Eq(10.10.20.76)'

# Aborted and failed transfers, benign size probes included
drpcli events watch 'tftp.serve.*.Status=Eq(FAILED)'

Filter this way rather than putting the filename in the key field. Specifiers are split on ., and TFTP keys are filenames that usually contain a dot, so tftp.serve.lpxelinux.0 does not match lpxelinux.0 (the trailing 0 is parsed as a filter expression). There is no working way to register an exact key containing a dot, so always register tftp.serve.* and narrow with a filter.

See Filtering Subsystem Events for the filter grammar and the constraints that apply to tftp events.

The event watch is silent. What does that mean?

No tftp.serve events at all means DRP received no TFTP request. Work through these in order:

  1. Confirm TFTP is enabled and on the port you expect.

    Bash
    drpcli info get | jq '{tftp_enabled, tftp_port, prov_enabled, address}'
    

    tftp_enabled: false means the endpoint was started with --disable-tftp or RS_DISABLE_TFTP_SERVER.

  2. Confirm the TFTP server actually responds.

    Bash
    drpcli info status
    

    This is not a port scan. info status performs a real TFTP fetch of lpxelinux.0 against the endpoint, so "TFTP": { "Enabled": true, "Alive": true, "Port": 69 } in the reported service list means the full round trip works from wherever you ran drpcli. Its Enabled combines tftp_enabled and prov_enabled, so a false here can mean the whole provisioner is off rather than just TFTP. Check both fields from step 1.

  3. If info status reports Alive: true but booting machines produce no events, TFTP is healthy and the request is being lost between the machine and DRP. See What capture commands should I run? and capture on the DRP host.

  4. Check that the machine was told to use TFTP at all. UEFI HTTP Boot clients fetch their bootloader over HTTP and never touch TFTP. You will see static.serve events instead. See Can I avoid TFTP entirely?.

A silent watch can also be a bad filter

Check your specifier before concluding nothing arrived. tftp.serve.lpxelinux.0 does not watch for lpxelinux.0. Use tftp.serve.* and narrow it as shown in Narrowing the stream.

DHCP has no event stream

A machine that never receives a next-server and boot filename will never send a TFTP request, and the TFTP event stream cannot tell you that. DHCP publishes no events, so raise its log level instead:

Bash
drpcli prefs set debugDhcp debug
drpcli logs watch
# and afterwards, back to the default
drpcli prefs set debugDhcp warn

If the DHCP logs show no request either, capture both protocols together as shown in What capture commands should I run?. See also DHCP Architecture and External DHCP Architecture.

How do I turn up TFTP logging?

The TFTP server logs through the static file service, so the preference is debugStatic.

Bash
drpcli prefs set debugStatic debug
drpcli logs watch

TFTP lines carry Service static and Principal tftp, so you can isolate them instead of reading the whole stream:

Bash
drpcli events watch 'log.*.static.event.Principal=Eq(tftp)'

The messages worth looking for:

Level Message Meaning
debug TFTP: attempting to send <file> Request received, lookup starting
debug TFTP: <file>: size: <n> File resolved, size known, transfer starting
info TFTP: <file>: transfer error: ... Transfer failed or was aborted by the client
error TFTP: Failed to get remote and local IP address information DRP cannot tell which local address to reply from

Transfer errors are hidden by default

debugStatic defaults to warn, and transfer errors log at info. On an endpoint whose preferences have never been changed you will see none of the messages above except the error line. Set debugStatic to at least info before concluding a transfer did not fail.

Return the preference to warn afterwards, as debug on a busy endpoint is noisy:

Bash
drpcli prefs set debugStatic warn

See Log Events for the event payload and the full list of log services.

Packet Capture

Why does tcpdump port 69 show the request but no file data?

Because TFTP only uses port 69 for the first packet of each transfer.

RFC 1350 has the server answer from a freshly allocated port. For every read or write request it opens a new UDP socket on an ephemeral port, bound to the local address the request arrived on. All option acknowledgements, data blocks, ACKs, and error packets flow between the client's ephemeral port and that new server port.

Two consequences when capturing:

  • port 69 captures only the request. You see the client ask and nothing else, which looks identical to a server that never replied.
  • Data-port traffic is not decoded. tcpdump only applies its TFTP decoder when a port is 69, so everything on the ephemeral port prints as a bare UDP, length N even when you do capture it.

Filter on the client's address instead of the port:

Bash
tcpdump -i any -n -vv 'host 10.10.20.76 and udp'

Warning

This also means a port 69 capture will not show you TFTP ERROR packets. DRP sends file-not-found errors from the transfer's ephemeral port, so a failed lookup is invisible under that filter. Confirm failures with drpcli events watch 'tftp.serve.*.Status=Eq(FAILED)' instead, or capture by host.

What capture commands should I run?

Run these on the DRP endpoint unless noted. All require root.

Watch the DHCP handshake and the TFTP request together:

Bash
tcpdump -i any -n -vv 'udp port 67 or udp port 68 or udp port 69'

Capture one machine's entire boot, including every data block, to a file for later analysis:

Bash
tcpdump -i any -n -s0 -w /tmp/tftp-boot.pcap 'host 10.10.20.76'

-i any matters on a multi-homed endpoint, where you may not know which interface DRP will answer on, and -s0 is required for the data blocks to be reassemblable. Open the capture in Wireshark and decode the data-port conversation as TFTP. tcpdump has no "decode as" option, so on replay those packets still print as UDP, length N.

Confirm from the client side of the network whether TFTP works at all, without waiting for a machine to boot. drpcli has a built-in TFTP client, so there is nothing to install:

Bash
drpcli static download lpxelinux.0 to /tmp/lpxelinux.0 --tftp

Run that from a host on the same VLAN as the machines that are failing, with tcpdump running on the DRP endpoint. If the download succeeds, the network path is fine.

What does a healthy transfer look like?

A BIOS PXE client at 10.10.20.76 fetching the bootloader from DRP at 10.10.20.10:

Text Only
14:02:11.402554 IP 10.10.20.76.2070 > 10.10.20.10.69: TFTP, length 28, RRQ "lpxelinux.0" octet tsize 0
14:02:11.403004 IP 10.10.20.10.45123 > 10.10.20.76.2070: UDP, length 14
14:02:11.403981 IP 10.10.20.76.2070 > 10.10.20.10.45123: UDP, length 17
14:02:11.404881 IP 10.10.20.76.2075 > 10.10.20.10.69: TFTP, length 33, RRQ "lpxelinux.0" octet blksize 1456
14:02:11.405233 IP 10.10.20.10.45124 > 10.10.20.76.2075: UDP, length 15
14:02:11.406102 IP 10.10.20.10.45124 > 10.10.20.76.2075: UDP, length 1460
14:02:11.406540 IP 10.10.20.76.2075 > 10.10.20.10.45124: UDP, length 4

Reading it line by line:

  1. First RRQ with tsize 0: the client is asking how big the file is, not for the file.
  2. DRP answers with an OACK carrying the size, from the new port 45123. Undecoded because the port is not 69.
  3. The client sends an ERROR and drops the transfer. It has what it wanted.
  4. Second RRQ, from a different client port, this time negotiating blksize. This is the real transfer.
  5. OACK confirming the block size, from another new server port 45124.
  6. First data block: 4 bytes of TFTP header plus 1456 bytes of payload.
  7. The client ACKs block 1. Steps 6 and 7 then repeat to the end of the file.

Timestamps, ephemeral port numbers, and packet lengths will differ in your capture. The shape is what matters: two requests per file, replies from ports other than 69, and a steady alternation of data and ACK packets.

Two requests per file is normal

The tsize probe followed by an abort is standard PXE client behaviour, and it is the source of the benign TFTP Aborted messages described in TFTP Error.

What do the common failures look like on the wire?

What you see What it means Where to go
No RRQ at all, and no DHCP traffic either The machine is not attempting network boot, or nothing reaches the endpoint. Check boot order, NIC PXE setting, VLAN, and firewall DHCP has no event stream
DHCP OFFER and ACK present, but no RRQ follows The machine was not given a boot filename, or was pointed at a different next-server DHCP Architecture
RRQ arrives, no reply of any kind Return path blocked. The reply comes from an ephemeral port, so stateless firewall rules that only allow port 69 will drop it Open UDP return traffic, or use conntrack
RRQ arrives, DRP logs a FAILED event and no SUCCESS follows for that filename The file is not there under that name. A FAILED followed by a SUCCESS is the normal size probe Which files come over TFTP and which come over HTTP?
ERROR with ENOTFOUND on pxelinux.cfg/... or grub/... paths Normally just the config-file waterfall. A fault only if no kernel request follows, which means no config was found: check BootEnv on the machine and the defaultBootEnv/unknownBootEnv prefs lpxelinux.0 error: no such file or directory
TFTP Aborted after block 0 of a bootloader The client's tsize probe being abandoned once it has the size. Benign TFTP Error
OACK then the same block number retransmitted repeatedly Block size exceeds the path MTU The transfer starts, then stalls and retries the same block
Replies arrive from an address other than the one the client sent the request to Multi-homed or HA endpoint answering on the wrong address The reply comes from an unexpected source address
Transfer completes but the machine reboots or hangs afterwards TFTP did its job. The problem is in the rendered bootenv template or the kernel command line PXEBoot and TFTP Troubleshooting

Pair every capture with drpcli events watch "tftp.serve.*". The capture tells you what was on the wire, and the event tells you what DRP processed it as.

The transfer starts, then stalls and retries the same block

This is almost always an MTU mismatch, and it is the most common cause of a PXE boot that gets partway through a kernel and then hangs.

TFTP over UDP has no path MTU discovery. When a client requests a blksize option, DRP grants up to the MTU of the interface the request arrived on, minus 32 bytes for IP, UDP, and TFTP headers, with a floor of the 512-byte protocol default. On a standard 1500-byte interface that is up to 1468 bytes per block.

If any hop between DRP and the machine has a smaller MTU (a tunnel, a VPN, a VXLAN overlay, or a jumbo-frame-enabled DRP interface talking to a 1500-byte switch path) those blocks are dropped. DRP retries each block 5 times at 5-second intervals, so a black-holed transfer takes roughly 25 to 30 seconds to give up rather than failing fast. The signature in a host-filtered capture is a data packet, then the same UDP length repeating every 5 seconds with no ACK between.

Bash
# What MTU is DRP's provisioning interface using?
ip link show <interface>

# Does a full-size frame reach the machine's network? (1472 + headers = 1500)
ping -M do -s 1472 -c 3 10.10.20.76

If the ping fails while a smaller size succeeds, you have found the constrained hop. Either raise the MTU along the path or lower it on DRP's provisioning interface so the granted block size fits. Because the grant is derived from the interface MTU at request time, lowering the DRP interface MTU is the fastest way to prove the diagnosis without touching the network.

The reply comes from an unexpected source address

DRP replies from whichever local address the request arrived on, and caches that client-to-local mapping so rendered templates carry a ProvisionerURL the machine can reach. Strict clients reject a reply whose source differs from the address they sent to, which shows up on multi-homed endpoints, on HA pairs behind a virtual IP, and wherever routing is asymmetric.

Bash
ss -ulnp | grep :69
drpcli subnets show <subnet-name> | jq '{NextServer, Options}'

Compare the destination the client used against the source DRP replied from in the capture. An empty NextServer means DRP nominates itself, which is correct for most single-homed deployments. See Preparing to Run DRP for the routing and multi-homing implications.

What Should Be Transferring

Which files come over TFTP and which come over HTTP?

This depends entirely on which bootloader the machine is using, and it is the single most useful thing to know before reading a capture. A UEFI Secure Boot machine pulls its whole kernel and initrd over TFTP, while an iPXE machine pulls almost nothing over it.

Bootloader Bootloader binary Config file Kernel and initrd
pxelinux (legacy BIOS) TFTP TFTP TFTP
grub / shim (UEFI Secure Boot) TFTP TFTP TFTP
iPXE (UEFI PXE) TFTP TFTP HTTP
UEFI HTTP Boot HTTP HTTP HTTP

The bootloader binary is named by DHCP option 67. The defaults come from the bootloaders param (lpxelinux.0 for 386-pcbios, ipxe.efi for amd64-uefi, and ipxe-arm64.efi for arm64-uefi) and a BootEnv's Loaders field overrides them. The sledgehammer BootEnv, for example, sets a shimx64.efi loader for amd64-uefi so that UEFI Secure Boot works, which routes the kernel and initrd over TFTP as a side effect.

Configuration files are requested in a waterfall, most specific first, so seeing requests for files that do not exist is expected:

Bootloader Request order
pxelinux pxelinux.cfg/<hex-ip>, pxelinux.cfg/<mac>, pxelinux.cfg/default
iPXE default.ipxe, <ip>.ipxe, <mac>.ipxe
grub grub/grub.cfg, grub/<ip>.cfg, grub/<mac>.cfg

Kernel and initrd paths for a machine-scoped render look like machines/<machine-uuid>/boot/<sha>/vmlinuz0. To see exactly what DRP will serve a given machine, inspect its BootEnv:

Bash
drpcli machines show <uuid> | jq '{BootEnv, Arch, Address}'
drpcli bootenvs show sledgehammer | jq '{Loaders, OS}'

Slow BIOS or Secure Boot installs are usually just TFTP

TFTP is a lock-step protocol: one block, one ACK, no windowing. Moving a 500 MB initrd that way takes minutes on a good network. If the transfer completes but is slow, that is the protocol, not a fault. Switching the machine to iPXE or UEFI HTTP Boot moves the large files to HTTP, see Can I avoid TFTP entirely?.

Can I avoid TFTP entirely?

Partly, and it is often the right answer when TFTP is blocked by policy or performing badly over a long or lossy path.

  • UEFI HTTP Boot removes TFTP from the boot path completely. Such a machine sends DHCP vendor class HTTPClient (option 60) instead of PXEClient, and DRP rewrites the boot filename into an http:// URL. Requests then arrive as static.serve events. Enable it in the machine's UEFI firmware; DRP needs no configuration change.
  • iPXE keeps TFTP for the bootloader and config script but moves the kernel and initrd to HTTP, which is where nearly all the bytes are. This is the default for amd64-uefi PXE clients.
  • Legacy BIOS PXE and UEFI Secure Boot (shim/grub) cannot avoid TFTP. Either fix the path, or boot from virtual media with the boot-virtual-iso param, which requires the IPMI plugin.

References