TFTP Honeypot: Design and Preliminary Results
Because of the simplicity of TFTP, and the difficulty I had using existing TFTP servers, I decided to write a honey pot TFTP server. This seemed like an easy, no consequences, way to try out my honey pot design principles.
My personal experience with TFTP made me believe it’s one of those “internal” protocols, typically used by bigger organizations in their computer rooms or data centers to PXE-boot rack mounted servers automatically. Only a madman would expose a TFTP server to The Internet.
Structure of the honey pot process
I wrote my honey pot TFTP server in the Go programming language. All buffers and channels are allocated before the process starts listening, and no new buffers, channels or goroutines are allocated during execution.
The main goroutine reads incoming TFTP requests in a loop, and then distributes them to a number of worker goroutines via a Go channel. After processing a TFTP request (or other UDP packet data), the workers send ready-to-reuse buffers back to the main goroutine via another channel. This design assumes that decoding and logging TFTP requests is the performance bottleneck.
The process initially reads command line arguments, then does setup, which consists of:
- opening a UDP socket on which to listen,
- creating two channels
- creating a number of packet buffers
- starting the same number of “worker” goroutines
The code puts the newly-created packet buffers on one of the two channels. Both channels have capacity of the number of worker goroutines, and the same number of packet buffers are created. This first channel-full-of-buffers is a FIFO queue “free list”.
The main goroutine reads one of the packet buffers from the first channel, and uses it to read data from the UDP socket. Once the main goroutine reads some data, it puts the packet buffer on the second channel. The second channel constitutes a FIFO queue of work to be done.
The worker goroutines all wait on the second channel. They read a packet buffer from the second channel, and do the work associated with the packet buffer. They put the packet buffer on the first channel, and return to waiting for a new packet buffer to arrive on the second channel
Any rate limiting gets done by the Linux kernel if the server doesn’t handle incoming UDP packets fast enough.
Design considerations and goals
Here’s how I implemented my design considerations and goals.
Minimize resource consumption, avoid resource exhaustion
My server process starts a fixed number of “worker” threads,
each of which handles a single incoming packet at a time.
The incoming packets are kept in a fixed number of pre-allocated
buffers.
This did complicate the coding in that it mandates
a struct to hold the packet,
and the flow-of-control has to keep track of the structs,
not just abandon them to the Go runtime garbage collector.
I chose not to spend network or storage resources by receiving files, or sending DATA packets in response to RRQ (read) requests, but rather respond to both RRQ and WRQ (write) requests with an ERROR packet. This has the simplifying consequence of not requiring any context to correctly deal with any newly arrived packet. Each new packet only requires a little bit of decoding, some logging, and possibly sending back an ERROR packet.
Mimicking an existing server
I tried to make this honey pot server act like the atftpd TFTP server, because I have the impression that it’s widely used.
Does my honey pot server fool anyone?
nmap has a TFTP server identification module
which is unreliable.
I ran this nmap command to verify the false identification:
sudo nmap -sU -p 69 --script tftp-version --script-args tftp-version.socket 127.0.0.1
It only identifies any TFTP server I ran once every 3 or 4 attempts.
I think it’s got a timing problem.
I had to take out all the timeouts I had that would make
attackers wait for a response to get nmap to identify
my server as atftpd.
Logging
TFTP only specifies 6 different packets, one of them (ACK) only 4 bytes in size. The spec only supplies a small amount of structure to the order of packets, so I wrote my honey pot server to handle all incoming packets in any order. I included logging incorrectly formatted packets, and logging any extra bytes that might be sent after or in addition to the specified bytes. I reasoned that it was possible for a TFTP server written in C to have buffer overflows when a larger-than-specified packet arrives, so it was worth keeping track of “extra” data.
It also seemed worthwhile to log the packets arriving on UDP port 69 that don’t fit any TFTP packet format. If entities are probing for TFTP servers with vulnerabilities that can be triggered by a malformed packet, it stands to reason my honey pot should log packets that don’t fit the TFTP specification.
Avoiding collateral damage
I didn’t seek to explicitly minimize collateral damage. TFTP, like DNS, could be used to raise a packet storm: putting a false “source” IP address in a TFTP payload’s IP header would cause my server to send an unsolicited ERROR packet to whatever address an attacker desired. Fortunately, a TFTP RRQ or WRQ packet is about the same size as the resulting ERROR response, so there’s no large amplification of small packets.
Considerations unmet or ignored
I did not try to maximize an attacker’s resource consumption. I judged that mimicking an existing TFTP server would lead to more interesting packets from more attackers.
I did not explicitly try to make my honey pot attractive to attackers. TFTP-the-protocol doesn’t have much latitude for making one server more attractive than others. It has no access controls at all, and no discovery, no way to find out what files a given server might have on offer. I was interested in discovering if anyone at all scans for TFTP servers, so it did not seem worthwhile to dress things up.
Early Results
I’ve been running my TFTP honey pot on two servers, one a CenturyLink Fiber IPv4 address since 2026-05-30, the other a VPS, since 2026-05-28. Both servers received TFTP probes within a few hours of me starting them. I think this means that entities are scanning the internet for TFTP servers constantly.
My VPS receives between 20 and 30 TFTP packets a day. Apparently, there’s not a whole lot of TFTP probing or exploitation attempts.
Because of TFTP’s lack of authentication, or really any other identifying features, I was reduced to trying DNS lookups on the IP addresses doing the TFTP probes to identify sources. Most of the activity is from Shadow Server, Censys and Shodan. All three of these organizations regularly scan for TFTP servers. Shadow Server makes their bulk results available. Censys lets you look up specific IP addresses.
Shadow Server scans
Shadow Server seems to run two different kinds of scans,
one probing for TFTP servers via an RRQ (read request) packet for a file named a.pdf,
the second a series of ERROR packets sent 11 seconds apart.
2026/06/16 07:25:02 65.49.1.64:47384, 14 bytes, RRQ, file name "a.pdf", "octet", 0 options
2026/06/16 09:08:52 65.49.1.57:70, 17 bytes, ERROR code 4, "Bad Filename"
2026/06/16 09:09:03 65.49.1.57:71, 21 bytes, ERROR code 0, "Access violation"
2026/06/16 09:09:14 65.49.1.57:72, 7 bytes, ERROR code 4, "4", 1 extra bytes "\x05\x00\x044\x00\x00"
2026/06/16 09:09:25 65.49.1.57:73, 16 bytes, ERROR code 5, "Illegal TID"
2026/06/16 09:09:36 65.49.1.57:74, 26 bytes, ERROR code 4, "Illegal TFTP operation"
2026/06/16 09:09:47 65.49.1.57:75, 23 bytes, unknown opcode 0, 000000036e616d65696e76616c69642072657175657374
The RRQ for a.pdf arrives between 20 and 50 minutes before the series of ERROR packets.
The ERROR packets always arrive from the same IPv4 address
and use UDP ports 70 through 75 sequentially.
My guess is that the RRQ is a discovery packet, to see if UDP port 69 at an IP address has something listening, and the series of ERROR packets is some kind of server software identification. One of the ERROR packets is malformed, having an extra ASCII-Nul byte. One of the packets isn’t really an ERROR packet, but a packet not fitting the TFTP specification at all. It contains some ASCII format strings.
00 00 00 03 6e 61 6d 65 69 6e 76 61 6c 69 64 20 72 65 71 75 65 73 74
n a m e i n v a l i d sp r e q u e s t
The first 4 bytes suggest a TFTP packet with a mistaken zero-value packet type field, and error or block no. 3, with the string “nameinvalid request”.
Shodan scans
Looks like Shodan, the search engine for the internet of everything, sends 16-byte nonsense packets to UDP port 69 every once in a while.
00 00 04 17 27 10 19 80 00 00 00 00 00 03 49 25
Shodan packets usually come in pairs about 20 seconds apart, but can arrive in bursts of more, or as singles. One weird fact about Shodan port 69 probes: the UDP packets come from port 18020 more than half of the time.
I’m at a loss to see what Shodan gets from this scan. Maybe the format of the inevitable TFTP ERROR packets lets them identify server software. Maybe otherwise silent TFTP servers respond to grotesquely malformed packets like this.
Censys scans
Censys apparently sends an RRQ
packet for a file named /a, with type netascii.
Very occasionally, a follow up RRQ for a file named file, type octet
arrives about one minute later from the IP address
that sent the RRQ for /a.
Only about half of the IP addresses sending an RRQ
for /a are registered to Censys itself.
The other half are Google Cloud addresses.
It’s possible some other entity is also sending RRQ packets with /a filename.