Let Linux Firewall rookie nftables escort your VPS

Last article This paper introduces the advantages and basic usage of nftables. The advantage of nftables is that it compiles network rules into bytecode directly in user mode, and then it is executed by virtual machine of kernel. Although it is based on netfilter as iptables, nftables is more flexible.

Before using iptables to match a large amount of data, you need ipset to match. nftables has built-in sets and dictionaries directly, which can match a large amount of data directly. This is much more convenient than iptables. It's really good to practice magic. There's not much explanation. Please read it directly Linux global intelligent shunting scheme.

This article will teach you how to configure nftables to implement a simple firewall for the server. This article takes CentOS 7 as an example, and other distributions are similar.

1. Install nftables

First, install nftables:

$ yum install -y nftables

Since nftables does not have a built-in chain by default, but provides some sample configurations, we can include them into the main configuration file. The main configuration file is / etc/sysconfig/nftables.conf, uncomment the following line:

# include "/etc/nftables/inet-filter"

Then start the nftables service:

$ systemctl start nftables

Now, if you look at the rules again, you will find that there is an additional filter table and several chains:

$ nft list ruleset

table inet filter {
	chain input {
		type filter hook input priority 0; policy accept;
	}

	chain forward {
		type filter hook forward priority 0; policy accept;
	}

	chain output {
		type filter hook output priority 0; policy accept;
	}
}

In nftables, ipv4 and ipv6 protocols can be combined into a single address cluster inet. If inet address cluster is used, there is no need to specify two different rules for ipv4 and ipv6 respectively.

2. Add INPUT rule

Like iptables, the filter table of nftables contains three chains: INPUT, FORWARD, and OUTPUT. Generally, you only need to configure the INPUT chain to configure the firewall.

Loopback Interfaces

First, allow access to localhost:

$ nft add rule inet filter input iif "lo" accept
$ nft add rule inet filter input iif != "lo" ip daddr 127.0.0.0/8 drop

It can be further optimized by adding comment s and counter s:

$ nft add rule inet filter input \
   iif "lo" \
   accept \
   comment \"Accept any localhost traffic\"

$ nft add rule inet filter input \
   iif != "lo" ip daddr 127.0.0.0/8 \
   counter \
   drop \
   comment \"drop connections to loopback not coming from loopback\"

View rules:

$ nft list chain inet filter input

table inet filter {
	chain input {
		type filter hook input priority 0; policy accept;
		iif "lo" accept comment "Accept any localhost traffic"
		iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback"
	}
}

Connection tracking module

The next rule uses a kernel module called conntrack (connection tracking), which is used to track the status of a connection. The most common usage scenario is NAT. Why do you need to track and record the status of the connection? Because nftables need to remember what the destination address of the packet has been changed to, and change the destination address back when the packet is returned.

Like iptables, a TCP connection has four states in nftables: NEW, ESTABLISHED, RELATED, and INVALID.

Except that the locally generated package is processed by the OUTPUT chain, all connection traces are processed in the pre routing chain, that is, iptables will calculate all the states in the pre routing chain. If we send an initialization package of a stream, the status will be set to NEW in the OUTPUT chain. When we receive the response package, the status will be set to ESTABLISHED in the pre outgoing chain. If the first packet that receives the response is not generated locally, it will be set to the NEW state in the PREROUTING chain. To sum up, all state changes and calculations are completed in the preceding chain and OUTPUT chain in the nat table.

There are two other states:

  • RELATED: the RELATED state is a bit complex. When a connection is RELATED to another connection that is already ESTABLISHED, the connection is considered RELATED. This means that to be RELATED, a connection must first have a connection that is already ESTABLISHED. The ESTABLISHED connection generates a new connection other than the primary connection, which is the RELATED state.
  • Invalid: indicates that the connection corresponding to the packet is unknown, indicating which connection or no state the packet cannot be recognized. There are several reasons for this, such as memory overflow, receiving ICMP error messages that do not know which connection they belong to. We need to DROP anything in this state and print the log:
$ nft add rule inet filter input \
   ct state invalid \
   log prefix \"Invalid-Input: \" level info flags all \
   counter \
   drop \
   comment \"Drop invalid connections\"

View rules:

$ nft list chain inet filter input

table inet filter {
	chain input {
		type filter hook input priority 0; policy accept;
		iif "lo" accept comment "Accept any localhost traffic"
		iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback"
		ct state invalid log prefix "Invalid-Input: " level info flags all counter packets 0 bytes 0 drop comment "Drop invalid connections"
	}
}

Token bucket

In order to prevent malicious attackers from using ping flood to attack, token bucket model can be used to limit the speed of ping packets. The principle of ping flooding is very simple, which is to send multiple ICMP request messages at one time by using multithreading method, so that the destination host is busy processing a large number of these messages, resulting in slow speed or even downtime.

Let's first introduce an order pail model.

Friends who are familiar with iptables should know that iptables implements the speed limit function through the hashlimit module, while the hashlimit matching method is based on the Token bucket model, and nftables are similar, Token bucket is a common buffer working principle in network communication. It has two important parameters: token bucket capacity n and token generation rate s

  • Token bucket capacity n: the token can be regarded as a ticket, while token bucket is the administrator responsible for making and issuing tickets. It has at most N tokens in its hand. At the beginning, the administrator starts to have n tokens in his hand. Every time a packet arrives, the administrator will see if there are any tokens available in his hand. If yes, send the token to the packet. Limit tells nftables that the packet is matched. When all the tokens on the administrator's handle are sent out, the next packet will not get the token. At this time, the limit module tells nftables that the packet cannot be matched.
  • Token generation rate s: when the number of tokens in the token bucket is less than N, it will generate new tokens at the rate s until the number of tokens reaches n.

Through the token bucket mechanism, you can effectively control the number of packets passing (matching) in unit time, and allow a large number of packets to pass in a short time (as long as the number of packets does not exceed the token bucket n), which is wonderful.

nftables is better than iptables. It can not only speed limit based on packets, but also speed limit based on bytes. In order to more accurately verify the token bucket model, we choose to speed limit based on bytes:

$ nft add rule inet filter input \
   ip protocol icmp icmp type echo-request \
   limit rate 20 bytes/second burst 500 bytes \
   counter \
   accept \
   comment \"No ping floods\"

The above rules represent:

  • Create a match for all ICMP packets of echo request type;
  • The token bucket capacity corresponding to the match item is 500 bytes;
  • Token generation rate is 20 bytes / s

Add another rule to reject packets that do not meet the appeal criteria:

$ nft add rule inet filter input \
   ip protocol icmp icmp type echo-request \
   drop \
  comment \"No ping floods\"

At the same time, receive the packets with the status of ESTABLISHED and RELATED:

$ nft add rule inet filter input \
   ct state \{ established, related \} \
   counter \
   accept \
   comment \"Accept traffic originated from us\"

Let's do an experiment to ping the IP address of the server directly. The packet size is set to 100 bytes and sent once per second:

$ ping -s 92 192.168.57.53 -i 1

PING 192.168.57.53 (192.168.57.53) 92(120) bytes of data.
100 bytes from 192.168.57.53: icmp_seq=1 ttl=64 time=0.402 ms
100 bytes from 192.168.57.53: icmp_seq=2 ttl=64 time=0.373 ms
100 bytes from 192.168.57.53: icmp_seq=3 ttl=64 time=0.465 ms
100 bytes from 192.168.57.53: icmp_seq=4 ttl=64 time=0.349 ms
100 bytes from 192.168.57.53: icmp_seq=5 ttl=64 time=0.411 ms
100 bytes from 192.168.57.53: icmp_seq=11 ttl=64 time=0.425 ms
100 bytes from 192.168.57.53: icmp_seq=17 ttl=64 time=0.383 ms
100 bytes from 192.168.57.53: icmp_seq=23 ttl=64 time=0.442 ms
100 bytes from 192.168.57.53: icmp_seq=29 ttl=64 time=0.464 ms
...

First of all, we can see that the responses of the first five packages are very normal, and then from the sixth package, we can receive a normal response every six seconds. This is because we set the token bucket capacity to 500 bytes, the token generation rate to 20 bytes / s, and the packet rate to 100 bytes per second, that is, 100 bytes per packet. After five packets are sent, the token bucket capacity becomes 0, and then it starts with 20 bytes / s 5 seconds later, the token bucket capacity becomes 100 bytes, so the normal response can be received 6 seconds later.

ICMP & IGMP

Receive other types of ICMP Protocol packets:

$ nft add rule inet filter input \
   ip protocol icmp icmp type \{ destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem \} \
   accept \
   comment \"Accept ICMP\"

Receive IGMP protocol packet:

$ nft add rule inet filter input \
   ip protocol igmp \
   accept \
   comment \"Accept IGMP\"

Handle TCP and UDP respectively

In this step, we split the traffic of TCP and UDP, and then deal with them separately. Create two chains first:

$ nft add chain inet filter TCP
$ nft add chain inet filter UDP

Then create a naming Dictionary:

$ nft add map inet filter input_vmap \{ type inet_proto : verdict \; \}

The key of the dictionary represents the protocol type, and the value represents the decision action.

To add elements to a dictionary:

$ nft add element inet filter input_vmap \{ tcp : jump TCP, udp : jump UDP \}

Finally, create a rule to split the traffic of TCP and UDP:

$ nft add rule inet filter input meta l4proto vmap @input_vmap

Among them, meta l4proto is used to match the protocol type.

Finally, take a look at the rules:

$ nft list ruleset

table inet filter {
	map input_vmap {
		type inet_proto : verdict
		elements = { tcp : jump TCP, udp : jump UDP }
	}

	chain input {
		type filter hook input priority 0; policy accept;
		iif "lo" accept comment "Accept any localhost traffic"
		iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback"
		ct state invalid log prefix "Invalid-Input: " level info flags all counter packets 95 bytes 6479 drop comment "Drop invalid connections"
		icmp type echo-request limit rate 20 bytes/second burst 500 bytes counter packets 17 bytes 2040 accept comment "No ping floods"
		icmp type echo-request drop comment "No ping floods"
		ct state { established, related } counter packets 172135 bytes 99807569 accept comment "Accept traffic originated from us"
		icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment "Accept ICMP"
		ip protocol igmp accept comment "Accept IGMP"
		meta l4proto vmap @input_vmap
	}

	chain forward {
		type filter hook forward priority 0; policy accept;
	}

	chain output {
		type filter hook output priority 0; policy accept;
	}

	chain TCP {
	}

	chain UDP {
	}
}

3. Process TCP traffic

In this step, we'll deal with TCP traffic. The first thing to do is ssh. You have to let this big brother go

$ nft add rule inet filter TCP \
   tcp dport 22 \
   ct state new \
   limit rate 15/minute \
   log prefix \"New SSH connection: \" \
   counter \
   accept \
   comment \"Avoid brute force on SSH\"

Secondly, Web services need to be released. As above, in order to be easy to manage and facilitate the subsequent dynamic addition of ports, a named collection needs to be created first:

$ nft add set inet filter web \{ type inet_service \; flags interval \; \}

To view a collection:

$ nft list set inet filter web

table inet filter {
	set web {
		type inet_service
		flags interval
	}
}

To add elements to a collection:

$ nft add element inet filter web \{ 80, 443 \}

To view a collection:

$ nft list set inet filter web

table inet filter {
	set web {
		type inet_service
		flags interval
		elements = { http, https }
	}
}

Release Web service:

$ nft add rule inet filter TCP \
   tcp dport @web \
   counter \
   accept \
   comment \"Accept web server\"

If you have other applications that cannot be described, such as proxy like xxx, you can add rules in the above way, and create a collection first:

$ nft add set inet filter xxx \{ type inet_service \; flags interval \; \}

Add more elements:

$ nft add element inet filter xxx \{ 9000-9005, 9007 \}

To view a collection:

$ nft list set inet filter xxx

table inet filter {
	set xxx {
		type inet_service
		flags interval
		elements = { 9000-9005, 9007 }
	}
}

Now realize the power of nftables collection. It can be interval, a collection of single elements, or mixed. iptables is a hassle.

Release of non describable services:

$ nft add rule inet filter TCP \
   tcp dport @xxx \
   counter \
   accept \
   comment \"Accept xxx\"

4. Processing UDP traffic

In this step, let's deal with UDP traffic, such as the application that can't be described in the above example. Besides TCP port and UDP port, I won't explain the specific use. Let's face Google to find out.

At this stage, even the collection does not need to be created. The collection created before is reused directly, and the UDP data that cannot be described application is released:

$ nft add rule inet filter UDP \
   udp dport @xxx \
   counter \
   accept \
   comment \"Accept xxx\"

View rules:

$ nft list chain inet filter UDP

table inet filter {
	chain UDP {
		udp dport @xxx counter packets 0 bytes 0 accept comment "Accept xxx"
	}
}

Other UDP data can be modularized according to this routine, which is not too pleasant to watch.

In order for the system or nftables to continue to take effect after restart, we need to persist these rules and write them directly to / etc / nftables / INET filter:

$ echo "#! /usr/sbin/nft -f" > /etc/nftables/inet-filter
$ nft list ruleset >> /etc/nftables/inet-filter

To automatically load the nftables service after power on:

$ systemctl enable nftables

5. Log in rsyslog

By default, after logging is turned on, the log will enter syslog directly, which is mixed with the system log and hard to read. The best way to do this is to redirect the nftables logs to separate files.

Taking this article as an example, we only turn on the logging of ct state invalid and SSH. First, create a directory named nftables in the / var/log directory, and create two files named invalid.log and ssh.log to store their own logs.

$ mkdir /var/log/nftables
$ touch /var/log/nftables/{ssh.log,invalid.log}

Make sure rsyslog is installed on the system. Now enter the / etc/rsyslog.d directory and create a file named nftables.conf. Its contents are as follows:

:msg,regex,"Invalid-Input: " -/var/log/nftables/invalid.log
:msg,regex,"New SSH connection: " -/var/log/nftables/ssh.log

Finally, to ensure that the logs are manageable, you need to create an nftables file in / etc/logrotate.d:

$ cat /etc/logrotate.d/nftables

/var/log/nftables/* { rotate 5 daily maxsize 50M missingok notifempty delaycompress compress postrotate invoke-rc.d rsyslog rotate > /dev/null endscript }

When you reconnect to the server through ssh, you can see the log:

$ tail -f /var/log/nftables/ssh.log

Dec 19 17:15:33 [localhost] kernel: New SSH connection: IN=ens192 OUT= MAC=00:50:56:bd:2f:3d:00:50:56:bd:d7:24:08:00 SRC=192.168.57.2 DST=192.168.57.53 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=43312 DF PROTO=TCP SPT=41842 DPT=22 WINDOW=29200 RES=0x00 SYN URGP=0

6. summary

This article shows you how to use nftables to build a simple firewall, and modularize the rule set through the set and dictionary. Later, you can dynamically add port and IP and other elements without modifying the rules. More complex rules will be introduced in the following article. The next article will teach you how to use nftables to prevent DDoS attacks. Please wait.

WeChat official account

Sweep the following two-dimensional code, pay attention to WeChat official account, reply to the official account and join the group of cloud native communication, discuss with cloud, Sun Hongliang, Zhang Guanchang, Yangming and other big guys together.

Tags: Operation & Maintenance ssh iptables firewall network

Posted on Wed, 25 Mar 2020 23:03:46 -0400 by langer