Port-knocking: open ports on demand (BONUS: Python client!)


As soon as I deploy a new server on the Internet, it starts being attacked by (evil) hackers and their bots. It's almost immediate, specially brute-force SSH attacks. There are probably thousands of bots out there, scanning non-stop whole ranges of IP addresses for vulnerabilities.

Sure, with a properly updated server, and with things like Fail2ban, you can (hopefully) sleep in peace... But wouldn't it be better if the ports that don't need to be permanently open (like SSH) stayed closed, and you somehow could tell the server to open it whenever needed?

Say hello to the port-knocker daemon! You may be asking at this point: "well, to be able to tell the server to open a certain port, it needs to be listening to my instructions, which means another open port, right?" Wrong! The basic mechanism behind port-knocking is that the daemon will capture raw packets in promiscuous mode, which means that even packets destined to closed ports will be "seen" by the daemon. That way, it can wait for the "right" packets to arrive, which will trigger a certain action.

What do I mean by "right" packets? They are packets that match a pre-defined sequence of ports and protocols, during a certain time interval. For instance, I can say that if, in a 3-seconds window, packets arrive for port 1111 UDP followed by port 10221 TCP, it should insert a firewall rule to open SSH for my (client) IP address.

It will become clearer during the hands-on.


While most Linux distributions provide packages for port-knocking daemons, I like to use this one, which includes IPv6 support. Follow the instructions there to compile and install, which are straightforward (you may need to install the packages necessary for the compilation, according to your Linux distribution).

Once the daemon is installed, we need to make sure it will be brought up on boot. For that, I created the following systemd unit:
Description=Port-Knock Daemon
After=syslog.target network.target

ExecReload=/bin/kill -HUP $MAINPID
CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN

Place it in /etc/systemd/system/knockd.service, then run:
systemctl daemon-reload
systemctl enable knockd
After we configure the service, in the next section, you can start it with systemctl start knockd.


The configuration is relatively simple, defined in /etc/knockd.conf (depending on your installation). Remember: a certain sequence of ports knocked in a time window will trigger an action. For example, we may want to open a door in the firewall by inserting a rule, or start a service.

"But we need to close the door, or stop the service, after we don't need it anymore, right?" Correct, and for that we have two approaches: another sequence to trigger the respective commands, or use the stop_command parameter in the same sequence, which will be called automatically after a configured timeout. In other words: we trigger the action, and after some time, another action runs automatically.

Here's an example:
Interface = eth0

sequence = 1111:udp,2222:udp
seq_timeout = 5
start_command = /sbin/iptables -I INPUT -s %IP% -p tcp -m tcp --dport 22 -m state --state NEW -j ACCEPT
cmd_timeout = 600
stop_command = /sbin/iptables -D INPUT -s %IP% -p tcp -m tcp --dport 22 -m state --state NEW -j ACCEPT
Self-explanatory, right? You can also use the parameters start_command_6 and stop_command_6 for IPv6. It's also possible to create more advanced configurations, like specifying, for TCP, which flags must be set in the packet header (like SYN, ACK etc.). Check the man page for more details.

If something is not working as expected, I usually use tcpdump to confirm that the packages are arriving correctly.

Bonus: Python client

For Linux and Android, I could find some nice port-knocking clients. But I wanted something for multiple platforms, and able to read a configuration file where I could specify different profiles to run. So, since I'm also learning Python, I decided to implement my own. :)

Check it out on my GitHub repository: https://github.com/dbolivar/python_port_knocker.