Skip to main content
  1. Linux Articles/
  2. Ubuntu 26.04 LTS Server/

Setting Up a TFTP Server on Ubuntu 26.04 LTS Server (tftpd-hpa)

Table of Contents

Before using a TFTP server

TFTP has neither authentication nor encryption. Anyone can read and write files, so use it only inside a closed network.

It is used where a device speaks nothing but TFTP, such as saving router configurations or transferring OS images. This article uses tftpd-hpa as the server and tftp-hpa as the client.

File to editPurpose
/etc/default/tftpd-hpaThe served directory, listening address and options
/etc/hosts.denyRestricting sources (TCP Wrappers)

The essentials of TFTP (RFC 1350):

ItemDetails
TransportUDP. The first request goes to port 69 on the server; the transfer itself comes from a new port chosen by the server
PacketsRRQ (read request) / WRQ (write request) / DATA / ACK / ERROR
Transfer modesnetascii (text, line ends sent as CR LF) / octet (bytes sent as they are)

Checking what is installed

Neither the server nor the client is installed, and neither the configuration file nor the served directory exists yet.

On the server side (sv1) - example
kazulog@sv1:~$ apt-cache policy tftpd-hpa | head -3
tftpd-hpa:
  Installed: (none)
  Candidate: 5.3+20251209-2
kazulog@sv1:~$ ls -l /etc/default/tftpd-hpa /srv/tftp 2>&1
ls: cannot access '/etc/default/tftpd-hpa': No such file or directory
ls: cannot access '/srv/tftp': No such file or directory
kazulog@sv1:~$ sudo ss -ulnp 'sport = :69'; echo "ss rc=$?"
State                  Recv-Q                  Send-Q                                   Local Address:Port                                   Peer Address:Port                 Process
ss rc=0
On the client side (sv2) - example
kazulog@sv2:~$ which tftp; echo "which rc=$?"
which rc=1
kazulog@sv2:~$ apt-cache policy tftp-hpa | head -3
tftp-hpa:
  Installed: (none)
  Candidate: 5.3+20251209-2

Installing the TFTP server

Installation command
sudo apt install -y tftpd-hpa

No super-server such as inetd is involved: tftpd-hpa.service listens on UDP port 69 by itself. How a super-server works is covered in Configuring inetd.

Installing creates /etc/default/tftpd-hpa, whose configuration is just these four lines.

Installation and the configuration file - example
kazulog@sv1:~$ sudo apt install -y tftpd-hpa | cat
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
Solving dependencies...
Installing:
  tftpd-hpa

Suggested packages:
  pxelinux

Summary:
  Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 96
  Download size: 44.5 kB
  Space needed: 128 kB / 63.1 GB available

Get:1 http://archive.ubuntu.com/ubuntu resolute/main amd64 tftpd-hpa amd64 5.3+20251209-2 [44.5 kB]
Preconfiguring packages ...
Fetched 44.5 kB in 1s (40.5 kB/s)
Selecting previously unselected package tftpd-hpa.
(Reading database ... 91400 files and directories currently installed.)
Preparing to unpack .../tftpd-hpa_5.3+20251209-2_amd64.deb ...
Unpacking tftpd-hpa (5.3+20251209-2) ...
Setting up tftpd-hpa (5.3+20251209-2) ...
Created symlink '/etc/systemd/system/multi-user.target.wants/tftpd-hpa.service''/usr/lib/systemd/system/tftpd-hpa.service'.
Processing triggers for man-db (2.13.1-1build1) ...
Scanning processes...
Scanning linux images...

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.
kazulog@sv1:~$ cat /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure"
kazulog@sv1:~$ ls -ld /srv/tftp
drwxr-xr-x 2 root nogroup 4096 Sep 12 10:01 /srv/tftp
kazulog@sv1:~$ systemctl status tftpd-hpa --no-pager | head -7 | cat
● tftpd-hpa.service - tftpd-hpa TFTP Server
     Loaded: loaded (/usr/lib/systemd/system/tftpd-hpa.service; enabled; preset: enabled)
     Active: active (running) since Sat 2026-09-12 10:01:24 JST; 6s ago
 Invocation: 4d07df8db779481dbcc25f5fda912011
       Docs: man:in.tftpd
    Process: 2706 ExecStart=/usr/sbin/in.tftpd --listen --user $TFTP_USERNAME --address $TFTP_ADDRESS $TFTP_OPTIONS $TFTP_DIRECTORY (code=exited, status=0/SUCCESS)
   Main PID: 2707 (in.tftpd)
kazulog@sv1:~$ sudo ss -ulnp 'sport = :69'
State               Recv-Q              Send-Q                           Local Address:Port                             Peer Address:Port              Process
UNCONN              0                   0                                      0.0.0.0:69                                    0.0.0.0:*                  users:(("in.tftpd",pid=2707,fd=4))              
UNCONN              0                   0                                         [::]:69                                       [::]:*                  users:(("in.tftpd",pid=2707,fd=5))              
VariableDefaultMeaning
TFTP_USERNAMEtftpThe user in.tftpd runs as
TFTP_DIRECTORY/srv/tftpThe directory to serve
TFTP_ADDRESS:69Address and port to listen on. Without an address, all addresses
TFTP_OPTIONS--secureExtra options

--secure changes the root directory to TFTP_DIRECTORY at startup (chroot), so clients give just the file name without a directory. /srv/tftp is owned by root, so uploads do not work with the defaults.

After changing the settings, apply them with sudo systemctl restart tftpd-hpa.

Downloading a file

Put a file in /srv/tftp on the server.

Putting a file in place - example
kazulog@sv1:~$ echo "hello from tftp server sv1" | sudo tee /srv/tftp/hello.txt
hello from tftp server sv1
kazulog@sv1:~$ sudo ls -l /srv/tftp/
total 4
-rw-r--r-- 1 root root 27 Sep 12 10:01 hello.txt

Install the client and fetch it. -c runs a single command and exits; -v shows the details of the transfer.

Installing the client and downloading - example
kazulog@sv2:~$ sudo apt install -y tftp-hpa | cat
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
Solving dependencies...
Installing:
  tftp-hpa

Summary:
  Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 96
  Download size: 19.8 kB
  Space needed: 55.3 kB / 63.1 GB available

Get:1 http://archive.ubuntu.com/ubuntu resolute/main amd64 tftp-hpa amd64 5.3+20251209-2 [19.8 kB]
Fetched 19.8 kB in 1s (24.6 kB/s)
Selecting previously unselected package tftp-hpa.
(Reading database ... 91448 files and directories currently installed.)
Preparing to unpack .../tftp-hpa_5.3+20251209-2_amd64.deb ...
Unpacking tftp-hpa (5.3+20251209-2) ...
Setting up tftp-hpa (5.3+20251209-2) ...
Processing triggers for man-db (2.13.1-1build1) ...
Scanning processes...
Scanning linux images...

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.
kazulog@sv2:~$ cd /tmp && tftp -v 192.168.100.10 -c get hello.txt
Connected to 192.168.100.10 (192.168.100.10), port 69
getting from 192.168.100.10:hello.txt to hello.txt [netascii]
Received 28 bytes in 0.1 seconds [2524 bit/s]
kazulog@sv2:/tmp$ cat /tmp/hello.txt
hello from tftp server sv1

The default transfer mode of the tftp-hpa client is netascii. The 27-byte file arrives as 28 bytes because the line end (LF) is sent as CR LF. The receiver turns it back into LF, so the saved file matches the original.

Allowing uploads (/etc/default/tftpd-hpa)

With the defaults, only overwriting a file that already exists on the server is allowed.

Uploading with the defaults - example
kazulog@sv2:~$ echo "uploaded from sv2" > /tmp/upload.txt
kazulog@sv2:~$ cd /tmp && tftp -v 192.168.100.10 -c put upload.txt
Connected to 192.168.100.10 (192.168.100.10), port 69
putting upload.txt to 192.168.100.10:upload.txt [netascii]
Error code 1: File not found

Edit /etc/default/tftpd-hpa and add --create to TFTP_OPTIONS.

File to edit
sudo vi /etc/default/tftpd-hpa
/etc/default/tftpd-hpa (after the change)
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure --create"
After the change - example
kazulog@sv1:~$ cat /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure --create"
kazulog@sv1:~$ sudo systemctl restart tftpd-hpa
kazulog@sv1:~$ ls -ld /srv/tftp
drwxr-xr-x 2 root nogroup 4096 Sep 12 10:01 /srv/tftp

That alone still fails, because the served directory is still owned by root.

Uploading with only --create - example
kazulog@sv2:~$ echo "uploaded from sv2" > /tmp/upload.txt
kazulog@sv2:~$ cd /tmp && tftp -v 192.168.100.10 -c put upload.txt
Connected to 192.168.100.10 (192.168.100.10), port 69
putting upload.txt to 192.168.100.10:upload.txt [netascii]
Error code 0: Request failed

Change the directory’s owner to tftp (the user in TFTP_USERNAME) and the upload succeeds.

Changing the owner - example
kazulog@sv1:~$ sudo chown tftp:tftp /srv/tftp
kazulog@sv1:~$ ls -ld /srv/tftp
drwxr-xr-x 2 tftp tftp 4096 Sep 12 10:01 /srv/tftp
Upload - example
kazulog@sv2:~$ echo "uploaded from sv2" > /tmp/upload.txt
kazulog@sv2:~$ cd /tmp && tftp -v 192.168.100.10 -c put upload.txt
Connected to 192.168.100.10 (192.168.100.10), port 69
putting upload.txt to 192.168.100.10:upload.txt [netascii]
Sent 19 bytes in 0.1 seconds [1441 bit/s]

Restricting the listening address (/etc/default/tftpd-hpa)

Put an address in TFTP_ADDRESS and the server listens only on that address. Enclose an IPv6 address in square brackets, as in [2001:db8:100::10]:69.

File to edit
sudo vi /etc/default/tftpd-hpa
/etc/default/tftpd-hpa (after the change)
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="192.168.100.10:69"
TFTP_OPTIONS="--secure --create"

Uploaded files are created readable and writable by anyone (666), and only one listener remains.

After the change - example
kazulog@sv1:~$ sudo ls -l /srv/tftp/
total 8
-rw-r--r-- 1 root root 27 Sep 12 10:01 hello.txt
-rw-rw-rw- 1 tftp tftp 18 Sep 12 10:02 upload.txt
kazulog@sv1:~$ cat /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="192.168.100.10:69"

Restricting sources (/etc/hosts.deny)

in.tftpd has TCP Wrappers (libwrap) built in, so /etc/hosts.deny can refuse a source. The daemon name is in.tftpd.

File to edit
sudo vi /etc/hosts.deny
Appended to /etc/hosts.deny
# TFTP は 192.168.100.20 からの接続を拒否する
in.tftpd: 192.168.100.20
After the change - example
kazulog@sv1:~$ ldd /usr/sbin/in.tftpd | grep libwrap
	libwrap.so.0 => /usr/lib/x86_64-linux-gnu/libwrap.so.0 (0x00007cfa15a18000)
kazulog@sv1:~$ cat /etc/hosts.deny
# /etc/hosts.deny: list of hosts that are _not_ allowed to access the system.
#                  See the manual pages hosts_access(5) and hosts_options(5).
#
# Example:    ALL: some.host.name, .some.domain
#             ALL EXCEPT in.fingerd: other.host.name, .other.domain
#
# If you're going to protect the portmapper use the name "rpcbind" for the
# daemon name. See rpcbind(8) and rpc.mountd(8) for further information.
#
# The PARANOID wildcard matches any host whose name does not match its
# address.
#
# You may wish to enable this to ensure any programs that don't
# validate looked up hostnames still leave understandable logs. In past
# versions of Debian this has been the default.
# ALL: PARANOID
# TFTP は 192.168.100.20 からの接続を拒否する
in.tftpd: 192.168.100.20

A refused request gets no ERROR; it is silently dropped. The client keeps resending and finally gives up with Transfer timed out.

On the refused side - example
kazulog@sv2:~$ cd /tmp && timeout 40 tftp -v 192.168.100.10 -c get hello.txt hello-denied.txt; echo "rc=$?"
Connected to 192.168.100.10 (192.168.100.10), port 69
getting from 192.168.100.10:hello.txt to hello-denied.txt [netascii]
Transfer timed out.
rc=69

The refusal is logged. However, the address after from is not the client’s but the server’s own (192.168.100.10). In tftp-hpa 5.3, in.tftpd uses the client address for the decision but writes the server-side address to the log.

Refusal log - example
kazulog@sv1:~$ sudo journalctl -u tftpd-hpa --no-pager -n 3
Sep 12 10:02:55 sv1 in.tftpd[5396]: connection refused from 192.168.100.10

Final state of the configuration files

This is the state with everything above applied. Only two files were touched: /etc/default/tftpd-hpa and /etc/hosts.deny.

Configuration files and service state
kazulog@sv1:~$ cat /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="192.168.100.10:69"
TFTP_OPTIONS="--secure --create"
kazulog@sv1:~$ cat /etc/hosts.deny
# /etc/hosts.deny: list of hosts that are _not_ allowed to access the system.
#                  See the manual pages hosts_access(5) and hosts_options(5).
#
# Example:    ALL: some.host.name, .some.domain
#             ALL EXCEPT in.fingerd: other.host.name, .other.domain
#
# If you're going to protect the portmapper use the name "rpcbind" for the
# daemon name. See rpcbind(8) and rpc.mountd(8) for further information.
#
# The PARANOID wildcard matches any host whose name does not match its
# address.
#
# You may wish to enable this to ensure any programs that don't
# validate looked up hostnames still leave understandable logs. In past
# versions of Debian this has been the default.
# ALL: PARANOID
# TFTP は 192.168.100.20 からの接続を拒否する
in.tftpd: 192.168.100.20
kazulog@sv1:~$ sudo ls -l /srv/tftp/
total 8
-rw-r--r-- 1 root root 27 Sep 12 10:01 hello.txt
-rw-rw-rw- 1 tftp tftp 18 Sep 12 10:02 upload.txt
kazulog@sv1:~$ systemctl is-enabled tftpd-hpa; systemctl is-active tftpd-hpa
enabled
active
kazulog@sv1:~$ sudo ss -ulnp 'sport = :69'
State               Recv-Q              Send-Q                            Local Address:Port                            Peer Address:Port              Process
UNCONN              0                   0                                192.168.100.10:69                                   0.0.0.0:*                  users:(("in.tftpd",pid=4631,fd=4))              
FileChange
/etc/default/tftpd-hpaAdd --create to TFTP_OPTIONS; set TFTP_ADDRESS to 192.168.100.10:69
/etc/hosts.denyAppend in.tftpd: 192.168.100.20
/srv/tftpChange the owner to tftp:tftp so uploads are possible

Saving a router configuration

From the IOS XE router R1, upload the running-config with the copy command. It was run with file prompt quiet configured, to skip the confirmation questions.

Because --secure chroots the server to /srv/tftp, the URL takes just the file name. Adding a directory gives %Error opening ... (No such file or directory).

Saving the configuration from R1 - example
R1#copy running-config tftp://192.168.100.10/r1-confg
!!
4964 bytes copied in 0.597 secs (8315 bytes/sec)
Server side - example
kazulog@sv1:~$ sudo ls -l /srv/tftp/
total 16
-rw-r--r-- 1 root root   27 Sep 12 10:01 hello.txt
-rw-rw-rw- 1 tftp tftp 4964 Sep 12 10:05 r1-confg
-rw-rw-rw- 1 tftp tftp   18 Sep 12 10:02 upload.txt
kazulog@sv1:~$ sudo head -3 /srv/tftp/r1-confg
!
! Last configuration change at 01:04:58 UTC Sat Sep 12 2026

Removing

Purging removes /etc/default/tftpd-hpa, but /srv/tftp, the files in it and the tftp user remain.

Removal - example
kazulog@sv1:~$ sudo apt purge -y tftpd-hpa | cat
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
Solving dependencies...
REMOVING:
  tftpd-hpa*

Summary:
  Upgrading: 0, Installing: 0, Removing: 1, Not Upgrading: 96
  Freed space: 128 kB

(Reading database ... 91411 files and directories currently installed.)
Removing tftpd-hpa (5.3+20251209-2) ...
Processing triggers for man-db (2.13.1-1build1) ...
(Reading database ... 91399 files and directories currently installed.)
Purging configuration files for tftpd-hpa (5.3+20251209-2) ...
kazulog@sv1:~$ dpkg -l tftpd-hpa 2>&1 | tail -1
dpkg-query: no packages found matching tftpd-hpa
kazulog@sv1:~$ ls -l /etc/default/tftpd-hpa 2>&1
ls: cannot access '/etc/default/tftpd-hpa': No such file or directory
kazulog@sv1:~$ sudo ls -ld /srv/tftp 2>&1
drwxr-xr-x 2 tftp tftp 4096 Sep 12 10:05 /srv/tftp
kazulog@sv1:~$ getent passwd tftp
tftp:x:103:109:tftp daemon:/srv/tftp:/usr/sbin/nologin
kazulog@sv1:~$ sudo ss -ulnp 'sport = :69'; echo "ss rc=$?"
State                  Recv-Q                  Send-Q                                   Local Address:Port                                   Peer Address:Port                 Process
ss rc=0

Looking inside with a capture

UDP to and from sv2 was captured on the link between sv1 and the switch. The download, the two failed uploads, the successful upload and the retries of a refused request are all in one file.

Capture overview - example
$ tshark -r ubuntu-tftp.pcap -n -t ad
    1 2026-09-12 10:01:45.203039 192.168.100.20 → 192.168.100.10 TFTP 63 Read Request, File: hello.txt, Transfer type: netascii
    2 2026-09-12 10:01:45.209098 192.168.100.10 → 192.168.100.20 TFTP 74 Data Packet, Block: 1 (last)
    3 2026-09-12 10:01:45.209729 192.168.100.20 → 192.168.100.10 UDP 46 54144 → 55822 Len=4
    4 2026-09-12 10:02:07.874218 192.168.100.20 → 192.168.100.10 TFTP 64 Write Request, File: upload.txt, Transfer type: netascii
    5 2026-09-12 10:02:07.880190 192.168.100.10 → 192.168.100.20 TFTP 61 Error Code, Code: File not found, Message: File not found
    6 2026-09-12 10:02:13.525002 192.168.100.20 → 192.168.100.10 TFTP 64 Write Request, File: upload.txt, Transfer type: netascii
    7 2026-09-12 10:02:13.532589 192.168.100.10 → 192.168.100.20 TFTP 61 Error Code, Code: Not defined, Message: Request failed
    8 2026-09-12 10:02:19.359314 192.168.100.20 → 192.168.100.10 TFTP 64 Write Request, File: upload.txt, Transfer type: netascii
    9 2026-09-12 10:02:19.367159 192.168.100.10 → 192.168.100.20 TFTP 46 Acknowledgement, Block: 0
   10 2026-09-12 10:02:19.367998 192.168.100.20 → 192.168.100.10 UDP 65 52988 → 54084 Len=23
   11 2026-09-12 10:02:19.369136 192.168.100.10 → 192.168.100.20 TFTP 46 Acknowledgement, Block: 1
   12 2026-09-12 10:02:50.855073 192.168.100.20 → 192.168.100.10 TFTP 63 Read Request, File: hello.txt, Transfer type: netascii
   13 2026-09-12 10:02:55.856019 192.168.100.20 → 192.168.100.10 TFTP 63 Read Request, File: hello.txt, Transfer type: netascii
   14 2026-09-12 10:03:00.856469 192.168.100.20 → 192.168.100.10 TFTP 63 Read Request, File: hello.txt, Transfer type: netascii
   15 2026-09-12 10:03:05.856777 192.168.100.20 → 192.168.100.10 TFTP 63 Read Request, File: hello.txt, Transfer type: netascii
   16 2026-09-12 10:03:10.858100 192.168.100.20 → 192.168.100.10 TFTP 63 Read Request, File: hello.txt, Transfer type: netascii
$ tshark -r ubuntu-tftp.pcap -n -V -O tftp -Y 'frame.number == 2'
Frame 2: Packet, 74 bytes on wire (592 bits), 74 bytes captured (592 bits)
Ethernet II, Src: 52:54:00:b7:4d:fd, Dst: 52:54:00:ba:01:74
Internet Protocol Version 4, Src: 192.168.100.10, Dst: 192.168.100.20
User Datagram Protocol, Src Port: 55822, Dst Port: 54144
Trivial File Transfer Protocol
    Opcode: Data Packet (3)
    [Destination File: hello.txt]
    [Read Request in frame 1]
    Block: 1
    [Full Block Number: 1]
Data (28 bytes)

$ tshark -r ubuntu-tftp-no2.pcap -n -x
0000  52 54 00 ba 01 74 52 54 00 b7 4d fd 08 00 45 00   RT...tRT..M...E.
0010  00 3c 53 dc 00 00 40 11 dd 65 c0 a8 64 0a c0 a8   .<S...@..e..d...
0020  64 14 da 0e d3 80 00 28 08 c3 00 03 00 01 68 65   d......(......he
0030  6c 6c 6f 20 66 72 6f 6d 20 74 66 74 70 20 73 65   llo from tftp se
0040  72 76 65 72 20 73 76 31 0d 0a                     rver sv1..

$ tshark -r ubuntu-tftp.pcap -n -Y 'tftp.opcode == 5' -T fields -e frame.number -e tftp.error.code -e tftp.error.message
5	1	File not found
7	0	Request failed
$ tshark -r ubuntu-tftp.pcap -n -t ad -Y 'frame.number >= 8 && frame.number <= 11'
    8 2026-09-12 10:02:19.359314 192.168.100.20 → 192.168.100.10 TFTP 64 Write Request, File: upload.txt, Transfer type: netascii
    9 2026-09-12 10:02:19.367159 192.168.100.10 → 192.168.100.20 TFTP 46 Acknowledgement, Block: 0
   10 2026-09-12 10:02:19.367998 192.168.100.20 → 192.168.100.10 UDP 65 52988 → 54084 Len=23
   11 2026-09-12 10:02:19.369136 192.168.100.10 → 192.168.100.20 TFTP 46 Acknowledgement, Block: 1
$ tshark -r ubuntu-tftp.pcap -n -Y 'frame.number >= 12' -T fields -e frame.number -e frame.time_delta_displayed -e udp.srcport -e udp.dstport -e tftp.source_file
12		45348	69	hello.txt

The request goes to port 69, but the reply comes from a different port chosen by the server (No.1 is addressed to 69, No.2 comes from 55822).

Inside the DATA packet - example
$ tshark -r ubuntu-tftp.pcap -n -V -O tftp -Y 'frame.number == 2'
Frame 2: Packet, 74 bytes on wire (592 bits), 74 bytes captured (592 bits)
Ethernet II, Src: 52:54:00:b7:4d:fd, Dst: 52:54:00:ba:01:74
Internet Protocol Version 4, Src: 192.168.100.10, Dst: 192.168.100.20
User Datagram Protocol, Src Port: 55822, Dst Port: 54144
Trivial File Transfer Protocol
    Opcode: Data Packet (3)
    [Destination File: hello.txt]
    [Read Request in frame 1]
    Block: 1
    [Full Block Number: 1]
Data (28 bytes)

Being netascii, the payload ends with 0d 0a (CR LF).

No.2 in hex - example
$ tshark -r ubuntu-tftp-no2.pcap -n -x
0000  52 54 00 ba 01 74 52 54 00 b7 4d fd 08 00 45 00   RT...tRT..M...E.
0010  00 3c 53 dc 00 00 40 11 dd 65 c0 a8 64 0a c0 a8   .<S...@..e..d...
0020  64 14 da 0e d3 80 00 28 08 c3 00 03 00 01 68 65   d......(......he
0030  6c 6c 6f 20 66 72 6f 6d 20 74 66 74 70 20 73 65   llo from tftp se
0040  72 76 65 72 20 73 76 31 0d 0a                     rver sv1..
Download the pcap of the packet in the tshark output above (No.2 DATA)

The two failures carry different error codes.

ERROR packets - example
$ tshark -r ubuntu-tftp.pcap -n -Y 'tftp.opcode == 5' -T fields -e frame.number -e tftp.error.code -e tftp.error.message
5	1	File not found
7	0	Request failed

A successful upload goes WRQ → ACK (Block 0) → DATA → ACK (Block 1).

The upload exchange - example
$ tshark -r ubuntu-tftp.pcap -n -t ad -Y 'frame.number >= 8 && frame.number <= 11'
    8 2026-09-12 10:02:19.359314 192.168.100.20 → 192.168.100.10 TFTP 64 Write Request, File: upload.txt, Transfer type: netascii
    9 2026-09-12 10:02:19.367159 192.168.100.10 → 192.168.100.20 TFTP 46 Acknowledgement, Block: 0
   10 2026-09-12 10:02:19.367998 192.168.100.20 → 192.168.100.10 UDP 65 52988 → 54084 Len=23
   11 2026-09-12 10:02:19.369136 192.168.100.10 → 192.168.100.20 TFTP 46 Acknowledgement, Block: 1

When refused, five requests from the same source port appear five seconds apart, and the server answers none of them.

Retries when refused - example
$ tshark -r ubuntu-tftp.pcap -n -Y 'frame.number >= 12' -T fields -e frame.number -e frame.time_delta_displayed -e udp.srcport -e udp.dstport -e tftp.source_file
12		45348	69	hello.txt
13	5.000946000	45348	69	hello.txt
14	5.000450000	45348	69	hello.txt
15	5.000308000	45348	69	hello.txt
16	5.001323000	45348	69	hello.txt

Test environment and session logs

Two Ubuntu 26.04 LTS Servers and one IOS XE router on CML. The Ubuntu disks were wiped immediately beforehand, because packages left over from earlier testing would stop the procedure from reproducing.

Test topology
  sv1                         sv2                         R1
  ens3 192.168.100.10/24      ens3 192.168.100.20/24      Gi2 192.168.100.1/24
  tftpd-hpa (UDP 69)          tftp-hpa                    IOS XE 17.03.08a
   |                           |                           |
   +--------- LAB-SW ----------+---------------------------+
   ^ captured on this link (UDP to and from sv2 only)

The full capture:

Capture of the TFTP traffic (ubuntu-tftp.pcap)
Stepsv1sv2R1
Initial state (just wiped)show / conf / logshow / conf / log
Installing and checking the fileshow / conf / log
Placing a file and downloading itshow / conf / logshow / conf / log
Uploading with the defaultsshow / conf / log
Adding –createshow / conf / logshow / conf / log
Changing the ownershow / conf / logshow / conf / log
Restricting the listening addressshow / conf / log
Restricting sourcesshow / conf / logshow / conf / log
The refusal in the logshow / conf / log
Final state of the filesshow / conf / log
Saving from the routershow / conf / logshow
Removing (final state)show / conf / log
Analysing the capturetshark

Reference

RFC 1350 - The TFTP Protocol (Revision 2)

Related articles

Ubuntu official pages