We're coming out with a managed firewall product at work that is basically an OpenBSD machine running pf that supports VPNs and all the usual malarkey.

An issue we run into a lot with our hosted PBX service is that some customers have networks with firewalls that cause problems with TFTP, SIP, latency, etc. It makes diagnosing problems harder and often the customers think the problems are with our phone system when they're really with their firewall. So if they get our firewall, we know everything will work and we'll have the ability to change things if something doesn't work.

One thing that usually doesn't work with firewalls is TFTP. TFTP is a crappy old protocol that I wish would just die, but SIP phone vendors still like to use it. Add NAT to the mix and TFTP is destined to fail.

Since TFTP doesn't work with NAT on pf, I had to make it work. I looked at the code for ftp-proxy and started implementing TFTP support in it (conditionalizing it to use UDP vs. TCP) but after writing all the code, I realized it wouldn't be an ideal solution. ftp-proxy runs as a daemon using libevent and manages all FTP sessions for the length of their TCP connections. A TFTP helper only needs to see the first packet of the connection and quit after setting up the pf rules.

So, I started over and made a separate program that is spawned from inetd and does some things:

  1. Read the initial packet from the client
  2. Make sure the packet is a TFTP request (only for some half-baked security, not necessarily needed)
  3. Look up the connection in pf's NAT table to see where the packet was originally destined (since we got it from an rdr)
  4. Create a new connection to the TFTP server from the firewall using a dynamic source port
  5. Add an rdr rule to redirect traffic from the TFTP server to the firewall on the dynamic port, to the original client on the port it initiated the request from
  6. Forward the initial packet we got in the first step to the TFTP server using the connection we just setup, initiating the transfer
  7. Exit, letting pf handle the task of rewriting packets between the client and the TFTP server

With a simple pf setup redirecting TFTP requests to port 6969:

nat on $ext_if from !($ext_if) -> ($ext_if:0)
no nat on $ext_if to port tftp

rdr-anchor "tftp-proxy/*"
rdr on $int_if proto udp from any to port tftp -> port 6969

and inetd set to spawn the TFTP helper on port 6969:

6969 dgram udp wait root /usr/local/libexec/tftp-proxy tftp-proxy -v

stuff just works. Here's a Cisco SIP phone booting up:

Nov 23 15:46:33 fw tftp-proxy[7737]: -> 216.145.x.x:49441 -> 209.242.x.x:69 RRQ "CTLSEP0014F2xxxxxx.tlv"
Nov 23 15:46:34 fw tftp-proxy[10976]: -> 216.145.x.x:51006 -> 209.242.x.x:69 RRQ "SEP0014F2xxxxxx.cnf.xml"
Nov 23 15:46:34 fw tftp-proxy[17497]: -> 216.145.x.x:63474 -> 209.242.x.x:69 RRQ "SIP0014F2xxxxxx.cnf"
Nov 23 15:46:34 fw tftp-proxy[9525]: -> 216.145.x.x:59203 -> 209.242.x.x:69 RRQ "P0S3-07-4-00.loads"
Nov 23 15:46:48 fw tftp-proxy[30808]: -> 216.145.x.x:57846 -> 209.242.x.x:69 RRQ "SIPDefault.cnf"
Nov 23 15:46:48 fw tftp-proxy[29502]: -> 216.145.x.x:57192 -> 209.242.x.x:69 RRQ "SIP0014F2xxxxxx.cnf"
Nov 23 15:46:50 fw tftp-proxy[32746]: -> 216.145.x.x:59155 -> 209.242.x.x:69 RRQ "RINGLIST.DAT"
Nov 23 15:46:50 fw tftp-proxy[30719]: -> 216.145.x.x:60863 -> 209.242.x.x:69 RRQ "dialplan.xml"

I'll clean up the code when I get back to work next week and release it.

Questions or comments?
Please feel free to contact me.