← Back
Bahasa

nftables: Cheat code or AI Slop?

A practical, syntax-first guide to nftables and Linux packet filtering.

Apa sebenarnya nftables itu

Firewall di Linux sebenarnya adalah netfilter, sebuah framework di dalam kernel. Netfilter memasang titik pemeriksaan yang disebut hook di sepanjang jalur yang dilewati paket. nftables hanyalah alat untuk memasang aturan (rule) di hook-hook tersebut. (iptables adalah alat lama dengan fungsi yang sama tapi sintaksnya lebih ribet.)

Sebelum menulis rule, ada tiga dasar networking yang perlu dipahami:

  • Interface — cara mesin terhubung ke jaringan: eth0, wlan0, lo (loopback, mesin bicara dengan dirinya sendiri), wg0 (VPN), docker0. Lihat dengan ip addr.
  • Paket — setiap paket punya header IP (mesin mana: alamat asal/tujuan) dan header TCP/UDP (program mana: port asal/tujuan). Rule firewall pada dasarnya hanya pertanyaan tentang field-field ini.
  • Routing — untuk setiap paket masuk, kernel bertanya: “ini untuk saya, atau untuk mesin lain?” Lihat tabel routing dengan ip route.

Alur paket (gambar paling penting)

                                ┌───────────────┐
 paket masuk ─► prerouting ───► │    routing    │ ────► forward ────────────┐
                                │ untuk saya?   │                           │
                                └───────┬───────┘                           ▼
                                        ▼                             postrouting ──► paket keluar
                                      input                                 ▲
                                        ▼                                   │
                                  proses lokal                              │
                                 (sshd, nginx...)                           │
                                        ▼                                   │
                                     output ────────────────────────────────┘

Paket hanya bisa melewati tiga jalur:

Jalur Hook Penggunaan umum
Masuk ke mesin ini prerouting → input → program Melindungi server (“hanya izinkan SSH + web”)
Keluar dari mesin ini program → output → postrouting Biasanya dibiarkan terbuka
Numpang lewat prerouting → forward → postrouting Router, server VPN, host Docker

Inilah sebabnya firewall server kebanyakan ada di input, dan port forwarding (DNAT) ada di prerouting: alamat tujuan harus diubah sebelum keputusan routing.

Bentuk setiap config

table <family> <nama> {            # folder yang berisi chain
    chain <nama> {                 # daftar rule
        <baris pengaturan>         # menghubungkan chain ke hook (opsional)
        <rule>                     # kondisi ... aksi
    }
}

Hanya tiga level: table → chain → rule. Setiap baris di dalam chain adalah baris pengaturan atau rule.

Dua cara bekerja dengan nftables:

  • Perintah (nft add ...) — langsung mengubah ruleset aktif, hilang saat reboot kecuali disimpan.
  • File config (nft -f file) — mendeskripsikan seluruh ruleset sekaligus. Pakai ini untuk yang permanen.

Melihat dan memuat

nft list ruleset                 # tampilkan semua yang aktif
nft -a list ruleset              # sama, plus nomor "handle" tiap rule (dibutuhkan untuk hapus)
nft list tables                  # hanya nama-nama table
nft list table inet filter       # satu table: family "inet", nama "filter"
nft list chain inet filter input # satu chain di table tersebut

nft -f /etc/nftables.conf        # muat file config (atomik: semua atau tidak sama sekali)
nft -c -f /etc/nftables.conf     # -c = cek saja: validasi sintaks, tanpa mengubah apa pun
nft list ruleset > backup.nft    # simpan yang aktif (muat ulang dengan nft -f)
nft monitor                      # pantau perubahan rule dan event secara live

-a = annotate (tampilkan handle), -c = check, -f = file.

Table

nft add table inet filter        # buat table: family "inet", nama "filter" (nama bebas)
nft delete table inet filter     # hapus beserta isinya (error jika tidak ada)
nft destroy table inet filter    # sama, tapi tidak error jika tidak ada (nftables 1.0.8+ / kernel 6.3+)
nft flush table inet filter      # kosongkan rule di semua chain-nya, chain tetap ada
nft flush ruleset                # hapus SEMUA table, termasuk milik Docker dll.

Family menentukan trafik apa yang dilihat table:

Family Melihat
inet IPv4 + IPv6 sekaligus — pakai ini untuk filtering
ip IPv4 saja — umum untuk table NAT
ip6 IPv6 saja
bridge trafik yang melewati bridge Linux (layer 2)
netdev sangat awal, per interface (misal drop DDoS di ingress)
arp paket ARP

Chain dan baris pengaturan

chain input {
    type filter hook input priority 0; policy drop;
}

Dipecah:

chain input        # NAMA chain — hanya label, bisa apa saja
type filter        # fungsi chain: filter | nat | route
hook input         # titik pemeriksaan tempat chain dipasang (makna sebenarnya)
priority 0         # urutan antar chain di hook yang sama: lebih kecil jalan duluan
policy drop        # keputusan default jika tidak ada rule yang cocok: accept | drop
;                  # pemisah antar pengaturan

Hati-hati: input muncul dua kali. Yang pertama hanya nama; hook input adalah titik pemeriksaan yang sebenarnya. chain satpam { type filter hook input ... } bekerja persis sama.

Hook:

Hook Posisi
prerouting setiap paket masuk, sebelum “ini untuk saya?”
input paket yang ditujukan ke mesin ini
forward paket yang numpang lewat ke mesin lain
output paket yang dibuat oleh mesin ini
postrouting setiap paket keluar, tepat sebelum pergi
ingress (family netdev) titik paling awal di sebuah interface

Nama priority yang sering muncul:

dstnat  = -100   # DNAT di prerouting (jalan sebelum filtering)
filter  =    0   # filtering normal
srcnat  =  100   # SNAT / masquerade di postrouting

Chain lewat perintah:

nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
#   │   │     │          │       └─ pengaturan; tanda kutip agar bash tidak memakan { } dan ;
#   │   │     │          └─ nama chain baru
#   │   │     └─ table mana (family + nama)
#   │   └─ apa yang ditambahkan
#   └─ aksinya

nft add chain inet filter my_ssh                   # chain "biasa": tanpa hook, hanya jalan via jump
nft chain inet filter input '{ policy accept; }'   # ubah policy saja
nft flush chain inet filter input                  # hapus semua rule di chain ini
nft delete chain inet filter my_ssh                # hapus chain (harus kosong & tidak dirujuk)

Anatomi sebuah rule

[kondisi ...]  [statement ...]  [verdict]

Baca seperti kalimat: jika semua kondisi cocok, maka jalankan statement, lalu verdict.

ip saddr 10.0.0.5 tcp dport 22 counter accept
# └── kondisi ───┘ └─ kondisi ──┘ └stmt┘ └verdict┘
# "jika asalnya 10.0.0.5 DAN port tujuan TCP adalah 22, hitung, lalu terima"
  • Beberapa kondisi dalam satu rule = DAN (AND).
  • Koma / set { } di dalam satu kondisi = ATAU (OR).
  • Rule dijalankan dari atas ke bawah; accept atau drop pertama mengakhiri chain. Urutan itu penting.

Mengelola rule lewat perintah:

nft add rule inet filter input tcp dport 22 accept                # tambah di akhir
nft insert rule inet filter input tcp dport 22 accept             # taruh di awal
nft add rule inet filter input position 7 tcp dport 80 accept     # setelah handle 7
nft insert rule inet filter input position 7 tcp dport 80 accept  # sebelum handle 7
nft replace rule inet filter input handle 7 tcp dport 2222 accept # timpa handle 7
nft delete rule inet filter input handle 7                        # hapus (lihat handle dengan -a)

Kondisi (match)

Alamat

ip saddr 192.168.1.10          # alamat asal IPv4 ("s" = source)
ip daddr 10.0.0.0/8            # tujuan IPv4 ("d" = destination), /8 = subnet
ip6 saddr 2001:db8::/32        # asal IPv6
ip saddr != 10.0.0.0/8         # != artinya "BUKAN ini"
ip saddr 10.0.0.1-10.0.0.50    # rentang
ip saddr { 1.2.3.4, 5.6.7.8 }  # { } = set anonim: SALAH SATU dari ini

Port dan protokol

tcp dport 22                   # port tujuan TCP 22
tcp dport { 80, 443 }          # port 80 ATAU 443
tcp dport 8000-8100            # rentang port
udp sport 53                   # port asal UDP 53
meta l4proto { tcp, udp } th dport 53
#    │                    └─ th = "transport header": port untuk tcp ATAU udp
#    └─ l4proto = protokol layer-4
ip protocol icmp               # paket IPv4 yang membawa ICMP
meta l4proto { icmp, ipv6-icmp }   # ICMP untuk IPv4 + IPv6 (pakai di table inet)
icmp type echo-request         # ping IPv4
icmpv6 type echo-request       # ping IPv6

Interface

iif lo                         # interface masuk (via index) — cepat, untuk interface permanen
oif eth0                       # interface keluar (via index)
iifname "wg0"                  # interface masuk via NAMA — untuk interface yang datang-pergi
oifname "eth0"                 # interface keluar via nama
iifname "docker*"              # wildcard

Patokan: iif untuk lo dan interface tetap, iifname untuk VPN, container, dan apa pun yang mungkin belum ada saat rule dimuat.

Connection tracking (ct)

ct state new                   # paket pertama dari koneksi baru
ct state established           # bagian dari koneksi yang sudah disetujui
ct state related               # baru tapi terkait koneksi yang ada (error ICMP, data FTP)
ct state invalid               # tidak masuk akal — drop saja
ct state established,related   # koma = ATAU

Ini kuncinya. Saat kamu menjalankan curl google.com, balasannya masuk ke mesin dan melewati chain input. ct state established,related accept meloloskan semua balasan dari koneksi yang sudah disetujui, jadi rule lainnya cukup memutuskan soal koneksi baru.

Info paket lainnya (meta)

meta nfproto ipv4              # hanya paket IPv4 (berguna di table inet)
meta mark 0x1                  # paket membawa mark 1 (diset sebelumnya oleh rule lain)
meta skuid 1000                # (output saja) dikirim oleh proses milik user ID 1000

Singkatan

Singkatan Arti
ct connection tracking
iif / oif interface masuk / keluar (via index)
iifname / oifname interface masuk / keluar (via nama)
saddr / daddr alamat asal / tujuan
sport / dport port asal / tujuan
l4proto protokol layer-4 (tcp, udp, icmp)
th transport header (port untuk tcp atau udp)
meta info paket di luar header (interface, mark, …)

Verdict (keputusan akhir)

accept                         # loloskan (chain ini selesai)
drop                           # buang diam-diam; pengirim tidak dapat balasan
reject                         # buang dan beri tahu pengirim "tidak"
reject with tcp reset          # balas dengan TCP RST ("connection refused")
reject with icmpx type admin-prohibited   # di table inet pakai "icmpx" (v4 + v6)
jump my_ssh                    # jalankan chain my_ssh, lalu kembali
goto my_ssh                    # jalankan chain my_ssh, tidak kembali
return                         # keluar dari chain ini (kembali ke pemanggil, atau pakai policy)

Statement (lakukan sesuatu, lalu lanjut)

counter                        # hitung paket/byte (lihat dengan nft list ruleset)
log                            # tulis ke log kernel (journalctl -k atau dmesg)
log prefix "SSH-in: "          # tambah label agar mudah di-grep
log prefix "DROP: " level warn # atur tingkat keparahan
limit rate 10/second           # cocok hanya sampai batas ini
limit rate over 10/second      # cocok hanya jika MELEBIHI batas ini
meta mark set 0x1              # beri tanda pada paket
comment "izinkan ssh"          # catatan yang menempel di rule

Statement tidak mengakhiri chain, jadi bisa ditumpuk: counter log prefix "X: " drop.

Arti sebenarnya policy drop

policy drop di input = setiap paket masuk yang tidak di-accept oleh rule mana pun akan di-drop. Tolak secara default, izinkan sebagai pengecualian — seperti daftar tamu.

  • drop — dibuang diam-diam. Pengirim menunggu sampai timeout. Tidak membocorkan info apa pun.
  • reject — dibuang dan dibalas “tidak”. Pengirim langsung dapat “connection refused”.

Policy hanya bisa accept atau drop. Untuk perilaku reject, tambahkan sebagai rule terakhir:

chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    tcp dport 22 accept
    reject          # sisanya dapat "tidak" yang sopan
}

Simulasi paket:

  • SSH ke port 22 → cocok tcp dport 22 accept → masuk.
  • Seseorang mencoba port 3306 → tidak cocok apa pun → di-drop oleh policy.
  • Balasan dari curl google.com → cocok ct state established → masuk.
  • Aplikasi lokal bicara ke 127.0.0.1 → cocok iif lo accept → masuk.

Lupa baris ct state → apt, DNS, dan semua balasan rusak. Lupa iif lo → software lokal rusak. Lupa port 22 → kamu terkunci di luar.

Chain forward

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

Tanpa rule + policy drop = jangan pernah meneruskan trafik orang lain. Tepat untuk server atau desktop biasa. Linux juga tidak akan melakukan routing kecuali forwarding diaktifkan:

sysctl net.ipv4.ip_forward       # 0 = mati (default), 1 = hidup

Saat mesin memang router (eth0 = internet, eth1 = LAN):

chain forward {
    type filter hook forward priority 0; policy drop;
    ct state established,related accept     # balasan dari koneksi yang disetujui
    iifname "eth1" oifname "eth0" accept     # LAN → internet diizinkan
}                                            # internet → LAN di-drop

Server VPN: iifname "wg0" oifname "eth0" accept.

Jebakan Docker: Docker mengaktifkan forwarding dan menambahkan chain-nya sendiri. drop di chain mana pun pada sebuah hook bersifat final, jadi policy drop milikmu di forward bisa memutus container walaupun chain Docker sudah meng-accept-nya. Izinkan interface Docker (misal iifname "docker0" accept, oifname "docker0" accept) dan jangan flush ruleset di host Docker.

Set (daftar yang bisa diubah secara live)

table inet filter {
    set blocklist {
        type ipv4_addr             # isi set
        flags interval             # izinkan rentang/subnet
        elements = { 10.0.0.0/8, 203.0.113.7 }
    }

    set temp_ban {
        type ipv4_addr
        flags timeout              # elemen bisa kedaluwarsa
        timeout 1h                 # masa berlaku default
    }

    chain input {
        type filter hook input priority 0; policy drop;
        ip saddr @blocklist drop   # @nama = "apakah ada di set ini?"
        ip saddr @temp_ban drop
    }
}
nft add element inet filter blocklist '{ 198.51.100.0/24 }'     # tambah
nft delete element inet filter blocklist '{ 203.0.113.7 }'      # hapus
nft add element inet filter temp_ban '{ 1.2.3.4 timeout 10m }'  # tambah dengan masa berlaku
nft list set inet filter blocklist                              # lihat isi

Tipe set: ipv4_addr, ipv6_addr, inet_service (port), ifname, ether_addr (MAC).

Map (tabel lookup)

tcp dport vmap { 22 : accept, 23 : drop, 80 : jump web }
# vmap = verdict map: cari port, terapkan verdict di sebelahnya

dnat to tcp dport map { 80 : 192.168.1.10, 443 : 192.168.1.11 }
# map = cari port, pakai hasilnya sebagai tujuan NAT

Membatasi brute force (per IP)

table inet filter {
    set ssh_meter {
        type ipv4_addr
        flags dynamic              # rule bisa menambah entri secara otomatis
        timeout 1m                 # lupakan IP setelah 1 menit diam
    }
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        tcp dport 22 ct state new update @ssh_meter { ip saddr limit rate over 3/minute } drop
        # "jika satu IP membuka lebih dari 3 koneksi SSH baru per menit, drop"
        tcp dport 22 accept
    }
}

NAT

table ip nat {                                     # family "ip": paling simpel untuk NAT IPv4
    chain prerouting {
        type nat hook prerouting priority dstnat;
        iifname "eth0" tcp dport 8080 dnat to 192.168.1.10:80
        # DNAT = ubah TUJUAN: publik :8080 → server internal :80
        tcp dport 80 redirect to :3128
        # redirect = DNAT ke mesin ini sendiri (misal proxy lokal)
    }
    chain postrouting {
        type nat hook postrouting priority srcnat;
        oifname "eth0" masquerade
        # masquerade = ubah ASAL jadi IP eth0 saat ini (IP dinamis)
        oifname "eth1" snat to 203.0.113.10
        # SNAT = ubah ASAL jadi IP tetap (IP statis)
    }
}

Di table inet, sebutkan versi IP-nya: dnat ip to 192.168.1.10:80. NAT yang di-routing juga butuh sysctl -w net.ipv4.ip_forward=1 plus rule di chain forward.

Variabel dan include

define LAN = 192.168.1.0/24        # definisikan variabel
define WEB_PORTS = { 80, 443 }     # bisa berisi set

ip saddr $LAN accept               # $ = pakai variabel
tcp dport $WEB_PORTS accept

include "/etc/nftables.d/*.nft"    # pecah config besar jadi beberapa file

Flush atau tidak

nftables tidak pernah flush sendiri:

  • Perintah nft add ... menambahkan ke yang sudah ada.
  • nft -f file menjalankan isi file. Tanpa flush di dalamnya = rule menumpuk (dimuat dua kali → duplikat).

flush ruleset di awal file menjadikan file itu satu-satunya sumber kebenaran, dan karena nft -f bersifat atomik, tidak pernah ada momen tanpa firewall. Tapi perintah ini menghapus semua table — Docker, libvirt, fail2ban, Tailscale, Kubernetes. Di mesin yang dipakai bersama, reset table milikmu saja:

# nftables 1.0.8+
destroy table inet myfw
table inet myfw { ... }

# versi lama — idiom buat-lalu-hapus (delete error jika table tidak ada)
table inet myfw
delete table inet myfw
table inet myfw { ... }

Patokan: mesin khusus tanpa tool lain yang mengatur firewall → flush ruleset aman. Ada tool lain yang mengatur networking → reset table milikmu saja.

Contoh config server lengkap dengan anotasi

#!/usr/sbin/nft -f

define SSH_ALLOWED = { 203.0.113.0/24 }        # siapa yang boleh SSH (sesuaikan!)

table inet myfw                                 # reset table kita saja
delete table inet myfw

table inet myfw {
    chain input {
        type filter hook input priority filter; policy drop;

        ct state invalid drop                   # paket sampah: tolak
        ct state established,related accept     # balasan koneksi kita: terima
        iif lo accept                           # mesin bicara ke dirinya sendiri: terima

        meta l4proto { icmp, ipv6-icmp } accept # ping & ICMP (IPv6 rusak tanpa ICMPv6)

        ip saddr $SSH_ALLOWED tcp dport 22 accept comment "ssh dari kantor"
        tcp dport { 80, 443 } accept comment "web"

        limit rate 5/minute log prefix "nft-drop: "   # log sebagian trafik yang diblokir
        # sisanya jatuh ke policy drop
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
        # bukan router: jangan teruskan apa pun
    }

    chain output {
        type filter hook output priority filter; policy accept;
        # izinkan semua yang kita kirim
    }
}

Dalam bahasa sehari-hari: izinkan balasan, loopback, ping, SSH dari kantor, dan web; drop sisanya; jangan meneruskan apa pun; kirim apa saja.

Service dan keamanan

systemctl enable --now nftables     # muat /etc/nftables.conf saat boot dan sekarang
systemctl reload nftables           # muat ulang setelah mengedit

# sebelum perubahan berisiko lewat SSH, jadwalkan pembatalan otomatis:
nft list ruleset > /root/good.nft
(sleep 120 && nft -f /root/good.nft) &   # terkunci? rule kembali dalam 2 menit
nft -f /etc/nftables.conf                # terapkan; jika aman: kill %1

iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT   # konversi sintaks iptables lama

Jika file backup tidak me-reset table-nya, memulihkannya di atas table yang sudah ada akan membuat duplikat — tambahkan flush ruleset di awal file backup untuk kasus itu.

Cara membangun kemampuan ini

  1. Pahami dasarnya: ip addr, ip route, ss -tulpn (apa yang listen di mana).
  2. Lihat paket nyata: tcpdump -i eth0 port 22.
  3. Pakai VM sekali pakai — kamu pasti pernah terkunci di luar setidaknya sekali.
  4. Muat config minimal, lalu tambahkan counter / log dan amati rule mana yang cocok.
  5. Buat router dengan dua VM untuk merasakan forward dan NAT; periksa koneksi dengan conntrack -L.

Jika kamu paham gambar alur paket dan connection tracking, kamu sudah paham ~80% nftables. Sisanya hanya sintaks yang bisa dicari kapan saja.