
By Sean, Principle Software Engineer
Nine-minute read
A few years ago, I had this idea: What if you could combine the security of 2 Factor Authentication (2FA) codes with port knocking?
Imagine you could have a network service which could only be reached by trusted people. In other words, access to the service’s network port is restricted as it would be in a traditional port knocking arrangement, but in this case, the port knock sequence is constantly changing in lockstep with one’s 2FA code. The idea was born out of frustration in my attempts to securely host an SSH service from my home’s internet connection. For example, in following best practices, I hosted the service on a non-standard port, something like 50123. Unfortunately, I often forgot which port I had chosen and had to resort to scanning my own host from the internet! For the sake of convenience, I want to run SSH on the standard port (22) – I just don’t want anyone else to know it’s there. I want to have my cake and eat it too.
Enter Tnok – a generic, drop-in, 2-factor gatekeeper for any TCP or UDP network service, providing port-level access controls. The name is a very uninspired combination of TOTP (Timed One-Time-Pad) and knock, shortened to a string of characters that’s easy to type on a keyboard.

Core Problem
I needed a way to conceal – and therefore protect – network ports such that no interaction with the running service was allowed until a time-based secret code was presented. In other words, there should be no evidence of a running network service until a valid secret code has been provided.
Additional Requirements
As I worked to prototype solutions and develop proof-of-concepts, I identified a handful of additional requirements to make the resulting tool more usable and deployable by anyone.
- The solution must support Windows and Linux, including legacy systems.
- The app that performs the knock to open a port must not require elevated privileges to run.
- The protected port must not reply with any packets when probed until unlocked.
- Deployment should not require any configuration changes or port forwarding.
Existing Solutions Don’t Cut It
It is possible to cobble together existing tools to meet some or most of the requirements above, but doing so would require duct tape and copious amounts of shoe string, and likely would be difficult to use and deploy. My spidey sense told me that a dedicated tool was likely a welcome addition to this space.
At first blush, traditional port knocking solves the hidden port problem, but it has several shortcomings. First and foremost, the knock sequence is static and doesn’t change over time. Not to mention the additional network configuration required to forward those knock ports to the destination server. Even if you had a port knocker where the port sequence changed, you’d still need a solution to handle the configuration so that those ports reach the server listening for a successful knock. You could run the port knock listener on the firewall (or in some cases port knocking may be supported by the firewall already), but there are so many different firewalls, and it makes for a more complicated setup by having the knock listener running separately from the service it’s protecting, especially if you want to protect several ports on several different servers.
The following depicts a simple example of traditional port knocking. A legitimate client sends 3 UDP packets to 3 pre-determined ports in order and then can connect to their target service – SSH in this case. Rogue clients are blocked, and the port appears closed.

It’s easy to imagine how complicated this solution can become once you start adding external firewalls, multiple servers, and multiple ports. The goal with Tnok is to keep the deployment simple and consistent no matter the size or complexity of your network.
Building Tnok
To solve my problem and meet all my requirements, I needed a client/server knock protocol that:
- Works for TCP and UDP without requiring response packets until after the knock succeeded.
- Works with existing network infrastructure and firewall configurations to reach the target service without additional port forwarding.
- Is cross platform to support protecting services running on both Windows and Linux as well as support knocking from Windows, Linux, and even Android clients (for ease of use).
- Utilizes existing TOTP libraries to easily integrate with people’s preferred 2FA authenticator apps.
- Supports multiple users each with unique TOTP secrets.
- Is extensible and supports updated knock techniques without requiring major rewrites.
I came up with a flexible TOTP port knocker that I call Tnok. It’s written in Python for ease of development and cross-platform support. Additionally, well-tested libraries exist for both TOTP code generation and packet sniffing. The Tnok application consists of a client and server, “tnok” and “tnokd” respectively. The protocol, depicted below, improves upon traditional port knocking by encoding the 6-digit TOTP code in the knock packet(s) which are sent directly to the port to open, allowing the knock protocol to piggy-back on existing network configuration and firewall rules to ensure knock packets reach the destination server.

As you can see from the above, it doesn’t matter what exists between you and the service you want to connect to. If the port is open and accessible, you can install the Tnok service, protect the port, and then knock to open it whether it’s a TCP or UDP port.
Knock Listener
The Tnok service, “tnokd”, configures the host’s firewall to block the port(s) to protect and then sets up as a packet sniffer. The service looks for our specially crafted knock packets and then compares the code against the database of valid codes for the user’s configured on the system. If the code is valid, the protocol moves to an authentication stage, where data is encrypted and exchanged with the client. This data includes the IP addresses to allow and the username. The username is matched against the code to ensure not only that the code was valid for some user, but that it’s valid for that specific user. Once complete, the port is opened for the provided source IPs.
UDP Knock – TOO EASY!
For UDP knocks from the client, the solution was simple: Send a UDP packet containing the 6-digit code. UDP is much easier than TCP; there is no handshake, there is no requirement that the remote server respond with anything. You can just blast out a UDP packet with whatever contents you want.
TCP Knock – This Protocol Was Not Designed for What I’m Trying to Do
For TCP knocks from the client, the solution is more complicated. You can’t just send a 6-digit code to a TCP port and expect that packet to get through. You must establish a TCP connection first. This starts with a SYN packet. If a TCP port is open and forwarded through a firewall to a server, you can be sure that a TCP SYN packet will make it to the destination. We can’t allow the TCP connection to establish though, because that would result in packets coming back from the server prior to the knock code being validated. So, what do we do?
We could append data to the end of the SYN. TCP Fast Open does something like that, so we can just put the code as the data of a SYN packet. This, however, doesn’t work well in practice. Not all networks are setup to support that, some networks drop the packets. Alright, how about TCP options? I spent more time than I would have liked combing through the various RFCs for TCP options, but at the end of the day, it’s just a field of the TCP header that can store bytes. We can put our bytes in there (maybe) and if we’re careful, network infrastructure will hopefully not drop our packets.
Lots of back and forth on what to use, I finally landed on a few options I could leverage:
- TCP Mood – An April fool’s joke from Google back in 2010 (https://datatracker.ietf.org/doc/html/rfc5841) to denote the TCP packet’s mood.
- TCP MD5 – https://datatracker.ietf.org/doc/html/rfc4808 – Used for BGB.
I only need to store a 6-digit code, which at most is 999,999 which can be stored in 3 bytes, so both of these options are sufficient. A bunch of testing on and between various different networks – cell, home LAN, work LAN, digital ocean droplets, etc. – and these two options both work well. They don’t seem to flag or trigger anything weird, and they don’t get dropped by anything I’ve tested on yet.

So, I finally have my solution. That’s what I thought initially at least. On Windows, this works well. Why? On Windows you don’t need to be admin to craft packets (by default). When you install npcap (required for Scapy to craft packets), the checkbox for “Restrict Npcap driver’s access to Administrators only” is unchecked. Meaning non-admins can craft packets on Windows if you didn’t make any non-standard changes during install. This is great and I’m fine with that for Windows, but what about Linux?
On Linux you can’t craft packets without root. I tried several workarounds here. I tried to auto-elevate, but struck me as slightly sketchy in that I don’t like things that just try to silently auto-elevate. I thought maybe setuid would work, but that wouldn’t work if you install from a python wheel. The other constraint I was working with here was that I wanted this solution to work from mobile devices. It wasn’t my primary goal, but I wanted to be able to knock from my android phone and I didn’t want to have to root the phone to do it.
So, it was back to the drawing board.
I had to find something I could do with a standard TCP socket, without requiring root, where I could control the value, and that value was transmitted with the packet.
I spent a good amount of time in Wireshark and Linux man pages playing around with different socket options. I would set a socket option with some data and look at it on the wire. I tried messing with source port, which seemed promising, until I remembered leaving a LAN through a gateway is going to blow away whatever source port you chose. You can’t control the source port mapping of your gateway. There are tons of IP options that can hold data, but I couldn’t get packets out with IP options set. They’d be dropped or ignored by intermediate routers and switches and things.
Max Segment Size (MSS) – This Isn’t Even Remotely Close to What This Option Is For!
After hours and hours of fighting with this non-root TCP knock problem, the only TCP option I could control with any reliability without root was the max segment size (MSS). I thought for sure this wouldn’t work, but it was all I had so I spent some time thinking about possible solutions. MSS is constrained between a minimum and maximum value. With root you could set the MSS to any 2 byte value by crafting the packet, but without root, the kernel will validate the value you try to set and not allow you to set it to technically invalid or impossible values. For example, the MSS cannot exceed the Maximum Transmission Unit (MTU). It also can’t be too small (88 is the minimum enforced by the Linux kernel).
So what now?
- We need to send 6 digits; let’s send 2 at a time.
- The MSS can never be lower than 88 and cannot exceed the MTU. We can safely stay away from both those numbers by using only 3-digit numbers under 400 for the MSS. This also puts our MSS values in a range not normally seen in packets, so we’ll stand out a bit more, which is handy for detecting the knocks.
So, if a valid 2FA code is currently 123456 and we want to knock using the MSS technique, we’ll send 3 TCP SYN packets, each with a different MSS. We need to send 12, 34, and 56. So we’ll set the MSS to 112, 234, and 356 respectively. This encodes the order (1XX, 2XX, then 3XX) as well as the code. We’ll also set the IP header TTL to the max value (255) as another way to stand out more for our knock sniffer.

Wrapping it All Up
Now that we have the client code figured out for TCP and UDP, and we can knock from Windows or Linux without elevation, we can make a few final tweaks to our knock protocol to really dial it in.
On the server-side where we sniff the packets, we’ll want to filter a bit so we’re ignoring packets that are not knock packets. We know for TCP we’re using either the MD5, Mood, or MSS option and all the knock packets will be SYN packets. So, if a TCP packet is a SYN and has Mood or MD5, it might be a knock and we’ll try to parse it. MSS seems to be present in every TCP SYN packet, so we’ll look for TCP SYN packets with MSS values under 400 since that’s less common. For UDP we can just look for our knock header bytes in the packet. As an additional filter we can set the IP TTL option to 255. The default on Linux is 64, so someone smarter than me is pretty sure nothing on the internet should be more than 64 hops away. We can look at the incoming packets and ignore anything with a TTL less than 200-ish to further limit noise in our sniffer.
Try it Out!
This was a long one, but it covers most of the development that went into Tnok. I didn’t cover every single feature. Head over to our GitLab page for Tnok and check it out to learn more or install it yourself. Some of the highlights:
- No additional port forwarding required.
- Built-in brute-force protection via automated IP blocking.
- Support for multiple users each with unique time codes.
- Support for legacy systems as far back as Windows 7 SP1.
- Simple service wrappers for ssh and scp (more to come in the future).