Skip to main content

2.1 nftables: Kiến Trúc Bên Dưới Lớp Cú Pháp

Hầu hết tài liệu về nftables, kể cả trang chính thức của Ubuntu, dừng lại ở mức "đây là cú pháp, đây là ví dụ nft add rule". Rất ít tài liệu trả lời những câu hỏi nằm ngay bên dưới: nft list ruleset thật ra đang hỏi ai? Vì sao nft có thể gộp nhiều lệnh add table lại thành 1 giao dịch atomic mà không cần khóa toàn bộ ruleset? "Bytecode" mà tài liệu Ubuntu nhắc tới thực chất là gì, có phải mã máy biên dịch sẵn không? Và vì sao iptables sau Ubuntu 20.10 vẫn chạy được, nhưng ufw lại không đụng được vào rule nftables thuần?

Tài liệu này bắt đầu từ trang Ubuntu Security Docs về nftables, sau đó đi sâu hơn vào tài liệu phát triển chính thức của dự án nftables (wiki nftables.org, mã nguồn kernel net/netfilter/) để trả lời đúng những câu hỏi kiến trúc ở trên, không dừng ở mức "cách dùng lệnh".

Đọc bài này không cần biết trước nftables là gì: mọi khái niệm (table, chain, hook, verdict...) được giới thiệu ngay khi lần đầu xuất hiện. Nhưng đây không phải bài hướng dẫn viết nftables.conf, nó đi thẳng vào những gì chạy bên dưới lớp cú pháp đó.

Mục lục​


1. Vì sao thay iptables: vấn đề nftables thật sự giải quyết​

Trước nftables, Linux có 4 công cụ tách rời gần như hoàn toàn: iptables (IPv4), ip6tables (IPv6), arptables (ARP), ebtables (bridge). Mỗi công cụ có cú pháp riêng, cơ chế mở rộng riêng (kernel module xt_* cho từng match/target), và quan trọng nhất: không chia sẻ được logic. Muốn 1 rule áp dụng cho cả IPv4 lẫn IPv6, phải viết 2 lần bằng 2 công cụ khác nhau, đồng bộ tay.

Về mặt thực thi, iptables (thế hệ xtables) đánh giá rule theo kiểu tuần tự, tuyến tính: mỗi rule là 1 danh sách match-module + target-module, kernel duyệt qua từng rule theo đúng thứ tự khai báo trong chain, dừng ở rule đầu tiên khớp có target chung cuộc. Với 1 danh sách vài chục nghìn IP cần chặn, cách duy nhất là vài chục nghìn rule -A, mỗi gói tin trong trường hợp xấu nhất phải duyệt qua gần hết danh sách đó, độ phức tạp O(n) theo số rule. ipset ra đời sau này chính là bản vá cho đúng vấn đề này: đưa việc tra cứu tập hợp lớn ra khỏi vòng lặp rule tuần tự, vào 1 cấu trúc dữ liệu tra cứu nhanh riêng.

nftables (Patrick McHardy khởi xướng, đưa vào mainline kernel 3.13 năm 2014) giải quyết cả 2 vấn đề cùng lúc bằng 1 thiết kế khác hẳn ở gốc rễ:

  • Một kernel subsystem chung (nf_tables) cho mọi họ giao thức (ip, ip6, inet cho cả 2 cùng lúc, arp, bridge, netdev), thay vì 4 subsystem xtables riêng biệt.
  • Không có "match module" và "target module" cố định. Thay vào đó là một tập hợp nhỏ các expression nguyên thủy (so sánh, tra bảng, lookup set, thao tác bit...) mà nft ở userspace ghép lại thành rule, tương tự cách 1 trình biên dịch ghép các lệnh máy đơn giản thành 1 chương trình, thay vì gọi thẳng 1 hàm C khổng lồ đã biên dịch sẵn cho từng loại match.
  • Set là công dân hạng nhất của ngôn ngữ, không phải tiện ích rời như ipset: nft add rule inet filter input ip saddr @blacklist drop tra cứu tập hợp ngay trong 1 rule, độ phức tạp gần O(1) bất kể tập hợp có bao nhiêu phần tử, không cần vài chục nghìn rule rời.

Kết quả là 1 ruleset "khai báo" (declarative) hơn: mô tả cấu trúc dữ liệu mà gói tin cần được đối chiếu, thay vì 1 chuỗi lệnh tuần tự phải duyệt hết.

Blocking 50,000 IPs: linear rule walk vs one set lookupiptables (xtables)one -A rule per IP address-A INPUT -s 198.51.100.20 -j DROP-A INPUT -s 198.51.100.21 -j DROP-A INPUT -s 198.51.100.22 -j DROP. . .rule number 50,000worst case: 50,000 evaluations per packetcost grows with the rule count: O(n)ipset was added later to patch exactly thisnftables (nf_tables)one rule plus one named setip saddr @denylist dropa single rule in the chainnft_lookup@denylist : hash set50,000 elements, kernel data structurelookup cost flat whatever the element countcost tied to the structure, not the rule count: near O(1)Same policy, two execution models: walk a long list, or ask one data structure onceThis is the gap that made four separate tools (iptables, ip6tables, arptables, ebtables) worth replacing

2. Kiến trúc 3 lớp: từ nft gõ trên terminal tới kernel​

Khi gõ nft add rule inet filter input tcp dport 22 accept, có 3 lớp tách biệt cùng tham gia (theo tài liệu phát triển chính thức trên wiki.nftables.org, mục "nftables internals"):

Lớp 1: nft (userspace, C, dùng libnftables). Parse cú pháp (bằng lex/yacc, file scanner.l + parser_bison.y) hoặc parse JSON (qua libjansson) thành cây lệnh nội bộ, chạy 1 vòng kiểm tra ngữ nghĩa (semantic evaluation: vd tcp dport chỉ hợp lệ nếu ruleset đã/sẽ biết gói tin là TCP), rồi biên dịch cây lệnh đó thành chuỗi cấu trúc libnftnl tương ứng.

Lớp 2: libnftnl. Thư viện trung gian mỏng, chỉ có nhiệm vụ ánh xạ 1-1 giữa khái niệm nftables (table, chain, rule, expression, set...) và thông điệp netlink: cấp phát, set/get field, serialize ra buffer nhị phân đúng định dạng netlink attribute (nla).

Lớp 3: kernel nf_tables. Nhận thông điệp netlink qua 1 subsystem netlink riêng (NFNL_SUBSYS_NFTABLES), giải mã, và đây là nơi duy nhất trực tiếp thực thi: vừa giữ ruleset trong bộ nhớ kernel, vừa chạy vòng lặp đánh giá rule cho mỗi gói tin đi qua netfilter hook (chi tiết ở mục 4).

Điểm quan trọng: nft không có trạng thái. Nó không giữ "bản sao" ruleset nào trong bộ nhớ của chính nó giữa 2 lần gọi lệnh. Mỗi lần chạy nft list ruleset, nó gửi 1 yêu cầu netlink NFT_MSG_GETRULE (kèm cờ DUMP) tới kernel, kernel trả về toàn bộ ruleset hiện có, nft chỉ format lại cho người đọc. Ruleset "sự thật" luôn nằm ở kernel, nft chỉ là 1 client ngắn hạn, không khác gì vai trò fail2ban-client so với fail2ban-server trong 1 kiến trúc client/daemon khác.

Three layers: nft (parse) → libnftnl (netlink) → kernel nf_tables (execute)nft (userspace)lex/yacc or JSONsemantic evaluationcompile → internal cmdsholds no state betweentwo command runslibnftnlmaps concepts 1-to-1nftables ↔ netlink attrserializes into binarynetlink bufferkernel nf_tablesholds ruleset in RAMthe only source of truthruns nft_do_chain()for every packetcommand treenetlink msgnft list ruleset: the kernel answers, not a local cacheNFNL_MSG_BATCH_BEGIN + N messages + NFNL_MSG_BATCH_END = one atomic transaction (section 5)

3. Gia đình địa chỉ, hook, và 2 loại chain​

Family quyết định gói tin nào đi qua table: ip (chỉ IPv4), ip6 (chỉ IPv6), inet (cả hai, đúng chỗ nftables giải quyết vấn đề "viết 2 lần" đã nói ở mục 1), arp, bridge (xử lý tại tầng Ethernet bridge), netdev (gắn thẳng vào 1 network device cụ thể, dùng cho ingress/egress sớm nhất có thể, trước cả khi packet đi vào stack IP).

Bên trong 1 table là chain, nhưng khác iptables (chỉ có đúng 5 chain built-in cố định INPUT/OUTPUT/FORWARD/PREROUTING/POSTROUTING), nftables cho phép tạo bao nhiêu chain tùy ý, và chia rõ 2 loại:

  • Base chain: đăng ký trực tiếp vào 1 trong 7 hook point của Netfilter (ingress, prerouting, input, forward, postrouting, output, egress), kèm 1 priority (số nguyên, chain priority thấp hơn chạy trước) để nhiều base chain của nhiều table khác nhau cùng gắn vào 1 hook vẫn có thứ tự xác định. Base chain là điểm packet thực tế đi qua trong hành trình netfilter.
  • Regular chain: không gắn hook nào cả, chỉ được gọi tới từ chain khác qua statement jump/goto, giống hệt 1 subroutine. Dùng để tổ chức lại ruleset lớn thành các khối logic tái sử dụng được, tách biệt khỏi đường đi thật của packet.

Chain type khai báo trên base chain (filter, nat, route) không chỉ là nhãn: nó ràng buộc statement nào được phép dùng ở đâu: dnat/snat chỉ hợp lệ trong chain type nat, và chain type nat chỉ được đánh giá cho gói tin đầu tiên của 1 connection (packet tiếp theo cùng connection đi qua conntrack, không lặp lại logic NAT). Đây là lý do rule NAT đặt sai chain type âm thầm không có tác dụng, dù cú pháp vẫn hợp lệ.

Seven hook points: base chains attach here, regular chains do notpacket flow through the network stackingresspreroutinginputforwardpostroutingoutputegressbase chain: hook + prioritypackets really traverse itregular chainreached only via jump/goto

4. "Bytecode" thực chất là gì: máy ảo thanh ghi bên trong kernel​

Trang Ubuntu mô tả nftables "biên dịch rule ra bytecode để kernel thực thi", cho tốc độ O(1). Cần làm rõ chữ "bytecode" ở đây nghĩa là gì, vì nó dễ gây hiểu lầm thành "JIT ra mã máy x86 thật". Theo đúng tài liệu phát triển của dự án (wiki.nftables.org, portal DeveloperDocs), thứ được gọi là bytecode ở đây là 1 chuỗi expression object (cấu trúc dữ liệu kernel, không phải instruction CPU) được nối lại thành 1 danh sách, kernel duyệt tuần tự danh sách đó cho mỗi rule, mỗi gói tin: đúng nghĩa 1 máy ảo (VM) diễn giải (interpret), không phải native machine code đã biên dịch sẵn thành binary thực thi trực tiếp trên CPU.

Cơ chế lõi là thanh ghi (register). Hàm trung tâm nft_do_chain() cấp cho mỗi rule 1 dải thanh ghi tạm (16 thanh ghi 32-bit, có thể ghép thành thanh ghi rộng hơn cho địa chỉ IPv6). Mỗi expression trong rule đọc/ghi các thanh ghi này để truyền dữ liệu cho expression tiếp theo, đúng mô hình 1 CPU register-based đơn giản:

  • nft_payload: đọc 1 vùng byte từ packet (offset + length xác định trước, ví dụ byte 16-19 của IP header = destination address) ghi vào 1 thanh ghi.
  • nft_cmp: so sánh giá trị trong thanh ghi với 1 hằng số (constant đã nạp sẵn lúc biên dịch rule): expression đứng sau hầu hết mọi điều kiện match dạng tcp dport 22.
  • nft_lookup: tra thanh ghi trong 1 set/map đã tạo sẵn (chi tiết mục 6): expression thay thế cho việc phải viết hàng nghìn nft_cmp liên tiếp.
  • nft_bitwise: các phép AND/OR/XOR/shift trên thanh ghi, dùng cho việc match theo netmask chẳng hạn.
  • nft_meta: đọc metadata không nằm trong packet payload: interface index, mark, thời điểm, uid tiến trình tạo ra socket...
  • nft_counter: cộng dồn số gói/số byte đã đi qua expression này, dùng chung cho mọi rule có kèm counter.

Rule kết thúc bằng 1 statement: statement terminal (accept, drop, queue, reject) chấm dứt luôn việc đánh giá, không rule nào phía sau trong chain đó được xét tới nữa; statement non-terminal (counter, log, ct mark set ...) chỉ làm 1 hành động phụ rồi packet tiếp tục đi qua rule kế tiếp. Nếu packet duyệt hết mọi rule trong 1 base chain mà không gặp statement terminal nào, kết quả rơi về policy khai báo trên chain đó (accept hoặc drop).

nft_do_chain(): rule "tcp dport 22 accept" interpreted through registersnft_payloadreads dport bytes→ writes register $1nft_cmp$1 == 22 ?no match → rule endsstatementaccept (terminal)stops evaluationmatchnext rulein the same chain(if no match)Not JIT machine code: a list of expression objects interpreted in sequencefor every rule, every packet, inside nft_do_chain(): a register VM, not real CPU bytecode

5. Giao dịch nguyên tử: vì sao thay cả ruleset không cần dừng traffic​

Ubuntu docs có 1 ví dụ ngắn gọn: sudo nft "add table inet foo; add table inet bar", 2 lệnh cách nhau dấu ; tạo thành 1 giao dịch atomic: hoặc cả 2 table cùng được tạo, hoặc không table nào được tạo nếu có lỗi giữa chừng. Cơ chế đứng sau chuyện này quan trọng hơn nhiều so với vẻ tiện lợi cú pháp của nó.

Ở tầng netlink, mọi thay đổi ruleset (dù gõ trực tiếp hay nft -f /etc/nftables.conf nạp cả trăm dòng) đều được libnftnl gói vào 1 batch: mở đầu bằng thông điệp NFNL_MSG_BATCH_BEGIN, theo sau là từng thông điệp thay đổi, kết thúc bằng NFNL_MSG_BATCH_END. Kernel coi trọn batch này là 1 transaction: nếu bất kỳ thông điệp nào trong batch bị từ chối (vd cú pháp hợp lệ nhưng tham chiếu tới 1 chain không tồn tại), toàn bộ batch bị rollback, không có trạng thái "thực hiện được nửa chừng".

Cơ chế cho phép rollback rẻ và không cần khóa toàn cục là generation ID (theo tài liệu DeveloperDocs của dự án): mỗi object trong ruleset (table, chain, rule...) mang theo 1 genmask: 2 bit đánh dấu object đó thuộc thế hệ (generation) ruleset nào. Kernel giữ song song 2 "thế hệ" ruleset khả dụng, chỉ 1 trong 2 là generation đang active tại một thời điểm (đường packet-processing đang đọc). Khi commit 1 batch: các object mới được gắn cờ thuộc thế hệ kế tiếp, còn thế hệ hiện tại vẫn nguyên vẹn và tiếp tục phục vụ gói tin đang bay tới trong lúc transaction được build; commit thành công thì con trỏ "generation đang active" chuyển sang thế hệ mới trong 1 bước; commit thất bại thì generation kế tiếp đơn giản bị vứt bỏ, generation hiện tại chưa từng bị đụng tới.

Hệ quả: đường xử lý gói tin (packet path, chạy trên mọi CPU, cực kỳ nhạy về độ trễ) không bao giờ phải dừng lại chờ khóa để 1 thay đổi ruleset hoàn tất. Nó luôn thấy 1 trong 2 thế hệ toàn vẹn, chưa bao giờ thấy 1 ruleset "lưng chừng" nửa cũ nửa mới. Khác biệt so với iptables-legacy nằm ở đó: thay cả bảng rule lớn bằng iptables-restore vẫn atomic ở cấp lệnh, nhưng không có khái niệm "batch nhiều loại object khác nhau (table/chain/set/rule) là 1 giao dịch duy nhất" tường minh như nftables.

Two parallel generations: the packet path never sees a half-applied rulesetCurrent generation (active)packet path reads from hereunchanged while the transaction buildsNext generation (building)batch NFNL_MSG_BATCH_BEGIN..ENDwritten here, not yet activeCommit succeeds → the active pointer moves to the new generationCommit fails → the new generation is dropped, the current one is untouched

6. Set và Map: từ hash/rbtree tới pipapo​

Set trong nftables không phải 1 tính năng phụ: nó là cấu trúc dữ liệu mà nft_lookup (đã nhắc ở mục 4) tra vào, và kernel tự chọn cấu trúc lưu trữ bên dưới tùy vào loại dữ liệu và cách khai báo, không phải người dùng chỉ định trực tiếp:

  • Set giá trị rời rạc (vd danh sách IP cụ thể, không có khoảng): dùng bảng băm (hash set), tra cứu trung bình gần O(1) bất kể số phần tử.
  • Set khoảng (interval) (vd 192.168.0.0/16, hay 1 dải cổng 1024-65535): dùng rbtree (red-black tree), cần thiết vì tra 1 giá trị trong khoảng đòi hỏi so sánh thứ tự (ordering), việc hash không hỗ trợ được.
  • Bitmap set: dùng cho miền giá trị nhỏ và rời rạc (kiểu 1 byte, ví dụ set theo protocol number): tra cứu bằng cách đánh chỉ số thẳng vào mảng bit, nhanh nhất trong mọi loại nhưng chỉ áp dụng được khi miền giá trị đủ nhỏ.
  • pipapo ("PIle PAcket POlicies", tác giả Stefano Brivio, Red Hat, đưa vào kernel 5.6): xử lý trường hợp khó nhất, tức set nối nhiều trường lại với nhau, trong đó có ít nhất 1 trường là khoảng, ví dụ { ip saddr . tcp dport } với saddr là 1 dải subnet còn dport là 1 khoảng cổng. rbtree đơn thuần không mở rộng tự nhiên sang nhiều chiều; pipapo giải bằng cách chia mỗi trường thành 1 giai đoạn trong 1 pipeline, mỗi giai đoạn dùng cấu trúc dạng lookup-table/bitmap thu hẹp dần tập ứng viên còn khớp qua từng trường, tới giai đoạn cuối chỉ còn đúng tập kết quả thỏa toàn bộ các trường cùng lúc, tránh việc phải quét chéo (cartesian) từng cặp giá trị.

Named set (nft add set inet filter blacklist { type ipv4_addr; }) sống độc lập, nhiều rule tham chiếu cùng lúc, thêm/bớt phần tử runtime (nft add element ...) mà không cần đụng tới rule. Anonymous set (ip saddr { 10.0.0.1, 10.0.0.2 } viết thẳng trong rule) chỉ tồn tại gắn với đúng 1 rule đó, không tái sử dụng được, đổi lại gọn cho ruleset đơn giản.

Map mở rộng set thêm 1 bước: thay vì chỉ trả lời "có/không thuộc tập", nó ánh xạ khóa sang giá trị. Khi giá trị trả về là 1 verdict (accept/drop/jump <chain>...), set đó gọi là verdict map (vmap), cho phép định tuyến 1 gói tin tới đúng chain xử lý tương ứng chỉ bằng 1 lần tra cứu O(1), thay vì 1 chuỗi rule if/else tuần tự kiểm tra từng giá trị. Đây là kỹ thuật thường dùng để thay thế hàng loạt rule iptables -A ... -j SOME_CHAIN lặp lại theo IP nguồn bằng đúng 1 rule nft.

The kernel picks the structure from what the set holdshash setholds: discrete valuessingle IPs, no rangeshash table on the keynear O(1) averagewhatever the sizerbtreeholds: intervals192.168.0.0/16, 1024-65535red-black tree, orderedordering is requireda hash cannot answer thisbitmapholds: tiny domainsprotocol numbers, 1 bytedirect index into bitsfastest of the foursmall domains onlypipapoholds: concatenatedfields, one is a rangestaged pipelinekernel 5.6 onwardrbtree does not scale herepipapo: one stage per field, the candidate set narrows at each stageexample key: ip saddr . tcp dport, a subnet range concatenated with a port rangestage 1: ip saddrall elements whose subnetcould still matchstage 2: tcp dportintersect with the ones whoseport range also matchesresultexactly the elements matchingevery field at onceNarrowing stage by stage avoids a cartesian sweep over every value pairNone of this is chosen by hand: the declared type and whether ranges appear decide it

7. Object có trạng thái: counter, quota, limit​

Ngoài set/map, nftables có 1 nhóm object khác giữ trạng thái tích lũy qua thời gian, tách biệt khỏi logic match/verdict của rule:

  • Counter: đếm số gói + số byte đã đi qua. Có thể gắn counter không tên (mỗi rule tự có bộ đếm riêng) hoặc counter name mycounter (nhiều rule cùng cộng dồn vào 1 bộ đếm chung).
  • Quota: giới hạn tổng số byte cho phép trước khi statement kèm theo kích hoạt (vd giới hạn băng thông tháng cho 1 interface).
  • Limit: giới hạn tốc độ, hoạt động theo mô hình token bucket, khác quota ở chỗ quota tính tổng cộng dồn không giới hạn thời gian, còn limit tính tốc độ tức thời (rate) và tự nạp lại token theo chu kỳ.
  • Connlimit: giới hạn số connection đồng thời cho 1 khóa nhất định (vd 1 địa chỉ IP nguồn), tích hợp thẳng với bảng theo dõi connection (conntrack) sẵn có của kernel, không cần tự đếm riêng.

Giống named set, các object này có thể khai báo named (dùng chung, độc lập vòng đời với rule tham chiếu nó) hoặc anonymous (gắn liền, sống chết theo rule).

8. Flowtable: đường tắt bỏ qua chính bộ máy vừa mô tả​

Toàn bộ mục 4–7 mô tả 1 packet phải đi qua: parse expression, tra set, cập nhật counter, đi qua đúng chuỗi hook prerouting → forward → postrouting mỗi lần. Với traffic forward thuần túy (router/gateway) mà connection đã ở trạng thái ESTABLISHED, phần lớn công việc đó là lặp lại y hệt cho mỗi gói tin tiếp theo của cùng 1 flow, và flowtable (theo tài liệu wiki chính thức của dự án) tồn tại để bỏ qua hẳn phần lặp lại đó.

Cơ chế: sau khi 1 flow đã có state trong conntrack (thường là ngay sau gói tin reply đầu tiên), flow đó được "offload" vào flowtable, tức 1 bảng tra cứu riêng, khóa theo tuple (interface, địa chỉ, cổng, giao thức L3/L4), cache sẵn interface đích và gateway. Gói tin tiếp theo thuộc flow đó, khi đi vào ở hook ingress (điểm sớm nhất, trước cả prerouting), được đối chiếu thẳng với flowtable: nếu khớp, gói tin bỏ qua toàn bộ chuỗi hook forward/postrouting thông thường, chỉ giảm TTL/hoplimit rồi chuyển thẳng ra tầng neighbor/transmit, không đi qua lại nft_do_chain() của bất kỳ base chain nào nữa. Nếu NIC hỗ trợ, bước này còn offload thẳng xuống hardware.

Đánh đổi rất rõ ràng: flowtable chỉ tăng tốc traffic forward đã established (gói tin đầu mở connection vẫn phải đi đường đầy đủ để tạo state), và một khi đã add vào flowtable bằng statement flow add @f, mọi rule accounting/logging phía sau (counter, log) trong base chain thông thường không còn thấy các gói tin tiếp theo của flow đó nữa, vì chúng không đi qua đường đó nữa. Đây là điểm cần cân nhắc khi vừa muốn tăng tốc vừa muốn giữ số liệu thống kê chính xác trên từng gói.

Flowtable: the first packet builds state, the rest skip the chain walkPacket 1 of the flow: full path, nft_do_chain() runs at every base chainingressno match yetpreroutingconntrack stateforwardfilter rulespostroutingsnat, counterstransmitout the egress niconce conntrack holds the reply direction, the flow is offloaded with the flow add statementPacket 2 and onward: ingress matches the flowtable, the hooks are bypassedingressearliest hookflowtable lookupkey: iface, addrs, ports, prototransmitttl decremented onlyhit: forward and postrouting never runThe trade-off: accounting in the bypassed chains goes blindcounter and log statements in the normal base chains never see packet 2 and onward, because those packets no longer pass throughonly forwarded, already established traffic qualifies: the connection-opening packet always takes the full pathWith a capable nic the same offload continues down into hardware

9. So với iptables cho đúng bản chất, không chỉ bề mặt​

Khác biệt sâu nhất không phải cú pháp mà là mô hình thực thi. iptables (xtables) đánh giá 1 chain bằng cách gọi tuần tự hàm C đã biên dịch sẵn cho từng match/target module (xt_tcp, xt_multiport, ipt_MASQUERADE...): thêm 1 loại match mới nghĩa là viết 1 kernel module mới. nftables đánh giá bằng cách diễn giải 1 danh sách expression nguyên thủy dùng chung (như mục 4 mô tả): thêm 1 khả năng match mới thường chỉ cần tổ hợp lại các expression sẵn có ở tầng userspace, không cần module kernel riêng cho từng trường hợp.

Từ Ubuntu 20.10, iptables mặc định chạy trên backend nft (gói iptables-nft). Đây là 1 lớp dịch đúng nghĩa, không phải một bản nftables "giả": lệnh iptables được dịch thành các object nftables tương đương (table/chain/rule dùng đúng cơ chế ở mục 2–4), rồi nft list ruleset thấy được các rule đó, vì cả 2 con đường cuối cùng đều hội tụ về cùng 1 kernel subsystem nf_tables. Đây là lý do tài liệu Ubuntu nhấn mạnh: chỉ nên dùng 1 trong 2 cách quản lý (thuần nft, hoặc iptables/ufw/firewalld chạy trên backend nft), không phải vì chúng "không tương thích kỹ thuật", mà vì 2 công cụ độc lập cùng chỉnh sửa chung 1 ruleset dễ dẫm chân, ghi đè hoặc hiểu sai ý định của nhau.

ufw là ngoại lệ đáng chú ý: nó vẫn thao tác qua cơ chế iptables-nft, nhưng không đọc hiểu được rule người dùng tự thêm bằng nft trực tiếp (native nftables rule nằm ngoài table mà ufw tự quản lý), nên trộn ufw với rule nft viết tay dễ tạo ra ruleset mà ufw status không phản ánh đúng thực tế đang chạy.

Two front ends, one kernel subsystem underneathiptables, ufw, firewalldxtables syntax on the surfacenft, nativespeaks the real object modeliptables-nfta real translation layer, notan emulated fake backendlibnftnlnetlink attributes, nointermediate dialectthe ufw blind spotufw reads back only the tableit manages itselfnative nft rules written by handsit outside that tableso ufw status can disagreewith the live rulesetkernel nf_tablesone ruleset, one evaluation engine, one set of expression primitivesboth paths land here, which is why nft list ruleset can see iptables rules tooThe deepest difference is the execution model: compiled match and target modules against interpreted expressionsPick one management path and stay on it: two tools editing one ruleset overwrite each other, not because they are incompatible

10. Vận hành thực tế: file cấu hình, atomicity, debug​

Cấu hình mặc định trên Ubuntu nằm ở /etc/nftables.conf, mở đầu bằng shebang #!/usr/sbin/nft -f, cho phép chạy trực tiếp file này như 1 script (sudo /etc/nftables.conf) thay vì gọi qua nft -f. Vài directive quan trọng:

  • flush ruleset: xóa sạch mọi table/chain/rule, thường đặt ở đầu file để đảm bảo load lại từ trạng thái sạch, tránh cộng dồn rule cũ khi load file nhiều lần.
  • flush table <family> <name>: chỉ xóa rule bên trong 1 table, giữ nguyên table đó.
  • destroy table <family> <name>: xóa hẳn cả table lẫn nội dung.
  • include "drop-in.d/*.conf": nạp thêm file khác, hỗ trợ wildcard, dùng để tách ruleset lớn thành nhiều file quản lý riêng.
  • define <tên> = <giá trị>: khai báo biến symbolic, dùng lại nhiều nơi trong ruleset, thuần túy xử lý ở tầng parse nft, không phải khái niệm tồn tại trong kernel.

Khi rule không hoạt động như kỳ vọng, có 1 chuỗi công cụ debug đi từ cao xuống thấp đúng theo 3 lớp kiến trúc ở mục 2:

  • log statement: đơn giản nhất, ghi thông tin gói tin ra kernel log (dmesg/journalctl) mỗi khi rule đó được match.
  • Cờ nftrace gắn trên rule + nft monitor trace: theo dõi đường đi thực tế của 1 gói tin cụ thể qua toàn bộ chain/rule nó chạm phải, hữu ích hơn log khi cần biết chính xác gói tin dừng lại ở rule nào, thay vì chỉ biết nó có match hay không.
  • nft --debug=netlink: xuống tận lớp 3, in ra đúng các expression object (nft_payload, nft_cmp...) mà kernel nhận được và sẽ chạy, đúng những gì mục 4 mô tả. Dùng khi nghi ngờ chính nft biên dịch sai ý định cú pháp gốc.

Nguồn tham khảo​