Skip to content

PXEBoot and TFTP Troubleshooting

This guide covers low-level debugging techniques for TFTP and PXE boot issues in DRP. It walks the boot sequence in order, so work through it from the top when you do not yet know where the boot is failing.

Tip

If you already have a symptom (a stalled transfer, a silent event stream, a packet capture that shows a request but no data) go straight to TFTP Debugging FAQ, which is organized by symptom and covers event watching and packet capture in depth.

Understanding the PXE Boot Process

Before troubleshooting, it helps to understand what happens during a typical successful PXE boot:

  1. Machine sends DHCP discover broadcast
  2. DRP DHCP server responds with IP address and boot filename
  3. Machine requests bootloader via TFTP (port 69/udp)
  4. Bootloader (iPXE/pxelinux) loads and requests configuration
  5. Bootloader fetches kernel and initrd via TFTP or HTTP
  6. Machine boots into the target boot environment (e.g., Sledgehammer for discovery)

DRP Server Diagnostic Commands

Before diving into step-by-step debugging, verify DRP is healthy:

Bash
# Verify DRP is running and listening on expected ports
drpcli info status

# Check if the API is responding
drpcli info check

Enable debug logging for more detail during troubleshooting:

Bash
# All service logging
drpcli prefs set logLevel debug

# Individual service logging
drpcli prefs set debugDhcp debug
drpcli prefs set debugStatic debug

# Remember to set back to the 'warn' default after debugging
drpcli prefs set logLevel warn
drpcli prefs set debugDhcp warn
drpcli prefs set debugStatic warn

Note

The TFTP server logs through the static file service, so debugStatic is the preference that controls TFTP log detail. There is no debugTftp preference. TFTP lines are tagged with service static and principal tftp.

These preferences default to warn, so TFTP transfer errors (which log at info) are not visible until you raise debugStatic.

Stream logs in real-time while attempting a boot:

Bash
drpcli logs watch

Watch file service requests as they arrive, which is the quickest way to tell whether a booting machine is reaching the endpoint at all:

Bash
drpcli events watch "tftp,static.serve.*"

See How do I confirm a machine's TFTP request actually reached DRP? for how to read these events.

Step 1: DHCP Discover

What happens: Machine sends DHCP discover broadcast.

Test DHCP Server is Responding

Bash
# Verify DHCP port is listening
ss -uln | grep 67

# Test DHCP from another Linux machine on the same network (requires root)
sudo dhclient -v -1 <interface>

Capture DHCP Traffic

Bash
tcpdump -i <interface> -n port 67 -vv

You should see DISCOVER, OFFER, REQUEST, and ACK packets.

Common Issues

  • Firewall blocking port 67/udp

    Bash
    iptables -L -n | grep 67
    firewall-cmd --list-ports | grep 67
    

  • Another DHCP server on the network - Check for unexpected OFFER packets in tcpdump output

Step 2: DHCP Response with Boot Filename

What happens: DRP DHCP server responds with IP address and boot filename.

Verify Subnet Configuration

Bash
# Check NextServer and the boot filename options
drpcli subnets show <subnet-name> | jq '{NextServer, Options}'

NextServer is empty when DRP nominates itself, which is correct for most single-homed deployments. A non-empty value that does not resolve to a reachable address on the machine's network stops the boot before any TFTP request is made.

Verify Boot Environment is Available

Bash
# List available boot environments
drpcli bootenvs list | jq '.[].Name'

# Check a specific boot environment exists and is valid
drpcli bootenvs show sledgehammer

Step 3: Bootloader Request via TFTP

What happens: Machine requests bootloader via TFTP (port 69/udp).

Verify TFTP is Enabled and Listening

Bash
drpcli info get | jq '{tftp_enabled, tftp_port}'
ss -uln | grep 69

tftp_enabled: false means the endpoint was started with --disable-tftp or RS_DISABLE_TFTP_SERVER, and nothing will answer on port 69.

Test TFTP End to End

drpcli info status performs a real TFTP fetch of lpxelinux.0 against the endpoint, so Alive: true confirms the full round trip rather than just an open port:

Bash
drpcli info status

Using tftp-hpa

Bash
# Install tftp client if needed
# RHEL/CentOS: yum install tftp
# Ubuntu/Debian: apt-get install tftp-hpa

# Test fetching the bootloader
tftp <drp-server-ip>
tftp> get lpxelinux.0
tftp> quit

# Verify file was retrieved
ls -la lpxelinux.0

Built-in Client

drpcli also has a built-in TFTP client, so no tftp-hpa package is needed. Run this from a host on the same VLAN as the machines that are failing:

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

Confirm the Request Reaches DRP

Bash
drpcli events watch "tftp.serve.*"

Power on the machine. Any tftp.serve event naming the machine's IP proves the request arrived, including an event with "Status": "FAILED", which rules out firewalls, VLAN isolation, and routing as the cause. See How do I confirm a machine's TFTP request actually reached DRP?.

Capture TFTP Traffic

Bash
tcpdump -i any -n -vv 'host <client-ip> and udp'

Warning

Do not filter on port 69 to watch a transfer. TFTP uses port 69 only for the initial request; DRP answers from a freshly allocated ephemeral port, so a port 69 capture shows the request and nothing else (including no error packets). Filter on the client's address instead. See Why does tcpdump port 69 show the request but no file data?.

Common Issues

  • Firewall blocking port 69/udp

    Bash
    iptables -L -n | grep 69
    
    # Open TFTP port if needed
    iptables -A INPUT -p udp --dport 69 -j ACCEPT
    
    # For firewalld
    firewall-cmd --permanent --add-port=69/udp
    firewall-cmd --reload
    

  • Firewall allowing only port 69 - The reply and all data blocks come from an ephemeral port. Stateless rules that permit only port 69 let the request in and drop every reply. Allow established UDP return traffic, or use connection tracking.

  • Network isolation - Client and DRP server on different VLANs without proper routing

Step 4: Bootloader Fetches Configuration

What happens: Bootloader (iPXE/pxelinux) loads and requests its configuration script.

Test Configuration Fetch via TFTP

Bash
drpcli static download default.ipxe to /tmp/default.ipxe --tftp
cat /tmp/default.ipxe

A valid response contains iPXE commands like #!ipxe, kernel, and initrd.

Configuration files are requested in a waterfall, most specific name first, so requests for files that do not exist are expected rather than a fault. See Which files come over TFTP and which come over HTTP? for the request order per bootloader, and lpxelinux.0 error: no such file or directory for why the resulting "no such file or directory" messages are normal.

Check for Boot Environment Errors

Bash
# Verify templates are rendered correctly
drpcli bootenvs show sledgehammer | jq '.Templates'

Common Issues

  • Missing or invalid templates - Check bootenv template syntax
  • Machine not assigned a bootenv - Verify with drpcli machines show <uuid> | jq '.BootEnv'

Step 5: Kernel and Initrd Fetch

What happens: Bootloader fetches kernel and initrd via TFTP or HTTP.

Which protocol is used depends on the bootloader, so establish that before assuming TFTP is involved at all. Legacy BIOS pxelinux and UEFI Secure Boot shim/grub pull the kernel and initrd over TFTP; iPXE and UEFI HTTP Boot pull them over HTTP. See Which files come over TFTP and which come over HTTP? for the full matrix.

Test Boot File Fetch via TFTP

Bash
tftp <drp-server-ip>
tftp> get machines/[machine-uuid]/boot/[sledgehammer-uuid]/vmlinuz0
tftp> get machines/[machine-uuid]/boot/[sledgehammer-uuid]/stage1.img
tftp> quit

# Verify files were retrieved
ls -la vmlinuz0 stage1.img

TFTP is a lock-step protocol with no windowing, so kernels and initrds take minutes to transfer. A transfer that stalls and retries the same block is usually an MTU mismatch (see The transfer starts, then stalls and retries the same block).

Check ISO/File Server Content

Bash
# Check specific bootenv files are uploaded
drpcli isos list

Common Issues

Step 6: Boot into Target Environment

What happens: Machine boots into the target boot environment (e.g., Sledgehammer for discovery).

Verify Machine Discovered Successfully

Bash
# List all machines
drpcli machines list | jq '.[] | {Name, Uuid, Stage, BootEnv}'

Check Machine Reached DRP

Bash
# View machine events/jobs
drpcli machines show <uuid> | jq '.CurrentJob'

References