nftables-mode-1.1/ 0000755 0001752 0001753 00000000000 14242773177 012542 5 ustar elpa elpa nftables-mode-1.1/nftables-mode.el 0000644 0001752 0001753 00000027260 14242773176 015612 0 ustar elpa elpa ;;; nftables-mode.el --- Major mode for editing nftables -*- lexical-binding: t -*-
;; Copyright (C) 2021-2022 Free Software Foundation, Inc
;; Author: trentbuck@gmail.com (Trent W. Buck)
;; Maintainer: emacs-devel@gnu.org
;; Version: 1.1
;; Package-Requires: ((emacs "25.1"))
;; Keywords: convenience
;; This package is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This package is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see .
;;; Commentary:
;; This major mode currently only offers basic highlighting and
;; primitive indentation. Contributions very welcome.
;;; Code:
(require 'rx)
(require 'syntax) ; syntax-ppss, for indentation
(require 'smie)
(defvar nftables-mode-map (make-sparse-keymap))
(defvar nftables-mode-hook nil)
(defvar nftables-mode-syntax-table
(let ((table (make-syntax-table)))
(modify-syntax-entry ?# "<\n" table) ; make #comment work
(modify-syntax-entry ?\n ">#" table) ; make #comment work
;; "foo_bar" should always be 2 words (it's a user-notion,
;; controllable via `super/subword-mode', not one that should
;; depend on the major mode).
;; (modify-syntax-entry ?_ "w" table) ; foo_bar is 1 word (not 2)
table))
;; NOTE: I started with the keywords in the nano highlighter, but
;; they were really incomplete. So instead I looked at the
;; flex/bison rules in the nft source code (as at debian/0.9.1-2-2-g3255aaa):
;; https://salsa.debian.org/pkg-netfilter-team/pkg-nftables/blob/master/src/scanner.l
;; https://salsa.debian.org/pkg-netfilter-team/pkg-nftables/blob/master/src/parser_bison.y
;; NOTE: not supporting multi-statement lines "list ruleset; flush ruleset".
;; NOTE: not supporting multi-line statements "list \\\n ruleset".
;; NOTE: not supporting arbitrary whitespace in some places.
;; NOTE: identifiers are hard (e.g. bare addresses, names, quoted strings), so
;; not supporting all those properly.
;; NOTE: family can be omitted; it defaults to "ip" (IPv4 only).
;; I am not supporting that, because you USUALLY want "inet" (IPv4/IPv6 dual-stack).
;; NOTE: there are two main styles, I'm supporting only those and not a mix of same.
;;
;; Style #1:
;;
;; flush ruleset
;; table inet foo {
;; chain bar {
;; type filter hook input priority filter
;; policy drop
;; predicate [counter] [log]
;; }
;; }
;;
;; Style #2 (everything at the "top level"):
;;
;; flush ruleset
;; add table inet foo
;; add chain inet foo bar { type filter hook input priority filter; policy drop }
;; add rule inet foo bar predicate [counter] [log]
(defvar nftables-font-lock-keywords
`(;; include "foo"
;; list ruleset
;; flush ruleset
(,(rx bol
(or "include"
"list ruleset"
"flush ruleset"
"list tables"
"list counters"
"list quotas")
symbol-end)
. font-lock-preprocessor-face)
;; define foo = bar
;; define foo = { bar, baz }
;; redefine foo = bar
;; undefine foo
(,(rx bol
(group (or "define" "redefine" "undefine"))
" "
(group (one-or-more (any alnum ?_)))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-variable-name-face))
;; add table inet my_table { ... }
;; table inet my_table { ... }
(,(rx bol
(group (or "table" ; style #1
"add table")) ; style #2
" "
;; This is parser_bison.y:family_spec
(group (or "ip" "ip6" "inet" "arp" "bridge" "netdev"))
" "
(group (one-or-more (any alnum ?_)))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-constant-face)
(3 font-lock-variable-name-face))
;; chain my_chain {
;; set my_set {
;; map my_map {
(,(rx bol
(one-or-more blank)
(group (or "chain" "set" "map"))
" "
(group (one-or-more (any alnum ?_))))
(1 font-lock-type-face)
(2 font-lock-variable-name-face))
;; add chain inet my_table my_chain { ... }
;; add set inet my_table my_set { ... }
;; add map inet my_table my_map { ... }
;; add rule inet my_table my_chain ...
;; add element inet my_table my_set { ... }
;; add element inet my_table my_map { ... }
(,(rx bol
(group "add "
(or "chain" "set" "map" "rule" "element"))
" "
(group (or "ip" "ip6" "inet" "arp" "bridge" "netdev"))
" "
(group (one-or-more (any alnum ?_)))
" "
(group (one-or-more (any alnum ?_)))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-constant-face)
(3 font-lock-variable-name-face)
(4 font-lock-variable-name-face))
;; Remaining rules not anchored at beginning-of-line.
;; << chain specification >>
;; { type filter hook input priority filter; }
(,(rx symbol-start
(group "type")
" "
(group (or "filter" "nat" "route"))
" "
(group "hook")
" "
(group (or "prerouting"
"input"
"forward"
"output"
"postrouting"
"ingress"
"dormant"))
" "
(group "priority")
" "
(group (or (and (opt "-") (one-or-more digit))
"raw"
"mangle"
"dstnat"
"filter"
"security"
"srcnat"
"dstnat"
"filter"
"out"
"srcnat"))
symbol-end)
(1 font-lock-type-face)
(3 font-lock-type-face)
(5 font-lock-type-face)
(2 font-lock-constant-face)
(4 font-lock-constant-face)
(6 font-lock-constant-face))
;; << Table 8. Set specifications >>
;; type x # set
;; type x : y # map
;; flags x , y , z # set/map
;; timeout 60s # set
;; gc-interval 12s # set
;; elements = { ... } # set/map
;; size 1000 # set/map
;; auto-merge # set
(,(rx symbol-start
(group "type")
" "
(group (or "ipv4_addr" "ipv6_addr" "ether_addr" "inet_proto" "inet_service" "mark"))
(optional
" : "
(group (or "ipv4_addr" "ipv6_addr" "ether_addr" "inet_proto" "inet_service" "mark" "counter" "quota")))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-constant-face))
(,(rx symbol-start
(group "flags")
" "
(group
(or "constant" "dynamic" "interval" "timeout")
(zero-or-more
", "
(or "constant" "dynamic" "interval" "timeout")))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-constant-face))
(,(rx symbol-start
(group (or "timeout" "gc-interval"))
" "
(group ; copied from scanner.l
(optional (one-or-more digit) "d")
(optional (one-or-more digit) "h")
(optional (one-or-more digit) "m")
(optional (one-or-more digit) "s")
(optional (one-or-more digit) "ms"))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-string-face))
(,(rx symbol-start
(group "size")
" "
(group (one-or-more digit))
symbol-end)
(1 font-lock-type-face)
(2 font-lock-string-face))
(,(rx symbol-start
"auto-merge"
symbol-end)
. font-lock-type-face)
(,(rx symbol-start
(group "elements")
" = "
symbol-end)
(1 font-lock-type-face))
;; policy accept
;; policy drop
(,(rx (group "policy") " " (group (or "accept" "drop")))
(1 font-lock-type-face)
(2 font-lock-function-name-face))
;; $variable
;; @array
(,(rx (or "@" "$")
alpha
(zero-or-more (any alnum ?_)))
. font-lock-variable-name-face)
;; Simplified because scanner.l is INSANE for IPv6.
;; 1234 (e.g. port number)
;; 1.2.3.4
;; ::1
(,(rx symbol-start
(or
;; IPv4 address (optional CIDR)
(and digit
(zero-or-more (any digit "."))
digit
(optional "/" (one-or-more digit)))
;; IPv6 address (optional CIDR)
;; Oops, this was matching "add"!
;; WOW THIS IS REALLY REALLY HARD!
(and (zero-or-more (or (and (repeat 1 4 hex-digit) ":")
"::"))
(repeat 1 4 hex-digit)
(optional "/" (one-or-more digit)))
;; Bare digits.
;; Has to be after IPv4 address, or IPv4 address loses.
;; (or (one-or-more digit))
)
symbol-end)
. font-lock-string-face)
;; parser_bison.y:family_spec_explicit
;; (,(rx symbol-start (or "ip" "ip6" "inet" "arp" "bridge" "netdev")
;; symbol-end)
;; . font-lock-constant-face)
;; parser_bison.y:verdict_expr
(,(rx symbol-start (or "accept" "drop" "continue" "return") symbol-end)
. font-lock-function-name-face)
(,(rx symbol-start (group (or "jump" "goto"))
" "
(group (one-or-more (any alnum ?_)))) ; chain_expr
(1 font-lock-function-name-face)
(2 font-lock-variable-name-face))))
;; Based on equivalent for other editors:
;; * /usr/share/nano/nftables.nanorc
;; * https://github.com/nfnty/vim-nftables
;;;###autoload
(define-derived-mode nftables-mode prog-mode "nft"
"Major mode to edit nftables files."
(setq-local comment-start "#")
(setq-local font-lock-defaults
`(nftables-font-lock-keywords nil nil))
;; ;; make "table my_table {" result in indents on the next line.
;; (setq-local electric-indent-chars ?\})
(setq-local indent-line-function #'nftables-indent-line)
;; Let's not override `tab-width' since I can't see any place where the
;; language documents it as having any particular size.
;;(setq-local tab-width 4)
)
(defvar nftables-indent-basic nil
"Basic indentation step size.
If nil, use `smie-indent-basic'."
;; :type '(choice (const nil) integer)
)
;; Stolen from parsnip's (bradyt's) dart-mode.
;; https://github.com/bradyt/dart-mode/blob/199709f7/dart-mode.el#L315
(defun nftables-indent-line ()
(let (old-point)
(save-excursion
(back-to-indentation)
(let ((depth (car (syntax-ppss))))
(when (= ?\) (char-syntax (char-after)))
(setq depth (1- depth)))
(indent-line-to (* depth (or nftables-indent-basic smie-indent-basic))))
(setq old-point (point)))
(when (< (point) old-point)
(back-to-indentation))))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.nft\\(?:ables\\)?\\'" . nftables-mode))
;;;###autoload
(add-to-list 'auto-mode-alist '("/etc/nftables.conf" . nftables-mode))
;;;###autoload
(add-to-list 'interpreter-mode-alist '("nft\\(?:ables\\)?" . nftables-mode))
(provide 'nftables-mode)
;;; nftables-mode.el ends here.
nftables-mode-1.1/tests/ 0000755 0001752 0001753 00000000000 14242773176 013703 5 ustar elpa elpa nftables-mode-1.1/tests/nftables-mode-test.nft 0000644 0001752 0001753 00000071333 14242773176 020120 0 ustar elpa elpa #!/sbin/nft -f
# This is not a real ruleset.
# This is just testing Emacs's syntax highlighting.
$foo
@bar
######################################################################
# EXAMPLE RULESETS FROM https://wiki.nftables.org/
######################################################################
table inet filter {
chain input {
type filter hook input priority 0;
# accept any localhost traffic
iif lo accept
# accept traffic originated from us
ct state established,related accept
# accept neighbour discovery otherwise connectivity breaks
ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept
# count and drop any other traffic
counter drop
}
}
# FLOWTABLE EXAMPLE (NOTE: 0.9 flowtable, not the unrelated 0.8 thing also called flowtable)
table inet x {
flowtable f {
hook ingress priority 0 devices = { eth0, eth1 };
}
chain y {
type filter hook forward priority 0; policy accept;
ip protocol tcp flow offload @f
counter packets 0 bytes 0
}
}
# UPDATING A SET FROM ANOTHER CHAIN (a la iptables -m recent?)
table ip filter {
set myset {
type inet_service
flags timeout
elements = { http expires 9s }
}
chain input {
type filter hook input priority 0; policy accept;
update @myset { tcp dport timeout 1m }
}
}
table ip filter {
set myset {
type ipv4_addr
elements = { 1.1.1.1 }
}
chain input {
type filter hook input priority 0; policy accept;
add @myset { ip saddr }
}
}
add rule bridge filter forward ether type ip tcp dport 22 accept
add rule bridge filter forward ether type arp accept
add rule inet nat prerouting dnat tcp dport map { 1000 : 1.1.1.1, 2000 : 2.2.2.2, 3000 : 3.3.3.3} : tcp dport map { 1000 : 1234, 2000 : 2345, 3000 : 3456 }
add rule inet nat postrouting snat ip saddr map { 192.168.1.1 : 1.1.1.1, 192.168.2.2 : 2.2.2.2, 192.168.3.3 : 3.3.3.3 }
flush ruleset
include "./defines.nft"
table inet filter {
chain global {
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
udp dport 53 accept
}
include "./inet-filter-sets.nft"
include "./inet-filter-forward.nft"
include "./inet-filter-local.nft"
}
# interfaces
define nic_inet = bond0
define nic_dmz = bond1
define nic_lan = bond2
# network ranks
define net_ipv4_dmz = 10.0.1.0/24
define net_ipv6_dmz = fe00:1::/64
define net_ipv4_lan = 10.0.2.0/24
define net_ipv6_lan = fe00:2::/64
# some machines
define server1_ipv4 = 10.0.1.2
define server1_ipv6 = fe00:1::2
define workstation1_ipv4 = 10.0.2.2
define workstation1_ipv6 = fe00:2::2
set myset_ipv4 {
type ipv4_addr;
elements = { $server1_ipv4 , $workstation1_ipv4 }
}
set myset_ipv6 {
type ipv6_addr;
elements = { $server1_ipv6 , $workstation1_ipv6 }
}
chain dmz_in {
# your rules for traffic to your dmz servers
ip saddr @myset_ipv4
ip6 saddr @myset_ipv6
}
chain dmz_out {
# your rules for traffic from the dmz to internet
}
chain lan_in {
# your rules for traffic to your LAN nodes
}
chain lan_out {
# your rules for traffic from the LAN to the internet
}
chain forward {
type filter hook forward priority 0; policy drop;
jump global
oifname vmap { $nic_dmz : jump dmz_in , $nic_lan : jump lan_in }
oifname $nic_inet iifname vmap { $nic_dmz : jump dmz_out , $nic_lan : jump lan_out }
}
chain input {
type filter hook input priority 0 ; policy drop;
jump global
# your rules for traffic to the firewall here
}
chain output {
type filter hook output priority 0 ; policy drop;
jump global
# your rules for traffic originated from the firewall itself here
}
flush ruleset
table ip Inet4 {
set Knocked_1 {
type ipv4_addr
flags timeout, interval
timeout 10s
gc-interval 4s
}
set Knocked_2 {
type ipv4_addr
flags timeout
timeout 10s
gc-interval 4s
}
set Knocked_3 {
type ipv4_addr
flags timeout
timeout 10s
gc-interval 4s
}
set Knocked_4 {
type ipv4_addr
flags timeout
timeout 2m
gc-interval 4s
}
chain Knock_1 {
set add ip saddr @Knocked_1
}
chain Unknock_1 {
set update ip saddr timeout 0s @Knocked_1
}
chain Knock_2 {
set update ip saddr timeout 0s @Knocked_1
set add ip saddr @Knocked_2
}
chain Unknock_2 {
set update ip saddr timeout 0s @Knocked_2
}
chain Knock_3 {
set update ip saddr timeout 0s @Knocked_2
set add ip saddr @Knocked_3
}
chain Unknock_3 {
set update ip saddr timeout 0s @Knocked_3
}
chain Knock_4 {
set update ip saddr timeout 0s @Knocked_3
set add ip saddr @Knocked_4 log prefix "Port-Knock accepted: "
}
chain RefreshKnock {
set update ip saddr timeout 2m @Knocked_4
}
chain PortKnock {
ct state new ip saddr @Knocked_4 goto RefreshKnock
tcp dport 456 ct state new ip saddr @Knocked_3 goto Knock_4
tcp dport 345 ct state new ip saddr @Knocked_3 return
ip saddr @Knocked_3 ct state new goto Unknock_3
tcp dport 345 ct state new ip saddr @Knocked_2 goto Knock_3
tcp dport 234 ct state new ip saddr @Knocked_2 return
ip saddr @Knocked_2 ct state new goto Unknock_2
tcp dport 234 ct state new ip saddr @Knocked_1 goto Knock_2
tcp dport 123 ct state new ip saddr @Knocked_1 return
ip saddr @Knocked_1 ct state new goto Unknock_1
tcp dport 123 ct state new goto Knock_1
}
chain FilterIn {
type filter hook input priority 0
policy drop
# allow established/related connections
ct state established,related accept
# early drop of invalid connections
ct state invalid drop
# allow from loopback
meta iif lo accept
# allow icmp
ip protocol icmp accept
# port-knocking
jump PortKnock
# misc. filtering
# ...
}
chain FilterOut {
type filter hook output priority 0
policy accept
}
}
table ip filter {
map subnet_map {
type ipv4_addr : verdict
flags interval
elements = { 10.20.255.48/29 : goto group_114, 10.20.255.88/29 : goto group_114,
10.20.255.128/29 : goto group_114 }
}
set priority_set {
type ipv4_addr
flags interval
elements = { 8.8.8.8, 8.8.4.4 }
}
map group_114 {
type ipv4_addr : classid
flags interval
elements = { 10.20.255.50 : 1:ffd8, 10.20.255.90 : 1:ffd5,
10.20.255.130 : 1:ffd2 }
}
map group_114_prio {
type ipv4_addr : classid
flags interval
elements = { 10.20.255.50 : 1:ffd9, 10.20.255.90 : 1:ffd6,
10.20.255.130 : 1:ffd3 }
}
chain forward {
type filter hook forward priority filter; policy accept;
meta priority none ip daddr vmap @subnet_map counter packets 0 bytes 0
meta priority none ip saddr vmap @subnet_map counter packets 0 bytes 0
ip daddr 192.168.0.0/16 meta priority none meta priority set 1:ffff counter packets 0 bytes 0 log prefix "total - "
ip saddr 192.168.0.0/16 meta priority none meta priority set 1:ffff counter packets 0 bytes 0 log prefix "total - "
ip daddr 10.0.0.0/8 meta priority none meta priority set 1:ffff counter packets 38931 bytes 2926076 log prefix "total - "
ip saddr 10.0.0.0/8 meta priority none meta priority set 1:ffff counter packets 14 bytes 1064 log prefix "total - "
meta priority none meta priority set 1:2 counter packets 0 bytes 0 log prefix "non_shaped - "
}
chain input {
type filter hook input priority filter; policy accept;
meta priority none meta priority set 1:2 counter packets 419381 bytes 45041195
}
chain output {
type filter hook output priority filter; policy accept;
meta priority none meta priority set 1:2 counter packets 507779 bytes 51809859
}
chain group_114 {
meta priority none ip saddr @priority_set meta priority set ip daddr map @group_114_prio counter packets 0 bytes 0
meta priority none ip daddr @priority_set meta priority set ip saddr map @group_114_prio counter packets 0 bytes 0
meta priority none meta priority set ip daddr map @group_114 counter packets 0 bytes 0
meta priority none meta priority set ip saddr map @group_114 counter packets 0 bytes 0
meta priority none meta priority set 1:ffff counter packets 0 bytes 0 log prefix "group_114 - "
}
}
add table ip filter
add chain ip filter forward { type filter hook forward priority 0; policy accept; }
add map ip filter subnet_map { type ipv4_addr : verdict; flags interval; }
add set ip filter priority_set { type ipv4_addr; flags interval; }
add element ip filter priority_set {8.8.8.8 }
add element ip filter priority_set {8.8.4.4 }
add rule ip filter forward meta priority 0 ip daddr vmap @subnet_map counter
add rule ip filter forward meta priority 0 ip saddr vmap @subnet_map counter
add rule ip filter forward ip daddr 192.168.0.0/16 meta priority 0 meta priority set "1:0xffff" counter log prefix "total - "
add rule ip filter forward ip saddr 192.168.0.0/16 meta priority 0 meta priority set "1:0xffff" counter log prefix "total - "
add rule ip filter forward ip daddr 10.0.0.0/8 meta priority 0 meta priority set "1:0xffff" counter log prefix "total - "
add rule ip filter forward ip saddr 10.0.0.0/8 meta priority 0 meta priority set "1:0xffff" counter log prefix "total - "
add rule ip filter forward meta priority 0 meta priority set "1:0x2" counter log prefix "non_shaped - "
add chain ip filter input { type filter hook input priority 0; policy accept; }
add rule ip filter input meta priority 0 meta priority set "1:0x2" counter
add chain ip filter output { type filter hook output priority 0; policy accept; }
add rule ip filter output meta priority 0 meta priority set "1:0x2" counter
add chain ip filter group_114
add map ip filter group_114 { type ipv4_addr : classid; flags interval; }
add map ip filter group_114_prio { type ipv4_addr : classid; flags interval; }
add rule ip filter group_114 meta priority 0 ip saddr @priority_set meta priority set ip daddr map @group_114_prio counter
add rule ip filter group_114 meta priority 0 ip daddr @priority_set meta priority set ip saddr map @group_114_prio counter
add rule ip filter group_114 meta priority 0 meta priority set ip daddr map @group_114 counter
add rule ip filter group_114 meta priority 0 meta priority set ip saddr map @group_114 counter
add rule ip filter group_114 meta priority 0 meta priority set "1:0xffff" counter log prefix "group_114 - "
add element ip filter subnet_map { 10.20.255.48/29 : goto group_114 }
add element ip filter subnet_map { 10.20.255.88/29 : goto group_114 }
add element ip filter subnet_map { 10.20.255.128/29 : goto group_114 }
add element ip filter group_114_prio { 10.20.255.50/32 : "1:0xffd9" }
add element ip filter group_114 { 10.20.255.50/32 : "1:0xffd8" }
add element ip filter group_114_prio { 10.20.255.90/32 : "1:0xffd6" }
add element ip filter group_114 { 10.20.255.90/32 : "1:0xffd5" }
add element ip filter group_114_prio { 10.20.255.130/32 : "1:0xffd3" }
add element ip filter group_114 { 10.20.255.130/32 : "1:0xffd2" }
# packet passing through server
chain forward {
# hook forward does the magic, not the name of the chain
# priority filter can be used in newer versions of nftables > 0.9.0
type filter hook forward priority filter; policy accept;
# packet is matched against subnet_map - it is verdict map = 10.20.255.48/29 : goto group_114
meta priority none ip daddr vmap @subnet_map counter packets 0 bytes 0 # packet's dst address is looked up
# it contains decision on where to send the packet for further processing when matched - chain group_114
meta priority none ip saddr vmap @subnet_map counter packets 0 bytes 0 # packet's src address is looked up
# private destination subnet without set priority is set to 1:0xffff
ip daddr 192.168.0.0/16 meta priority none meta priority set 1:ffff counter packets 0 bytes 0 log prefix "total - "
# private source subnet without set priority is set to 1:0xffff
ip saddr 192.168.0.0/16 meta priority none meta priority set 1:ffff counter packets 0 bytes 0 log prefix "total - "
ip daddr 10.0.0.0/8 meta priority none meta priority set 1:ffff counter packets 38931 bytes 2926076 log prefix "total - "
ip saddr 10.0.0.0/8 meta priority none meta priority set 1:ffff counter packets 14 bytes 1064 log prefix "total - "
# rest of traffic is sent to separate tc class object
meta priority none meta priority set 1:2 counter packets 0 bytes 0 log prefix "non_shaped - "
}
# subnet_map redirected the packet here
chain group_114 {
# packet's source / destination address is matched against set named priority_set and it can't contain any priority set
meta priority none ip saddr @priority_set meta priority set ip daddr map @group_114_prio counter packets 0 bytes 0
# when matched it compares destination address of the packet against group_114_prio map and sets the priority accordingly - 1:ffd9
meta priority none ip daddr @priority_set meta priority set ip saddr map @group_114_prio counter packets 0 bytes 0
# packets heading / originating to / from non prioritized addresses are matched in next steps
meta priority none meta priority set ip daddr map @group_114 counter packets 0 bytes 0
meta priority none meta priority set ip saddr map @group_114 counter packets 0 bytes 0
# unknown traffic is set to untracked object - 1:0xffff
meta priority none meta priority set 1:ffff counter packets 0 bytes 0 log prefix "group_114 - "
}
map group_114 {
type ipv4_addr : classid
flags interval
elements = { 10.20.255.50 : 1:ffd8, 10.20.255.90 : 1:ffd5,
10.20.255.130 : 1:ffd2 }
}
map group_114_prio {
type ipv4_addr : classid
flags interval
elements = { 10.20.255.50 : 1:ffd9, 10.20.255.90 : 1:ffd6,
10.20.255.130 : 1:ffd3 }
}
######################################################################
# EXAMPLE STATEMENTS FROM THE MANPAGE
######################################################################
list ruleset
flush ruleset
list ruleset ip
flush ruleset ip6
table my_table { ... }
table arp my_table { ... }
add table my_table { ... }
add table arp my_table { ... }
create table my_table { ... }
create table arp my_table { ... }
delete table my_table
delete table arp my_table
list table my_table
list table arp my_table
flush table my_table
flush table arp my_table
list tables
delete table handle 1234
delete table arp handle 1234
create table inet mytable
add chain inet mytable myin { type filter hook input priority 0; }
add rule inet mytable myin counter
add table inet mytable { flags dormant; }
add table inet mytable
chain my_table my_chain { type filter hook input priority filter }
# {add | create} chain [family] table chain [{ type type hook hook [device device] priority priority ; [policy policy ;] }]
# {delete | list | flush} chain [family] table chain
# list chains
# delete chain [family] table handle handle
# rename chain [family] table chain newname
add rule filter output ip daddr 192.168.0.0/24 accept # 'ip filter' is assumed
# same command, slightly more verbose
add rule ip filter output ip daddr 192.168.0.0/24 accept
# nft -a list ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
ct state established,related accept # handle 4
ip saddr 10.1.1.1 tcp dport ssh accept # handle 5
...
}
}
# delete the rule with handle 5
# nft delete rule inet filter input handle 5
add rule inet filter input ip saddr { 10.0.0.0/8, 192.168.0.0/16 } tcp dport { 22, 443 } accept
add rule inet filter input ip saddr @allowed_hosts tcp dport @allowed_ports accept
# add set [family] table set { type type ; [flags flags ;] [timeout timeout ;] [gc-interval gc-interval ;] [elements = { element[, ...] } ;] [size size ;] [policy policy ;] [auto-merge ;] }
# {delete | list | flush} set [family] table set
# list sets
# delete set [family] table handle handle
# {add | delete} element [family] table set { element[, ...] }
# add map [family] table map { type type [flags flags ;] [elements = { element[, ...] } ;] [size size ;] [policy policy ;] }
# {delete | list | flush} map [family] table map
# list maps
# {add | delete} element [family] table map { elements = { element[, ...] } ; }
# {add | create} flowtable [family] table flowtable { hook hook priority priority ; devices = { device[, ...] } ; }
# {delete | list} flowtable [family] table flowtable
# {add | delete | list | reset} type [family] table object
# delete type [family] table handle handle
# list counters
# list quotas
# ct helper helper { type type protocol protocol ; [l3proto family ;] }
table inet myhelpers {
ct helper ftp-standard {
type "ftp" protocol tcp
}
chain prerouting {
type filter hook prerouting priority 0;
tcp dport 21 ct helper set "ftp-standard"
}
}
# ct timeout name { protocol protocol ; policy = { state: value [, ...] } ; [l3proto family ;] }
table ip filter {
ct timeout customtimeout {
protocol tcp;
l3proto ip
policy = { established: 120, close: 20 }
}
chain output {
type filter hook output priority filter; policy accept;
ct timeout set "customtimeout"
}
}
# counter [packets bytes]
# quota [over | until] [used]
# describe expression
describe tcp flags
# Interface name
filter input iifname eth0
# Weird interface name
filter input iifname "(eth0)"
# Ethernet destination MAC address
filter input ether daddr 20:c9:d0:43:12:d9
# dotted decimal notation
filter output ip daddr 127.0.0.1
# host name
filter output ip daddr localhost
# abbreviated loopback address
filter output ip6 daddr ::1
# without [] the port number (22) would be parsed as part of the
# ipv6 address
ip6 nat prerouting tcp dport 2222 dnat to [1ce::d0]:22
# match if route exists
filter input fib daddr . iif oif exists
# match only non-fragmented packets in IPv6 traffic
filter input exthdr frag missing
# match if TCP timestamp option is present
filter input tcp option timestamp exists
# match ping packets
filter output icmp type { echo-request, echo-reply }
# match ICMPv6 ping packets
filter output icmpv6 type { echo-request, echo-reply }
# meta {length | nfproto | l4proto | protocol | priority}
# [meta] {mark | iif | iifname | iiftype | oif | oifname | oiftype | skuid | skgid | nftrace | rtclassid | ibrname | obrname | pkttype | cpu | iifgroup | oifgroup | cgroup | random | ipsec | iifkind | oifkind}
filter input meta iif "foo"
# qualified meta expression
filter output meta oif eth0
# unqualified meta expression
filter output oif eth0
# packet was subject to ipsec processing
raw prerouting meta ipsec exists accept
# socket {transparent | mark}
# Mark packets that correspond to a transparent socket
table inet x {
chain y {
type filter hook prerouting priority -150; policy accept;
socket transparent 1 mark set 0x00000001 accept
}
}
# Trace packets that corresponds to a socket with a mark value of 15
table inet x {
chain y {
type filter hook prerouting priority -150; policy accept;
socket mark 0x0000000f nftrace set 1
}
}
# Set packet mark to socket mark
table inet x {
chain y {
type filter hook prerouting priority -150; policy accept;
tcp dport 8080 mark set socket mark
}
}
# osf [ttl {loose | skip}] {name | version}
# Accept packets that match the "Linux" OS genre signature without comparing TTL.
table inet x {
chain y {
type filter hook input priority 0; policy accept;
osf ttl skip name "Linux"
}
}
# fib {saddr | daddr | mark | iif | oif} [. ...] {oif | oifname | type}
# drop packets without a reverse path
filter prerouting fib saddr . iif oif missing drop
# drop packets to address not configured on ininterface
filter prerouting fib daddr . iif type != { local, broadcast, multicast } drop
# perform lookup in a specific 'blackhole' table (0xdead, needs ip appropriate ip rule)
filter prerouting meta mark set 0xdead fib daddr . mark type vmap { blackhole : drop, prohibit : jump prohibited, unreachable : drop }
# rt [ip | ip6] {classid | nexthop | mtu | ipsec}
# IP family independent rt expression
filter output rt classid 10
filter output rt ipsec missing
# IP family dependent rt expressions
ip filter output rt nexthop 192.168.0.1
ip6 filter output rt nexthop fd00::1
inet filter output rt ip nexthop 192.168.0.1
inet filter output rt ip6 nexthop fd00::1
# ipsec {in | out} [ spnum NUM ] {reqid | spi}
# ipsec {in | out} [ spnum NUM ] {ip | ip6} {saddr | daddr}
# ether {daddr | saddr | type}
# vlan {id | cfi | pcp | type}
# arp {htype | ptype | hlen | plen | operation | saddr { ip | ether } | daddr { ip | ether }
# ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
# icmp {type | code | checksum | id | sequence | gateway | mtu}
# igmp {type | mrt | checksum | group}
# ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}
# matching if first extension header indicates a fragment
ip6 nexthdr ipv6-frag
# icmpv6 {type | code | checksum | parameter-problem | packet-too-big | id | sequence | max-delay}
# tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}
# udp {sport | dport | length | checksum}
# udplite {sport | dport | checksum}
# sctp {sport | dport | vtag | checksum}
# dccp {sport | dport}
# ah {nexthdr | hdrlength | reserved | spi | sequence}
# esp {spi | sequence}
# comp {nexthdr | flags | cpi}
# @base,offset,length
# Matching destination port of both UDP and TCP.
inet filter input meta l4proto {tcp, udp} @th,16,16 { 53, 80 }
# Rewrite arp packet target hardware address if target protocol address
# matches a given address.
input meta iifname enp2s0 arp ptype 0x0800 arp htype 1 arp hlen 6 arp plen 4 @nh,192,32 0xc0a88f10 @nh,144,48 set 0x112233445566 accept
# hbh {nexthdr | hdrlength}
# frag {nexthdr | frag-off | more-fragments | id}
# rt {nexthdr | hdrlength | type | seg-left}
# dst {nexthdr | hdrlength}
# mh {nexthdr | hdrlength | checksum | type}
# srh {flags | tag | sid | seg-left}
# tcp option {eol | noop | maxseg | window | sack-permitted | sack | sack0 | sack1 | sack2 | sack3 | timestamp} tcp_option_field
# exthdr {hbh | frag | rt | dst | mh}
# tcp option {eol | noop | maxseg | window | sack-permitted | sack | sack0 | sack1 | sack2 | sack3 | timestamp}
filter input tcp option sack-permitted kind 1 counter
ip6 filter input frag more-fragments 1 counter
# ct {state | direction | status | mark | expiration | helper | label}
# ct [original | reply] {l3proto | protocol | bytes | packets | avgpkt | zone}
# ct {original | reply} {proto-src | proto-dst}
# ct {original | reply} {ip | ip6} {saddr | daddr}
# restrict the number of parallel connections to a server.
filter input tcp dport 22 meter test { ip saddr ct count over 2 } reject
# {accept | drop | queue | continue | return}
# {jump | goto} chain
# process packets from eth0 and the internal network in from_lan
# chain, drop all packets from eth0 with different source addresses.
filter input iif eth0 ip saddr 192.168.0.0/24 jump from_lan
filter input iif eth0 drop
# payload_expression set value
# route some packets instead of bridging.
# redirect tcp:http from 192.160.0.0/16 to local machine for routing instead of bridging
# assumes 00:11:22:33:44:55 is local MAC address.
bridge input meta iif eth0 ip saddr 192.168.0.0/16 tcp dport 80 meta pkttype set unicast ether daddr set 00:11:22:33:44:55
# Set IPv4 DSCP header field.
ip forward ip dscp set 42
# extension_header_expression set value
tcp flags syn tcp option maxseg size set 1360
# set a size based on route information:
tcp flags syn tcp option maxseg size set rt mtu
# log the UID which generated the packet and ip options
ip filter output log flags skuid flags ip options
# log the tcp sequence numbers and tcp options from the TCP packet
ip filter output log flags tcp sequence,options
# enable all supported log flags
ip6 filter output log flags all
# counter packets number bytes number
# counter { packets number | bytes number }
# save packet nfmark in conntrack.
ct mark set meta mark
# set zone mapped via interface.
table inet raw {
chain prerouting {
type filter hook prerouting priority -300;
ct zone set iif map { "eth1" : 1, "veth1" : 2 }
}
chain output {
type filter hook output priority -300;
ct zone set oif map { "eth1" : 1, "veth1" : 2 }
}
}
# restrict events reported by ctnetlink.
ct event set new,related,destroy
# meta {mark | priority | pkttype | nftrace} set value
# limit rate [over] packet_number / TIME_UNIT [burst packet_number packets]
# limit rate [over] byte_number BYTE_UNIT / TIME_UNIT [burst byte_number BYTE_UNIT]
# TIME_UNIT := second | minute | hour | day
# BYTE_UNIT := bytes | kbytes | mbytes
# create a suitable table/chain setup for all further examples
add table nat
add chain nat prerouting { type nat hook prerouting priority 0; }
add chain nat postrouting { type nat hook postrouting priority 100; }
# translate source addresses of all packets leaving via eth0 to address 1.2.3.4
add rule nat postrouting oif eth0 snat to 1.2.3.4
# redirect all traffic entering via eth0 to destination address 192.168.1.120
add rule nat prerouting iif eth0 dnat to 192.168.1.120
# translate source addresses of all packets leaving via eth0 to whatever
# locally generated packets would use as source to reach the same destination
add rule nat postrouting oif eth0 masquerade
# redirect incoming TCP traffic for port 22 to port 2222
add rule nat prerouting tcp dport 22 redirect to :2222
# inet family:
# handle ip dnat:
add rule inet nat prerouting dnat ip to 10.0.2.99
# handle ip6 dnat:
add rule inet nat prerouting dnat ip6 to fe80::dead
# this masquerades both ipv4 and ipv6:
add rule inet nat postrouting meta oif ppp0 masquerade
# Example ruleset for tproxy statement.
table ip x {
chain y {
type filter hook prerouting priority -150; policy accept;
tcp dport ntp tproxy to 1.1.1.1
udp dport ssh tproxy to :2222
}
}
table ip6 x {
chain y {
type filter hook prerouting priority -150; policy accept;
tcp dport ntp tproxy to [dead::beef]
udp dport ssh tproxy to :2222
}
}
table inet x {
chain y {
type filter hook prerouting priority -150; policy accept;
tcp dport 321 tproxy to :ssh
tcp dport 99 tproxy ip to 1.1.1.1:999
udp dport 155 tproxy ip6 to [dead::beef]:smux
}
}
flow add @flowtable
# send to machine with ip address 10.2.3.4 on eth0
ip filter forward dup to 10.2.3.4 device "eth0"
# copy raw frame to another interface
netdetv ingress dup to "eth0"
dup to "eth0"
# combine with map dst addr to gateways
dup to ip daddr map { 192.168.7.1 : "eth0", 192.168.7.2 : "eth1" }
fwd to device
# Example for simple blacklist.
# declare a set, bound to table "filter", in family "ip". Timeout and size are mandatory because we will add elements from packet path.
add set ip filter blackhole "{ type ipv4_addr; flags timeout; size 65536; }"
# whitelist internal interface.
add rule ip filter input meta iifname "internal" accept
# drop packets coming from blacklisted ip addresses.
add rule ip filter input ip saddr @blackhole counter drop
# add source ip addresses to the blacklist if more than 10 tcp connection requests occurred per second and ip address.
# entries will timeout after one minute, after which they might be re-added if limit condition persists.
add rule ip filter input tcp flags syn tcp dport ssh meter flood size 128000 { ip saddr timeout 10s limit rate over 10/second} add @blackhole { ip saddr timeout 1m } drop
# inspect state of the rate limit meter:
list meter ip filter flood
# inspect content of blackhole:
list set ip filter blackhole
# manually add two addresses to the set:
add element filter blackhole { 10.2.3.4, 10.23.1.42 }
# select DNAT target based on TCP dport:
# connections to port 80 are redirected to 192.168.1.100,
# connections to port 8888 are redirected to 192.168.1.101
add rule ip nat prerouting dnat tcp dport map { 80 : 192.168.1.100, 8888 : 192.168.1.101 }
# source address based SNAT:
# packets from net 192.168.1.0/24 will appear as originating from 10.0.0.1,
# packets from net 192.168.2.0/24 will appear as originating from 10.0.0.2
add rule ip nat postrouting snat to ip saddr map { 192.168.1.0/24 : 10.0.0.1, 192.168.2.0/24 : 10.0.0.2 }
# jump to different chains depending on layer 4 protocol type:
add rule ip filter input ip protocol vmap { tcp : jump tcp-chain, udp : jump udp-chain , icmp : jump icmp-chain }
monitor ruleset
nftables-mode-1.1/tests/iptab 0000644 0001752 0001753 00000004463 14242773176 014734 0 ustar elpa elpa #!/usr/sbin/iptables-apply
### This -*-conf-*- file is my template /etc/iptab for new hosts.
### Ref. http://jengelh.medozas.de/documents/Perfect_Ruleset.pdf
### From init, use iptables-restore /etc/iptab (NOT iptables-apply).
### Ruleset can and should be loaded BEFORE network ifaces exist.
###
### Named hosts and services names are resolved ONCE, at load time.
### See getent(1). For meaningful ethernet iface names, edit
### /etc/udev/rules.d/*persistent-net.rules and reboot.
######################################################################
## Rulesets (*foo ... COMMIT) load atomically. First load a deny-all
## ruleset so that if the "real" ruleset fails to load, the system
## WILL NOT be left in an allow-all state.
*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT
-A INPUT -s 192.168/16 -j ACCEPT -m comment --comment "Allow recovery from LAN."
-A OUTPUT -p udp --dport domain -j REJECT -m comment --comment "On error, avoid DNS timeout delays"
COMMIT
######################################################################
*filter
:OUTPUT ACCEPT # Local users/processes are trusted.
:INPUT DROP # Ingress policy is "default deny".
:FORWARD DROP # Routing policy is "default deny".
:PRELUDE - # Best practices for filtered chains.
## Quickly handle the essentials of a "default deny" environment.
## Anything left after this chain implicitly has --ctstate NEW.
-A INPUT -j PRELUDE
-A FORWARD -j PRELUDE
-A PRELUDE -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A PRELUDE -m conntrack ! --ctstate NEW -j DROP -m comment --comment "Same as --ctstate INVALID."
-A PRELUDE -p icmp -j ACCEPT
-A PRELUDE -i lo -j ACCEPT
## YOUR RULES GO HERE. Below is a simple example: a firewalling
## router and SSH gateway that also serves DHCP/DNS/NTP to the LAN,
## with a web server "www" and a mail server "mail" behind it.
-A INPUT -p tcp --dport ssh -j ACCEPT
-A INPUT -i lan -p udp -m multiport --dports bootps,domain,ntp -j ACCEPT
-A FORWARD -d mail -p tcp -m multiport --dports smtp,submission,imaps -j ACCEPT
-A FORWARD -d www -p tcp -m multiport --dports http,https -j ACCEPT
## Finally, politely reject all other attempts. Omit these to use the
## chains' default policies (DROP, above) instead.
-A INPUT -j REJECT
-A FORWARD -j REJECT
COMMIT
nftables-mode-1.1/tests/nftables-router.nft 0000644 0001752 0001753 00000102467 14242773176 017542 0 ustar elpa elpa #!/usr/sbin/nft --file
# -*- mode: conf; mode: nftables; conf-space-keywords: "table\\|chain\\|type\\|policy\\|accept\\|drop\\|counter\\|jump"; -*-
#### This -*-nftables-*- ruleset is my /etc/nftables.conf for new hosts.
#### Ref. http://jengelh.medozas.de/documents/Perfect_Ruleset.pdf
#### Ref. https://wiki.nftables.org/
####
#### GOTCHA: This is written for nft 0.9.1.
#### Several options have changed, even since 0.9.0!
####
#### GOTCHA: "nft --file" does not flush by default.
#### That is, it acts like "iptables-restore --noflush".
####
#### GOTCHA: If your ruleset does "flush; add; flush; add",
#### it will actually do "flush; flush; add; add".
#### That is, all the flushes move to the top.
####
#### This makes it impossible to do my old trick of
#### having the ruleset start with a "deny all" policy,
#### which was useful to make sure that if the firewall failed,
#### the OS would lock down the whole system.
#### (See "iptab" for notes about that.)
####
#### To achieve that under nft, you instead need to patch
#### nftables.service to have
#### OnFailure=nftables-denyall.service, and then write that
#### unit to load a SEPARATE, DIFFERENT nftables file that
#### blocks everything. And even then, it won't behave quite
#### the same.
####
#### This also means it is no longer safe to refer to
#### hostnames in this ruleset, safe in the knowledge that
#### they can only be resolved via local /etc/hosts. Because
#### the "deny all" ruleset can't be prepended here, we CANNOT
#### be sure the real ruleset will only resolve hostnames via
#### local sources -- so you might add ones that are only in
#### DNS, and then have the firewall fail to load during early
#### boot -- leaving you with a "working" host, with no
#### firewall!
####
#### GOTCHA: "list ruleset" here will print the ruleset BEFORE it goes through the kernel;
#### "nft list ruleset" later will print the ruleset AFTER it goes through the kernel.
#### The difference is usually like "iptables -p tcp" getting an implied "-m tcp".
#### However, differences MIGHT indicate a bug! Watch out!
####
#### GOTCHA: "table filter" and "chain INPUT" or "chain input" just
#### conventions and have NO MEANING WHATSOEVER. The actual
#### meaning comes from the "type filter hook input priority
#### filter" line.
####
#### GOTCHA: you can't use globs inside a set or map/vmap:
####
#### iifname "ppp*" # works
#### iifname {"ppp*", "en*"} # fails
#### iifname vmap {"ppp*": drop} # fails
####
#### GOTCHA: Service names resolve via nss (/etc/services) only in nft 0.9.1+!
#### For example, "imap" is in /etc/services, but not in nft 0.9.0.
#### In nft 0.9.0, "nft describe tcp dport" will print the internal list.
####
#### GOTCHA: Using variable ("define x = y") is annoying:
####
#### * definition variable names aren't limited to C-style identifiers;
#### "define 4 = 0" is not allowed, but "define ::1 = 0" is.
####
#### * definitions ARE SCOPED to the file, table, or chain.
#### You can define $x differently in two chains, but
#### you can't define a default $x in a table, then
#### "override" it in a single chain.
####
#### * the definition (define x = y) MUST PRECEDE the usage ($x).
####
#### * a round-trip through the kernel will lose the variable
#### names (nft -f + nft list ruleset).
####
#### CORROLLARY: to change the value, you have to reload the ruleset.
####
#### A workaround for most of this is to use a named set, e.g.
####
#### define x = y
#### rule inet a b ip saddr $x log
####
#### becomes
####
#### set inet a b x { type ipv4_addr; elements={y} }
#### rule inet a b ip saddr @x log
####
#### HOWEVER, sets aren't allowed in some places, e.g.
####
#### rule ip a b tcp dport { http, https } dnat to @www
####
#### NOTE: Mixing nft and legacy xtables should MOSTLY Just Work, but
#### is discouraged because of confusion and kernel bugs.
#### In such case, you need to look at "nft list ruleset" **AND** "iptables-legacy-save".
####
#### NOTE: as at systemd v242, "machinectl start my-container" will
#### create a legacy xtables MASQUERADE rule by default.
####
#### NOTE: Only create a chain if you use it.
#### An empty chain is slightly slower than no chain at all.
#### e.g. most hosts don't need an output chain.
####
#### NOTE: iptables ALWAYS counts how many packets/bytes hit every chain and rule.
#### nftables makes this OPT IN, e.g. change "accept" to "counter accept".
#### iptables-save -c would print "[pkts:bytes] -A ...".
#### nftables list ruleset will print "... counter packets 12 bytes 34 ...".
####
#### Since counters are useful during debugging but not production,
#### I have left them out of this example.
####
#### NOTE: "table x" is implicitly "table ip x", which is IPv4 only.
#### If you want dual-stack, say "table inet x".
####
#### NOTE: "iif lo" is resolved at ruleset load time into an interface
#### NUMBER inside the kernel; whereas "iifname lo" remains a
#### string. This means that:
####
#### * "iifname" is ~10 times slower than "iif" for every
#### packet considered (strcmp versus ==).
####
#### * If you load a ruleset with "iif foo" before foo exists,
#### the load will fail, LEAVING YOU UNPROTECTED!
####
#### * If you load a ruleset with "iif foo" and then foo is
#### removed and readded (e.g. ppp0 for a flaky ADSL link),
#### what happens?
####
#### * Rule of thumb: always use "iifname" (not "iif").
####
#### NOTE: instead of using "define dmz = enp12s0f3",
#### give your interface a logical name directly.
#### Then you use the name EVERYWHERE, e.g. "ip -s l show dmz".
####
#### OLD METHOD:
#### $ cat /etc/udev/rules.d/70-persistent-net.rules
#### SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="de:ad:be:ef:ba:be", NAME="internet"
#### SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="de:af:be:ad:de:ed", NAME="dmz"
#### SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="da:bb:ed:fa:ca:de", NAME="byod"
####
#### NEW METHOD (DID NOT WORK WHEN I TRIED IT):
#### $ cat /etc/systemd/network/dmz.link
#### [Match]
#### MACAddress=dead.beef.babe
#### [Link]
#### Name=dmz
#### # Is this line needed?
#### #NamePolicy=
####
#### NOTE: a single rule can match "allow 53/tcp and 53/udp", but
#### it is ugly, so don't do it:
####
#### meta l4proto {tcp, udp} @th,16,16 53 accept comment "accept DNS on UDP/TCP"
####
#### The @th,16,16 goes to the transport header, skips 16 bits, then reads 16 bits.
#### If those bits are equal to 53, the rule matches.
####
#### This relies on (abuses) the fact that TCP dport and UDP dport have the same offset and length.
#### You cannot use "domain", because nft doesn't know we're matching a service number.
# NOTE: this will remove *ALL* tables --- including tables set up by
# other things (e.g. sshguard)!
#
# In theory you can flush just your own rules, e.g.
#
# flush table inet my_filter
#
# FIXME: I tried that, and I got locked out of SSH!
# What it did was remove all the rules, but NOT the chains, so
# the default-deny policy dropped EVERYTHING!!!
#flush ruleset
# This seems to be a viable workaround (NOTE: must do this for each table):
add table inet my_filter # idempotent
delete table inet my_filter # not idempotent
table inet my_filter {
chain my_input {
type filter hook input priority filter
policy drop
jump my_prologue comment "deal with boring conntrack/loopback/ICMP/ICMPv6"
# YOUR RULES HERE.
# NOTE: service names resolve via nss (/etc/hosts) only in nft 0.9.1+!
tcp dport ssh accept
tcp dport smtp reject comment "alpha is null-listed first MX for CCA (antispam measure)."
iifname {lan, dmz, byod} tcp dport domain accept
iifname {lan, dmz, byod} udp dport {domain, ntp, bootps, tftp} accept
jump my_epilogue
}
chain my_forward {
type filter hook forward priority filter
policy drop
jump my_prologue comment "deal with boring conntrack/loopback/ICMP/ICMPv6"
# YOUR RULES HERE.
# If a pwned devices spams the internet,
# your entire network will be blacklisted!
# To avoid this, block outbound SMTP (25/tcp) from non-MTA hosts.
# MSAs (e.g. Outlook) are not affected, because they use submission (587/tcp).
#
# NOTE: this must appear BEFORE "allow all to internet", obviously.
# NOTE: the LANs can still spam the DMZ.
# NOTE: the DMZ can still spam the internet, because
# occasionally someone adds an MTA to the DMZ without telling me.
iifname != dmz oifname internet reject comment "MSAs MUST use submission (not smtp)"
# Example of a router between four networks:
# internet (the internet),
# dmz (internet-facing servers),
# lan (internal servers & managed laptops).
# byod (BYOD phones and laptops)
#
# Due to prologue, we're only handling NEW FLOWS.
# The return traffic is implicitly allowed.
# We want to say something like:
#
# * full access to internet (except port 25?)
# * limited access to dmz ("typical" server ports)
# * everything else implicitly blocked
#
# Ignoring hairpin traffic (iifname = oifname),
# we have 4 P 2 = 12 permutations.
#
# We can write every combination out longhand:
#
# * BONUS: very clear
# * BONUS: can't get rules in "wrong" order (efficiency)
# * BONUS: can't accidentally have overlapping rules (correctness)
# * BONUS: if a new iface appears, it will default deny (correctness)
# * MALUS: too verbose with many networks (5P2 = 20; 6P2 = 30, 7P2 = 42)
# * MALUS: can't use globs for shorthand (e.g. "*" . dmz).
#
# NOTE: the "continue" lines could be omitted, but are harmless.
iifname . oifname vmap {
lan . internet : accept, # all can attack the internet
byod . internet : accept, # all can attack the internet
dmz . internet : accept, # all can attack the internet
lan . dmz : jump my_dmz, # all can attack DMZ (only specific ports)
byod . dmz : jump my_dmz, # all can attack DMZ (only specific ports)
internet . dmz : jump my_dmz, # all can attack DMZ (only specific ports)
lan . byod : accept, # only LAN can attack phones
dmz . byod : continue, # only LAN can attack phones
internet . byod : continue, # only LAN can attack phones
byod . lan : continue, # nobody can attack LAN
dmz . lan : continue, # nobody can attack LAN
internet . lan : continue, # nobody can attack LAN
}
# OR, we can try to be clever, and write individual rules.
# This is shorter, but harder to reason about!
# oifname internet accept
# oifname dmz jump my_dmz
# iifname lan accept
## Allow connections to protected (non DMZ) services.
## FIXME: this is all IPv4 only! We need equivalent rules for IPv6 as well!!!
iifname dmz ip daddr @ldap_servers tcp dport ldaps accept comment "Centralized authentication"
iifname {lan, byod} ip daddr @irc_servers tcp dport ircd accept comment "IRC from laptops"
iifname {dmz, lan, byod} ip daddr @apt_servers tcp dport {http, 3142} accept comment "APT mirror access (3142 = apt-cacher-ng)"
iifname {dmz, lan, byod} ip daddr @log_servers tcp dport 2514 accept comment "RELP (modern syslog)"
iifname {dmz, lan, byod} ip daddr @log_servers udp dport syslog accept comment "*legacy* syslog (inc. wifi APs on BYOD network)"
ip saddr @ssh_servers tcp dport ssh accept comment "SSH *FROM* login gateway to anything else"
ip saddr @www_servers tcp dport https accept comment "HTTPS *FROM* reverse proxy to backend web apps"
jump my_epilogue
}
# This is mostly for transition from omega (one IP per service) to new-omega (one IP for all services).
# When the transition is done, we can flatten all of this down.
set ldap_servers { type ipv4_addr; elements={203.7.155.214, 203.7.155.154} }
set irc_servers { type ipv4_addr; elements={203.7.155.214, 203.7.155.134} }
set apt_servers { type ipv4_addr; elements={203.7.155.214, 203.7.155.153} }
set log_servers { type ipv4_addr; elements={203.7.155.214, 203.7.155.157} }
set ssh_servers { type ipv4_addr; elements={203.7.155.214, 203.7.155.5} }
set www_servers { type ipv4_addr; elements={203.7.155.214, 203.7.155.8} }
# We want output to be "allow all", so we don't even create a chain.
#chain my_output {
# type filter hook output priority filter
# policy accept
#}
# In theory DMZ hosts must fend for themselves;
# in practice their competence is suspect.
# Thus, limit DMZ access to "typical" ports (plus some per-host exceptions).
# Within those typical ports, DMZ hosts still fend for themselves.
chain my_dmz {
tcp dport {domain,ssh,http,https,smtp,submission,imaps} accept
udp dport {domain} accept
# Allow additional special ports, but only to the server that serves them.
# UPDATE: sigh, apparently there is no conntrack helper for mosh, and bulk allowing a giant port range REALLY IS the recommended workaround. --twb, Aug 2019
define russm.example.com = 127.254.254.254
ip daddr $russm.example.com udp dport 60000-61000 accept comment "mosh (FIXME: write nf_conntrack_mosh.ko!)"
}
chain my_prologue {
# Typically 99%+ of packets are part of an already-established flow.
# Allow those first, so we're a fast, stateful firewall.
# After this only "ct state new" (or "ct state untracked") will remain.
# FIXME: is a vmap here better (more efficient) than two separate rules?
# FIXME: {established or related: accept} does not match correctly!
ct state vmap { established: accept, related: accept, invalid: drop }
# FIXME: I HAVE **NO IDEA** IF THIS IS THE RIGHT THING!
#
# NOTE: this rule is only useful if you use port forwarding
# ("dnat to" or "redirect" in a nat prerouting chain).
#
# In xtables, port forwards would look like this:
# -t nat -A PREROUTING -p tcp --dports http,https -j DNAT --dnat-to www
# -t filter -A FORWARD -p tcp --dports http,https -d www -j ACCEPT
# to avoid having to keep those two rules in sync, you could simply do this:
# -t nat -A PREROUTING -p tcp --dports http,https -j DNAT --dnat-to www
# -t filter -A FORWARD --ctstate DNAT -j ACCEPT
#
# I *THINK* the following rule is the nftables equivalent.
ct status dnat accept
# Loopback traffic is needed for e.g. NFS RPC, and for debugging.
# FIXME: is iiftype here better than iif/iifname?
iiftype loopback accept
# Allow *some* kinds of IPv4/ICMP and IPv6/ICMPv6.
# FIXME: are "ip protocol icmp" and "ip6 nexthdr icmpv6" needed?
#
# NOTE: see also "sysctl net.ipv4.icmp_ratelimit=1000".
#ip protocol icmp icmp type vmap @ICMP_policy
#ip6 nexthdr icmpv6 icmpv6 type vmap @ICMPv6_RFC4890_policy
# Simpler version that relies on "ct state" and is PROBABLY good enough.
icmp type echo-request accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
jump my_IPS
}
chain my_epilogue {
# Finally, politely reject all other attempts.
# Omit to use the default policy ("policy drop", above) instead.
iifname internet drop # FIXME: why drop, not reject??
reject
}
## An automated SSH (et al) brute-force blacklist.
##
## The nominal goal is to nerf brute-force password guessing.
## Since I disable password auth, the REAL goal is to reduce the
## amount of spam in my SSH auth log.
##
## (Running SSH on a non-standard port would also work, but
## I want to benefit from ISPs giving preferential QOS to 22/tcp).
##
## 1. if you brute-force port X more than Y times/minute,
## you're blacklisted for Z minutes.
##
## 2. if you are blacklisted and make ANY connection,
## you're blacklisted for Z minutes (i.e. countdown resets).
##
## 3. if you are blacklisted, all your new flows are dropped.
## (We used to TARPIT, to tie up attacker resources.
## That used xtables-addons and isn't supported in nftables 0.9.1.)
##
## Compared to sshguard or fail2ban or DenyHosts:
##
## BONUS: installed on a gateway, protects the entire network.
##
## BONUS: works even when syslogd is down, or /var/log is full, or
## the syslog "access denied" log format changes.
##
## BONUS: works even when sshd (or whatever) is down.
## That is, if the host is off, the gateway will still trigger.
##
## BONUS: works even when sshd (or whatever) is unused.
## If you never even run FTP or RDP, trigger on them!
##
## MALUS: cannot ignore legitimate traffic.
##
## For SSH, you can mitigate this by forcing your users to
## use ControlMaster.
##
## For HTTPS and IMAPS, you're screwed --- those ALWAYS
## make 30+ connections at once (in IMAP's case, because
## IDLE extension sucks).
##
## You can also mitigate this by having a "backdoor" open
## while blacklisted, which adds you to a temporary
## whitelist if you port knock in the right sequence.
##
## The port knock sequence is a pre-shared key to your end
## users, with all the problems that a PSK involves!
##
## MALUS: easy for an attacker to spoof SYNs to block a legitimate user?
## (See port knock mitigation, above)
##
## MALUS: because we run this AFTER "ct state established accept",
## connections that are "in flight" when the ban hits
## are allowed to complete.
##
## This happens in the wild where the attacker makes 100
## SSH connections in 1 second.
##
## The alternative is to run this (relatively expensive)
## check on EVERY packet, instead of once per flow.
##
## You can see the current state of the list with:
##
## nft list set inet my_filter my_IPS_IPv4_blacklist
## nft list set inet my_filter my_IPS_IPv6_blacklist
##
## I recommend:
##
## * this IPS for low-rate (SSH w/ ControlMaster) and unused (FTP, RDP) services,
## on gateways, for flows originating from the internet / upstream.
##
## For a list of ports to (maybe) IPS guard, consider the first N lines of:
##
## sort -rnk3 /usr/share/nmap/nmap-services
##
## * a basic firewall, and sshguard, on every host that runs a relevant service.
## (This includes SSH, so basically everything.)
## This also covers legitimately bursty traffic on imaps.
## Does this cover submission 587/tcp (postfix)?
##
## * EXCEPT, sshguard doesn't do apache or nginx, so fail2ban on the www hosts?
##
## * postscreen covers smtp (25/tcp).
## FIXME: per https://wiki.dovecot.org/Authentication/Penalty, we
## should meter/block IPv6 sources by /48 instead of by single address (as we do for IPv4).
## Each corresponds to the typical allocation of a single ISP subscriber.
chain my_IPS {
ct state != new return comment "Operate per-flow, not per-packet (my_prologue guarantees this anyway)"
iiftype != ppp return comment "IPS only protects against attacks from the internet"
# Track the rate of new connections (my_IPS_IPvX_meter).
# If someone (ip saddr) connects to a service (ip daddr . tcp dport) too often,
# then blacklist them (my_IPS_IPvX_blacklist).
tcp dport @my_IPS_TCP_ports \
add @my_IPS_IPv4_meter { ip saddr . ip daddr . tcp dport limit rate over 1/minute burst 3 packets } \
add @my_IPS_IPv4_blacklist { ip saddr } \
log level audit log prefix "Blacklist SRC: "
tcp dport @my_IPS_TCP_ports \
add @my_IPS_IPv6_meter { ip6 saddr . ip6 daddr . tcp dport limit rate over 1/minute burst 3 packets } \
add @my_IPS_IPv6_blacklist { ip6 saddr } \
log level audit log prefix "Blacklist SRC: "
# If someone is NOT whitelisted, and IS blacklisted, then drop their connection, AND reset their countdown (hence "update" not "add").
# In other words, once blacklisted for brute-forcing SSH, you REMAIN blacklisted until you STFU for a while (on ALL ports).
ip saddr != @my_IPS_IPv4_whitelist ip saddr @my_IPS_IPv4_blacklist update @my_IPS_IPv4_blacklist { ip saddr } drop
ip6 saddr != @my_IPS_IPv6_whitelist ip6 saddr @my_IPS_IPv6_blacklist update @my_IPS_IPv6_blacklist { ip6 saddr } drop
}
set my_IPS_IPv4_meter { type ipv4_addr . ipv4_addr . inet_service; timeout 10m; flags dynamic; }
set my_IPS_IPv6_meter { type ipv6_addr . ipv6_addr . inet_service; timeout 10m; flags dynamic; }
set my_IPS_IPv4_blacklist { type ipv4_addr; timeout 10m; }
set my_IPS_IPv6_blacklist { type ipv6_addr; timeout 10m; }
set my_IPS_IPv4_whitelist { type ipv4_addr; timeout 10h; }
set my_IPS_IPv6_whitelist { type ipv6_addr; timeout 10h; }
set my_IPS_TCP_ports { type inet_service; elements={
ssh,
telnet, # we don't use it
ftp, ftps, # we don't use it
3389, 5900, # we don't use it (VNC & RDP)
pop3, pop3s, imap, # we don't use it
microsoft-ds, # we don't use it (SMB)
mysql, postgresql, ms-sql-s, # we don't use it (from the internet, without a VPN)
1723, # we don't use it (PPTP)
login, # we don't use it
}; }
# CONSIDERED AND REJECTED FOR my_IPS_TCP_ports
# ============================================
#
# * http, https:
#
# HTTP/0.9 and HTTP/1.1 is one TCP connect per request.
#
# HTTP/1.1 has workarounds that still suck due to head-of-line blocking.
# https://en.wikipedia.org/wiki/HTTP_persistent_connection
# https://en.wikipedia.org/wiki/HTTP_pipelining
#
# HTTP/2 solves this fully, but is /de facto/ never used on port 80.
#
# The end result is that as at August 2019,
# GUI browsers still routinely burst many HTTP connections to a single DST:DPT.
# This IPS only measures burstiness, so it can't work for HTTP/S.
#
# * imaps:
#
# If the server (and client) speak IMAP IDLE but not IMAP NOTIFY,
# the client will make ONE CONNECTION PER MAILBOX FOLDER.
# This looks very bursty, so the IPS can't do it's thing.
#
# See also:
# https://tools.ietf.org/html/rfc5465
# https://wiki2.dovecot.org/Plugins/PushNotification (??? -- different RFC)
# https://bugzilla.mozilla.org/show_bug.cgi?id=479133 (tbird)
# https://blog.jcea.es/posts/20141011-thunderbird_notify.html
# https://en.wikipedia.org/wiki/JMAP (just ditch IMAP entirely)
#
# * smtp, submission:
#
# For smtp (25/tcp), can't do shit because we have to talk to
# whatever the fuck crackhead MTAs are out there.
#
# For submission, we could limit connection rate IFF we knew
# ALL STAFF were running an MSA that batched up the messages.
# We know that at least msmtp does not, so this is a no-go.
#
# (Consider a manager sending 4+ one-liner "yes" or "do it!"
# emails in a single minute. We might be able to mitigate this
# by matching on submission with a more forgiving burst limit,
# e.g. 1/min burst 10? Otherwise, we have to rate-limit in the
# postfix->dovecot SASL backend, or the dovecot->ad LDAP
# backend. UGH.)
#
# * msrpc:
#
# FIXME: wtf even. I don't want to read enough about this to
# know if it's reasonable to IPS it.
#
# * openvpn:
#
# Normally UDP, and we currently only IPS TCP.
# Normally cert-based (but can use PSKs).
# Might be worth considering if we do this later.
#
# * ident:
#
# I think when you irssi -c irc.oftc.net,
# OFTC tries to ident back to you?
# I don't want to accidentally block OFTC/Freenode.
# Allow all ICMPv6 is wrong (insecure);
# Deny all ICMPv6 is wrong (breaks IPv6).
# The following vmap merges RFC 4890 4.43(for hosts) and 4.4 (for routers).
# Fortunately, the only verdict conflicts occur in
# "Traffic That Will Be Dropped Anyway" sections, so we can share this vmap
# between hook input (host) and hook forward (router).
#
# I *think* "dropped anyway" also means we also don't need these:
# ip6 hoplimit 1 # for LLMNR
# ip6 hoplimit 255 # for RA/RS/NA/NS
# ip6 saddr fe80::/10 # for LLMNR and MLD
#
# NOTE: I was going to use named types, but "nft describe icmpv6 type" doesn't have them all.
# Also, using bare numbers makes it possible to use intervals intuitively.
#
# FIXME: add "auto-merge" when possible
# (nft 0.9.1 has set auto-merge, but not map auto-merge).
map ICMPv6_RFC4890_policy {
type icmpv6_type : verdict
flags interval
elements = {
1 - 4: accept, # RFC 4890 4.3.1 & 4.4.1 essential errors
128 - 129: accept, # RFC 4890 4.3.1 & 4.4.1 Echo (ping)
144 - 147: accept, # RFC 4890 4.3.2 & 4.4.3 Mobile IPv6
133 - 136: accept, # RFC 4890 4.3.3 & 4.4.1 (replaces ARP and DHCPv4)
141 - 142: accept, # RFC 4890 4.3.3 & 4.4.1 (replaces ARP and DHCPv4)
130 - 132: accept, # RFC 4890 4.3.3 & 4.4.1 LLMNR
143: accept, # RFC 4890 4.3.3 & 4.4.1 LLMNR
148 - 149: accept, # RFC 4890 4.3.3 & 4.4.1 SEND
151 - 153: accept, # RFC 4890 4.3.3 & 4.4.1 Multicast Router
137: drop, # RFC 4890 4.3.3 & 4.4.4 Redirect
150: drop, # RFC 4890 4.3.4 & 4.4.3 Seamoby
5 - 99: drop, # RFC 4890 4.3.4 & 4.4.4 unallocated error messages
102 - 126: drop, # RFC 4890 4.3.4 & 4.4.4 unallocated error messages
154 - 199: drop, # RFC 4890 4.3.4 & 4.4.? unallocated informational messages
202 - 254: drop, # RFC 4890 4.3.4 & 4.4.? unallocated informational messages
138: drop, # RFC 4890 4.3.5 & 4.4.3 route renumbering
100 - 101: drop, # RFC 4890 4.3.5 & 4.4.5 experimental allocations
200 - 201: drop, # RFC 4890 4.3.5 & 4.4.5 experimental allocations
127: drop, # RFC 4890 4.3.5 & 4.4.5 extension type numbers
139 - 140: drop, # RFC 4890 4.3.5 & 4.4.4 Node Information
255: drop, # RFC 4890 4.3.5 & 4.4.5 extension type numbers
}
}
# NOTE: I couldn't find an RFC for ICMPv4 firewall, so
# I am adopting the following heuristic:
#
# 1. if there is an ICMPv6 equivalent, follow RFC4890.
# 2. if deprecated or experimental or reserved or unallocated, drop.
# 3. NOT rate-limiting ping for now, because ICBF.
# 4. NOT filtering by type.code (only type) for now, because ICBF.
#
# FIXME: duclicsic of #netfilter claims that "ct state related" implicitly handles
# everything except echo-request:
#
# twb: if you accept related/established before dropping icmp it will work fine. destination unreachable messages are matched by related, echo replies are established, etc.
# duclicsic: interesting; I didn't realize that. That covers PMTUD at least
# twb: the only icmpv4 message you need to explicitly accept in a sensible config is echo-request
# duclicsic: for ICMPv4, the only other thing I can see is RS/RA, which is probably not ACTUALLY needed
# no I don't think I've ever seen them used, but you'd know to expect them if you were using them I'm sure.
# duclicsic: yeah except in the *even more* rare case where they're simply transiting through my network on the way somewhere else
# i don't think you'd ever want to forward an icmp rotuer solicitation or advertisement
# duclicsic: oh yeah - DERP - RS/RA are dropped by the kernel automatically and do not transit a route (per RFC 4890)
#
# duclicsic: do you have related Opinions about ICMPv6, or have you simply not bothered with IPv6 yet?
# twb: for v6 I accept echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert
# duclicsic: on gateways, or hosts, or both?
# twb: that's an example from my desktop here
map ICMP_policy {
type icmp_type : verdict
flags interval
elements = {
destination-unreachable: accept, # RFC 4890 4.3.1 essential errors
time-exceeded: accept, # RFC 4890 4.3.1 essential errors
parameter-problem: accept, # RFC 4890 4.3.1 essential errors
echo-request: accept, # RFC 4890 4.3.1 echo (ping)
echo-reply: accept, # RFC 4890 4.3.1 echo (ping)
router-advertisement: accept, # RFC 4890 4.3.3 & 4.4.1 (IRDP - alternative to DHCPv4??)
router-solicitation: accept, # RFC 4890 4.3.3 & 4.4.1 (IRDP - alternative to DHCPv4??)
redirect: drop, # RFC 4890 4.3.3 & 4.4.4 Redirect
source-quench: drop, # deprecated
1 - 2: drop, # unassigned
6 - 7: drop, # deprecated / unassigned
15 - 39: drop, # deprecated / unassigned / reserved / experimental
41 - 255: drop, # deprecated / unassigned / reserved / experimental
13 - 14: continue, # FIXME Timestamp / Timestamp Reply???
40: continue, # FIXME Photuris???
}
}
}
# NOTE: dual-stack (IPv4/IPv6) NAT is annoying.
# IPv6 addresses are plentiful, so don't NAT IPv6.
#
# NOTE: in linux 5.2+ you *CAN* do inet (combined IPv4/IPv6) nat chains.
#
# table inet x { chain y { type nat hook postrouting priority srcnat; policy accept; } }
#
# GOTCHA: apparently you MUST hook BOTH prerouting AND postrouting.
# If you only hook one, it won't work.
#
# Ref. https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_%28NAT%29
#
# FIXME: discuss hostname resolution in DNS, /etc/hosts, versus hard-coding into this file.
#
# NOTE: I assume "the internet" iface is ADSL PPPoE/PPPoA (iiftype/oiftype ppp), and
# that's the ONLY ppp iface you have (cf. bullshit ppptp VPN for iPhone users).
# If you have decent internet, you will probably want to give the iface a logical name,
# then match by that name (iifname/oifname "internet").
#
# NOTE: see "nft flush ruleset" comment at top of file.
add table ip my_nat # idempotent
delete table ip my_nat # not idempotent
table ip my_nat {
chain my_postrouting {
type nat hook postrouting priority srcnat
policy accept
oiftype ppp masquerade
}
chain my_prerouting {
type nat hook prerouting priority dstnat
policy accept
iiftype != ppp return comment "port forwards are only relevant from the internet"
# YOUR PORT FORWARDS HERE.
# NOTE: Not using DNS because https://bugs.debian.org/933531, and
# because we can't use twb's trick ensure resolution
# ONLY via /etc/hosts (no DNS).
define www.example.com = 127.1.2.3
define mail.example.com = 127.254.253.252
tcp dport { http, https } dnat to $www.example.com
tcp dport { smtp, submission, imaps } dnat to $mail.example.com
}
}
# This is here to aid debugging.
# Note that its output WILL NOT MATCH a later "nft list ruleset".
# Also, it is buggy, e.g. the ICMPv6_RFC4890_policy it prints has gibberish in v0.9.1.
# UPDATE: in nftables=0.9.8-3 it sometimes core dumps! https://bugs.debian.org/982576
# Therefore comment out for now.
#list ruleset
nftables-mode-1.1/tests/nftables-host.nft 0000644 0001752 0001753 00000016301 14242773176 017166 0 ustar elpa elpa #!/usr/sbin/nft --file
# -*- mode: conf; mode: nftables; conf-space-keywords: "table\\|chain\\|type\\|policy\\|accept\\|drop\\|counter\\|jump"; -*-
#### This -*-nftables-*- ruleset is my /etc/nftables.conf for new hosts.
#### Ref. http://jengelh.medozas.de/documents/Perfect_Ruleset.pdf
#### Ref. https://wiki.nftables.org/
####
#### GOTCHA: This is written for nft 0.9.1.
#### Several options have changed, even since 0.9.0!
####
#### GOTCHA: "nft --file" does not flush by default.
#### That is, it acts like "iptables-restore --noflush".
####
#### GOTCHA: If your ruleset does "flush; add; flush; add",
#### it will actually do "flush; flush; add; add".
#### That is, all the flushes move to the top.
####
#### This makes it impossible to do my old trick of
#### having the ruleset start with a "deny all" policy,
#### which was useful to make sure that if the firewall failed,
#### the OS would lock down the whole system.
#### (See "iptab" for notes about that.)
####
#### To achieve that under nft, you instead need to patch
#### nftables.service to have
#### OnFailure=nftables-denyall.service, and then write that
#### unit to load a SEPARATE, DIFFERENT nftables file that
#### blocks everything. And even then, it won't behave quite
#### the same.
####
#### This also means it is no longer safe to refer to
#### hostnames in this ruleset, safe in the knowledge that
#### they can only be resolved via local /etc/hosts. Because
#### the "deny all" ruleset can't be prepended here, we CANNOT
#### be sure the real ruleset will only resolve hostnames via
#### local sources -- so you might add ones that are only in
#### DNS, and then have the firewall fail to load during early
#### boot -- leaving you with a "working" host, with no
#### firewall!
####
#### GOTCHA: "list ruleset" here will print the ruleset BEFORE it goes through the kernel;
#### "nft list ruleset" later will print the ruleset AFTER it goes through the kernel.
#### The difference is usually like "iptables -p tcp" getting an implied "-m tcp".
#### However, differences MIGHT indicate a bug! Watch out!
####
#### GOTCHA: "table filter" and "chain INPUT" or "chain input" just
#### conventions and have NO MEANING WHATSOEVER. The actual
#### meaning comes from the "type filter hook input priority
#### filter" line.
####
#### NOTE: Only create a chain if you use it.
#### An empty chain is slightly slower than no chain at all.
#### e.g. most hosts don't need an output chain.
####
#### NOTE: iptables ALWAYS counts how many packets/bytes hit every chain and rule.
#### nftables makes this OPT IN, e.g. change "accept" to "counter accept".
#### iptables-save -c would print "[pkts:bytes] -A ...".
#### nftables list ruleset will print "... counter packgets 12 bytes 34 ...".
####
#### Since counters are useful during debugging but not production,
#### I have left them out of this example.
####
#### NOTE: "table x" is implicitly "table ip x", which is IPv4 only.
#### If you want dual-stack, say "table inet x".
####
#### NOTE: "iif lo" is resolved at ruleset load time into an interface
#### NUMBER inside the kernel; whereas "iifname lo" remains a
#### string. This means that:
####
#### * "iifname" is ~10 times slower than "iif" for every
#### packet considered (strcmp versus ==).
####
#### * If you load a ruleset with "iif foo" before foo exists,
#### the load will fail, LEAVING YOU UNPROTECTED!
####
#### * If you load a ruleset with "iif foo" and then foo is
#### removed and readded (e.g. ppp0 for a flaky ADSL link),
#### what happens?
####
#### * Rule of thumb: always use "iifname" (not "iif").
# NOTE: this will remove *ALL* tables --- including tables set up by
# other things (e.g. sshguard)!
#
# In theory you can flush just your own rules, e.g.
#
# flush table inet my_filter
#
# FIXME: I tried that, and I got locked out of SSH!
# What it did was remove all the rules, but NOT the chains, so
# the default-deny policy dropped EVERYTHING!!!
## NOTE: we add+delete each table (not "flush ruleset"), because
## otherwise we would wipe out sshguard table.
#flush ruleset
add table inet my_filter # idempotent
delete table inet my_filter # not idempotent
table inet my_filter {
chain my_input {
type filter hook input priority filter
policy drop
# Typically 99%+ of packets are part of an already-established flow.
# Allow those first, so we're a fast, stateful firewall.
# After this only "ct state new" (or "ct state untracked") will remain.
# FIXME: is a vmap here better (more efficient) than two separate rules?
# NOTE: {established or related: accept} does not match correctly!
ct state vmap { established: accept, related: accept, invalid: drop }
# Loopback traffic is needed for e.g. NFS RPC, and for debugging.
iiftype loopback accept
# Allow *some* kinds of IPv4/ICMP and IPv6/ICMPv6.
icmp type echo-request accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
# YOUR RULES HERE.
# NOTE: service names resolve via nss (/etc/hosts) only in nft 0.9.1+!
##FOR "router" EXAMPLE### NOTE: a single rule CAN match "allow 53/tcp and 53/udp", but it's UGLY, so we don't.
##FOR "router" EXAMPLE### NOTE: I assume you used systemd (networkd or udev) to rename "enp0s0f0" to "lan".
##FOR "router" EXAMPLE### NOTE: "iif foo" must exist at ruleset load time.
##FOR "router" EXAMPLE### If your ruleset starts BEFORE udev and/or systemd-networkd are READY=1,
##FOR "router" EXAMPLE### consider using 'iifname lan' instead of "iif lan".
tcp dport ssh accept
tcp dport { http, https } accept
##FOR "router" EXAMPLE##iif enp11s0 tcp dport domain accept
##FOR "router" EXAMPLE##iif enp11s0 udp dport { domain, ntp, bootps } accept
# Finally, politely reject all other attempts.
# Omit to use the default policy ("policy drop", above) instead.
reject
}
# A host can't route unless you explicitly enable it, e.g.:
#
# sysctl net/ipv4/ip_forward=1
# sysctl net/ipv6/conf/all/forwarding=1
#
# We create a "deny all" inet filter forward chain anyway, as
# defense-in-depth against someone enabling routing ACCIDENTALLY.
chain my_forward {
type filter hook forward priority filter
policy drop
}
# We want output to be "allow all", so we don't even create a chain.
#chain my_output {
# type filter hook output priority filter
# policy accept
#}
}
# This is here to aid debugging.
# Note that its output WILL NOT MATCH a later "nft list ruleset".
# Also, it is buggy, e.g. the ICMPv6_RFC4890_policy it prints has gibberish in v0.9.1.
# UPDATE: in nftables=0.9.8-3 it sometimes core dumps! https://bugs.debian.org/982576
# Therefore comment out for now.
#list ruleset
nftables-mode-1.1/tests/iptab.ips 0000644 0001752 0001753 00000007113 14242773176 015521 0 ustar elpa elpa #!/usr/sbin/iptables-apply
### This -*-conf-*- file is my template /etc/iptab for new hosts.
### Ref. http://jengelh.medozas.de/documents/Perfect_Ruleset.pdf
### From init, use iptables-restore /etc/iptab (NOT iptables-apply).
### Ruleset can and should be loaded BEFORE network ifaces exist.
###
### Named hosts and services names are resolved ONCE, at load time.
### See getent(1). For meaningful ethernet iface names, edit
### /etc/udev/rules.d/*persistent-net.rules and reboot.
######################################################################
## Rulesets (*foo ... COMMIT) load atomically. First load a deny-all
## ruleset so that if the "real" ruleset fails to load, the system
## WILL NOT be left in an allow-all state.
*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT
-A INPUT -s 192.168/16 -j ACCEPT -m comment --comment "Allow recovery from LAN."
-A OUTPUT -p udp --dport domain -j REJECT -m comment --comment "On error, avoid DNS timeout delays"
COMMIT
######################################################################
*filter
:OUTPUT ACCEPT # Local users/processes are trusted.
:INPUT DROP # Ingress policy is "default deny".
:FORWARD DROP # Routing policy is "default deny".
:PRELUDE - # Best practices for filtered chains.
:BLACKLIST -
## Quickly handle the essentials of a "default deny" environment.
## Anything left after this chain implicitly has --ctstate NEW.
-A INPUT -j PRELUDE
-A FORWARD -j PRELUDE
-A PRELUDE -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A PRELUDE -m conntrack ! --ctstate NEW -j DROP -m comment --comment "Same as --ctstate INVALID."
-A PRELUDE -p icmp -j ACCEPT
-A PRELUDE -i lo -j ACCEPT
## An automated SSH brute-force blacklist. Requires xtables. Unlike
## fail2ban or DenyHosts, there are NO userspace requirements -- not
## even sshd is needed! echo +1.2.3.4 >/proc/net/xt_recent/whitelist
## to whitelist 1.2.3.4 for an hour. Protects both this host AND all
## hosts "behind" this one.
##
# New connections from IPs blacklisted within the last ten minutes are
# chaotically rejected, AND reset the countdown back to ten minutes.
# This is in PRELUDE such that blacklisted attackers are refused ALL
# services, not just rate-limited ones.
-A PRELUDE -m recent --name blacklist --update --seconds 600 --rttl -j BLACKLIST
# This NON-TERMINAL chain counts connections passing through it. When
# a connection rate exceeds 3/min/srcip/dstip/dstport, the source IP
# is blacklisted. Acting on the blacklist is done elsewhere, as is
# accepting or rejecting this connection.
-A PRELUDE -i ppp+ -p tcp --dport ssh -m hashlimit --hashlimit-name maybe-blacklist --hashlimit-mode srcip,dstip,dstport --hashlimit-above 1/min --hashlimit-burst 3 -m recent --name blacklist --set -j LOG --log-prefix "Blacklisted SRC: "
-A BLACKLIST -m recent --name whitelist --rcheck --seconds 3600 -j RETURN -m comment --comment "whitelist overrides blacklist"
-A BLACKLIST -j CHAOS --tarpit
## YOUR RULES GO HERE. Below is a simple example: a firewalling
## router and SSH gateway that also serves DHCP/DNS/NTP to the LAN,
## with a web server "www" and a mail server "mail" behind it.
-A INPUT -p tcp --dport ssh -j ACCEPT
-A INPUT -i lan -p udp -m multiport --dports bootps,domain,ntp -j ACCEPT
-A FORWARD -d mail -p tcp -m multiport --dports smtp,submission,imaps -j ACCEPT
-A FORWARD -d www -p tcp -m multiport --dports http,https -j ACCEPT
## Finally, politely reject all other attempts. Omit these to use the
## chains' default policies (DROP, above) instead.
-A INPUT -j REJECT
-A FORWARD -j REJECT
COMMIT
nftables-mode-1.1/tests/iptab.nat 0000644 0001752 0001753 00000005617 14242773176 015517 0 ustar elpa elpa #!/usr/sbin/iptables-apply
### This -*-conf-*- file is my template /etc/iptab for new hosts.
### Ref. http://jengelh.medozas.de/documents/Perfect_Ruleset.pdf
### From init, use iptables-restore /etc/iptab (NOT iptables-apply).
### Ruleset can and should be loaded BEFORE network ifaces exist.
###
### Named hosts and services names are resolved ONCE, at load time.
### See getent(1). For meaningful ethernet iface names, edit
### /etc/udev/rules.d/*persistent-net.rules and reboot.
######################################################################
## Rulesets (*foo ... COMMIT) load atomically. First load a deny-all
## ruleset so that if the "real" ruleset fails to load, the system
## WILL NOT be left in an allow-all state.
*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT
-A INPUT -s 192.168/16 -j ACCEPT -m comment --comment "Allow recovery from LAN."
-A OUTPUT -p udp --dport domain -j REJECT -m comment --comment "On error, avoid DNS timeout delays"
COMMIT
######################################################################
*filter
:OUTPUT ACCEPT # Local users/processes are trusted.
:INPUT DROP # Ingress policy is "default deny".
:FORWARD DROP # Routing policy is "default deny".
:PRELUDE - # Best practices for filtered chains.
## Quickly handle the essentials of a "default deny" environment.
## Anything left after this chain implicitly has --ctstate NEW.
-A INPUT -j PRELUDE
-A FORWARD -j PRELUDE
-A PRELUDE -m conntrack --ctstate RELATED,ESTABLISHED,DNAT -j ACCEPT
-A PRELUDE -m conntrack ! --ctstate NEW -j DROP -m comment --comment "Same as --ctstate INVALID."
-A PRELUDE -p icmp -j ACCEPT
-A PRELUDE -i lo -j ACCEPT
## YOUR RULES GO HERE. Below is a simple example: a firewalling
## router and SSH gateway that also serves DHCP/DNS/NTP to the LAN,
## with a web server "www" and a mail server "mail" behind it.
-A INPUT -p tcp --dport ssh -j ACCEPT
-A INPUT -i lan -p udp -m multiport --dports bootps,domain,ntp -j ACCEPT
## Finally, politely reject all other attempts. Omit these to use the
## chains' default policies (DROP, above) instead.
-A INPUT -j REJECT
-A FORWARD -j REJECT
COMMIT
*nat
:PREROUTING ACCEPT
:POSTROUTING ACCEPT
:OUTPUT ACCEPT
## Translate private LAN IPs to a single, dynamic public IP.
## If you have a static IP or multiple IPs, use SNAT instead.
## DO NOT use NAT between LANs; route between them.
-A POSTROUTING -o upstream -j MASQUERADE
## Route inbound traffic on specific ports, to machines in the DMZ.
## DNAT destinations cannot be hostnames, because a hostname could
## resolve to more than one IP, which would not be meaningful.
##
## Note that these flows must also be allowed in *filter.
-A PREROUTING -i upstream -p tcp -m multiport --dport smtp,submission,imaps -j DNAT --to 192.168.1.2
-A PREROUTING -i upstream -p tcp -m multiport --dport http,https -j DNAT --to 192.168.1.3
COMMIT
nftables-mode-1.1/tests/README.rst 0000644 0001752 0001753 00000001176 14242773176 015377 0 ustar elpa elpa These are my example Linux firewalls.
The nftables ones are from 2020:
:nftables-host.nft: basic server firewall. Dual stack.
:nftables-router.nft: basic router firewall. Dual stack. Includes optional example IPS.
The xtables (iptables) ones are from 2012 to 2019:
:iptab: basic router firewall (NOTE: legacy IP only!)
:iptab.ips: as ``iptab``, plus an purely in-kernel equivalent of fail2ban or sshguard_.
:iptab.nat: as ``iptab``, plus NAT and "port forwarding".
This also has the initial draft for Emacs nft syntax highlighting.
See also https://debbugs.gnu.org/cgi/bugreport.cgi?bug=36759
.. _sshguard: https://sshguard.net/
nftables-mode-1.1/nftables-mode-pkg.el 0000644 0001752 0001753 00000000625 14242773177 016366 0 ustar elpa elpa ;; Generated package description from nftables-mode.el -*- no-byte-compile: t -*-
(define-package "nftables-mode" "1.1" "Major mode for editing nftables" '((emacs "25.1")) :commit "05600129ee8ea0774c6ac446a2bd18fc1dde54eb" :url "https://elpa.gnu.org/packages/nftables-mode.html" :authors '(("Trent W. Buck" . "trentbuck@gmail.com")) :maintainer '(nil . "emacs-devel@gnu.org") :keywords '("convenience"))