#!/usr/bin/bash
#
# Placed in the public domain.
#
# A small bash script that generates an nft portknocking table
#
# If a client knocks a set of tcp ports in the right sequence (declared in
# the PORTS array below), the client IP is added to the clients_ipv4 and
# clients_ipv6 tables, respectively.
#
# The given example configuration blocks all access to the ssh port (port
# 22) for all clients (except localhost) per default. Only a client that
# successfully knocks tcp ports 123, 234, 345, and 456 (within 1s between
# individual knocks) has 10s to establish an SSH session. On the client
# side this can be done as follows
#
#     $ telnet $host 123
#     $ telnet $host 234
#     $ telnet $host 345
#     $ telnet $host 456
#     $ ssh $host
#

# name and priority of the inet filter table that will be created
TABLE_NAME="portknock"
TABLE_PRIORITY="-10"

# the port knocking sequence (caveat: in the current implementation
# repeated port numbers short-circuit the knocking sequence)
PORTS=(123 234 345 456)

# timeout after which a candiate gets removed from the candidates_* table
# if no knock on the correct next port occurs
TIMEOUT_CANDIDATES="1s"

# timeout after which a client gets removed from the clients_* table after
# a successful port knock
TIMEOUT_CLIENTS="10s"

# a log rule appended to a successful port knock
LOG_RULE="log prefix \"Successful portknock: \""

# a preamble printed at the beginning of the generated rule set
PREAMBLE="
define guarded_ports = {ssh}
"

# early rules printed in the input chain before the port knocking logic
EARLY_RULES="
		iifname \"lo\" return
"

# late rules printed in the input chain after the port knocking logic
LATE_RULES="
		tcp dport \$guarded_ports ip  saddr @clients_ipv4 counter accept
		tcp dport \$guarded_ports ip6 saddr @clients_ipv6 counter accept
		tcp dport \$guarded_ports ct state established,related counter accept

		tcp dport \$guarded_ports counter reject with tcp reset
"


################################################################################
################################################################################


cat <<EOF
$PREAMBLE
table inet $TABLE_NAME {
	set clients_ipv4 {
		type ipv4_addr
		flags timeout
	}

	set clients_ipv6 {
		type ipv6_addr
		flags timeout
	}
EOF

if [ ${#PORTS[@]} -gt 1 ]; then
cat <<EOF

	set candidates_ipv4 {
		type ipv4_addr . inet_service
		flags timeout
	}

	set candidates_ipv6 {
		type ipv6_addr . inet_service
		flags timeout
	}
EOF
fi

cat <<EOF

	chain input {
		type filter hook input priority $TABLE_PRIORITY; policy accept;
$EARLY_RULES
EOF

for (( i = 0; i < ${#PORTS[@]}; i+=1 )); do
  echo -n "		tcp dport ${PORTS[i]}"
  [ $i != 0 ] && echo -n " ip  saddr . tcp dport @candidates_ipv4"
  if [ $i != $((${#PORTS[@]} - 1)) ]; then
    echo " add @candidates_ipv4 {ip  saddr . ${PORTS[i+1]} timeout $TIMEOUT_CANDIDATES}"
  else
    echo " add @clients_ipv4 {ip  saddr timeout $TIMEOUT_CLIENTS} $LOG_RULE"
  fi

  echo -n "		tcp dport ${PORTS[i]}"
  [ $i != 0 ] && echo -n " ip6 saddr . tcp dport @candidates_ipv6"
  if [ $i != $((${#PORTS[@]} - 1)) ]; then
    echo " add @candidates_ipv6 {ip6 saddr . ${PORTS[i+1]} timeout $TIMEOUT_CANDIDATES}"
  else
    echo " add @clients_ipv6 {ip6 saddr timeout $TIMEOUT_CLIENTS} $LOG_RULE"
  fi
done

cat <<EOF
$LATE_RULES
	}
}
EOF

