3.1 iptables: Kiến Trúc xtables Bên Dưới Lớp Cú Pháp
Trên một server Ubuntu dựng năm 2026, lệnh iptables -V in ra iptables v1.8.10 (nf_tables). Cái tên vẫn là iptables, nhưng chữ trong ngoặc nói rằng thứ đang thực thi bên dưới không còn là iptables nữa. Gõ tiếp một lệnh đọc ruleset mà không có quyền root, thông báo lỗi trả về là Could not fetch rule set generation id, và "generation id" vốn là khái niệm của nftables chứ không phải của iptables.
Hai dòng output đó mở ra những câu hỏi mà phần lớn tài liệu iptables không chạm tới, vì chúng dừng ở mức liệt kê -A INPUT -p tcp --dport 22 -j ACCEPT nghĩa là gì:
- Lệnh
iptables -Athật ra gửi gì xuống kernel, và gửi bằng đường nào? - Vì sao
iptables-restoređược coi là atomic, mà hai tiến trìnhiptableschạy cùng lúc vẫn có thể làm nhau thất bại? - Vì sao
nft list rulesetđọc được rule doiptablestạo ra, trong khiufwlại không đọc được rule donfttạo ra?
Tài liệu này trả lời đúng ba câu đó, đi từ kernel header và mã nguồn thay vì từ cú pháp dòng lệnh. Phạm vi dừng ở kiến trúc: không có phần hướng dẫn viết ruleset, cũng không có cấu hình mẫu để copy.
Một điểm cần nói rõ ngay, vì nó định hình cách đọc cả bài: iptables không còn là công cụ được khuyến nghị. Netfilter ghi trong wiki chính thức rằng từ tháng 6 năm 2018, bộ công cụ xtables/setsockopt cũ được coi là legacy. Red Hat đã deprecate gói iptables-nft và ipset trên RHEL 9, khuyến nghị chuyển sang nft hoặc firewalld, và ghi rõ chúng có thể bị loại khỏi bản major kế tiếp. Dù vậy ruleset iptables vẫn là thứ phải tiếp quản trên phần lớn server đang chạy, Docker vẫn sinh rule ở tầng nat, và cú pháp iptables vẫn sống tiếp qua lớp dịch iptables-nft. Hiểu kiến trúc bên dưới là cách duy nhất để đọc, debug và di trú những ruleset đó.
Mục lục
- 1. Bốn công cụ rời rạc và thế hệ xtables
- 2. Ba tầng từ terminal xuống kernel
- 3. Năm table, năm hook, và thứ tự nằm trong một enum
- 4. Match và target là kernel module biên dịch sẵn
- 5. Ruleset là một blob liền khối
- 6. Vì sao gói thứ hai không đi qua rule NAT nữa?
- 7. ipset: bản vá từ bên ngoài cho vấn đề O(n)
- 8. iptables-nft: cú pháp cũ trên kernel mới
- 9. So với nftables cho đúng bản chất
- 10. Debug theo đúng ba tầng kiến trúc
1. Bốn công cụ rời rạc và thế hệ xtables
Trước nftables, Linux lọc gói bằng bốn công cụ tách biệt: iptables cho IPv4, ip6tables cho IPv6, arptables cho ARP, ebtables cho tầng Ethernet bridge. Bốn tên gọi, bốn trang man, bốn bộ kernel module.
Chúng chia sẻ một phần hạ tầng chung, và phần chung đó mới là cái tên chính xác của thế hệ này: xtables. Kernel module x_tables giữ phần dùng chung (sổ đăng ký match và target, cơ chế duyệt rule), còn ip_tables, ip6_tables, arp_tables, ebtables là các module riêng cho từng họ giao thức, mỗi module định nghĩa table và hook của riêng nó. Quan hệ này nhìn thấy được ngay trên một máy đang chạy:
lsmod | grep -E "^(x_tables|ip_tables)"
# Output minh họa (rút gọn cột)
x_tables 65536 7 xt_conntrack,nft_compat,xt_tcpudp,xt_addrtype,xt_nat,xt_set,ip_tables
ip_tables 32768 0
Cột cuối cho thấy x_tables đang được bảy module khác dùng, trong đó có ip_tables. Đây là quan hệ nền tảng của cả bài: ip_tables không tự chứa logic match, nó vay hạ tầng của x_tables.
Hệ quả thực tế của việc tách bốn công cụ nằm ở chỗ logic không dùng lại được. Một policy cần áp cho cả IPv4 và IPv6 phải viết hai lần, bằng hai lệnh khác nhau, rồi tự đồng bộ bằng tay. Không có cách nào khai một rule cho cả hai họ, vì hai họ nằm ở hai kernel module không nói chuyện với nhau.
💡 Hình dung như thế này: xtables giống bốn phòng ban dùng chung một nhà kho vật tư. Nhà kho (
x_tables) chứa đủ dụng cụ cho mọi phòng, nhưng mỗi phòng có sổ công việc riêng, quy trình riêng, và không phòng nào đọc được sổ của phòng khác. Muốn một việc được làm ở cả hai phòng thì phải ghi vào hai quyển sổ.
2. Ba tầng từ terminal xuống kernel
Khi gõ iptables -A INPUT -p tcp --dport 22 -j ACCEPT, có ba tầng tách biệt cùng tham gia.
Tầng 1: iptables, chương trình userspace. Parse dòng lệnh, rồi với mỗi -m <tên> và mỗi -j <TÊN> nó đi nạp một shared object tương ứng, đặt tên theo quy ước libxt_<tên>.so. Shared object này biết cách biến các tham số dòng lệnh của riêng extension đó thành một struct nhị phân mà kernel module cùng tên hiểu được. Trước khi dựng struct, iptables còn hỏi kernel xem module đang chạy hỗ trợ revision nào của struct, qua hai lệnh riêng IPT_SO_GET_REVISION_MATCH và IPT_SO_GET_REVISION_TARGET. Đây là cách một userspace tool cũ vẫn làm việc được với kernel mới và ngược lại: hai bên thỏa thuận phiên bản layout dữ liệu trước khi truyền.
Tầng 2: libxtables và libiptc. libxtables lo việc nạp và gọi các shared object extension. libiptc lo việc khác hẳn và quan trọng hơn: nó giữ toàn bộ một table dưới dạng một khối byte liền nhau trong bộ nhớ của tiến trình, sửa trên khối đó, rồi dựng ra struct để gửi xuống kernel. Cơ chế này là nội dung của mục 5.
Tầng 3: kernel ip_tables. Nhận khối byte, kiểm tra tính hợp lệ, giữ nó trong bộ nhớ kernel, và duyệt nó cho mỗi gói tin đi qua netfilter hook.
Khác biệt đáng chú ý nhất nằm ở đường truyền giữa tầng 2 và tầng 3: iptables không dùng netlink. Nó dùng setsockopt và getsockopt trên một raw socket, với một dải hằng số định nghĩa trong kernel header linux/netfilter_ipv4/ip_tables.h:
#define IPT_BASE_CTL 64
#define IPT_SO_SET_REPLACE (IPT_BASE_CTL) /* 64 */
#define IPT_SO_SET_ADD_COUNTERS (IPT_BASE_CTL + 1) /* 65 */
#define IPT_SO_GET_INFO (IPT_BASE_CTL) /* 64 */
#define IPT_SO_GET_ENTRIES (IPT_BASE_CTL + 1) /* 65 */
#define IPT_SO_GET_REVISION_MATCH (IPT_BASE_CTL + 2) /* 66 */
#define IPT_SO_GET_REVISION_TARGET (IPT_BASE_CTL + 3) /* 67 */
Bốn lệnh đọc, hai lệnh ghi, hết. So sánh với nftables, nơi mọi thay đổi đi qua một subsystem netlink riêng và gói được vào batch (mục 2 bài nftables), sự khác biệt không nằm ở việc đường nào nhanh hơn. Nó nằm ở chỗ setsockopt là một lời gọi độc lập cho một table, nên không có chỗ nào trong giao diện này diễn đạt được ý "ba thay đổi trên ba table là một giao dịch".
3. Năm table, năm hook, và thứ tự nằm trong một enum
Man page của iptables 1.8.13 mô tả năm table độc lập, table nào tồn tại tùy kernel config và module đã nạp:
| Table | Chain built-in | Vai trò |
|---|---|---|
filter | INPUT, FORWARD, OUTPUT | Table mặc định khi lệnh không có -t. |
nat | PREROUTING, INPUT, OUTPUT, POSTROUTING | Chỉ được tham vấn cho gói mở connection mới. NAT cho IPv6 cần kernel từ 3.7. |
mangle | PREROUTING, OUTPUT, và từ kernel 2.4.18 thêm INPUT, FORWARD, POSTROUTING | Sửa thuộc tính gói tin. |
raw | PREROUTING, OUTPUT | Chủ yếu để loại traffic khỏi conntrack qua target NOTRACK. |
security | INPUT, OUTPUT, FORWARD | Rule mandatory access control, thường đi cùng SELinux. Chạy sau filter. |
Phía kernel thì không có khái niệm "table" theo nghĩa trên. Netfilter chỉ biết năm hook point, khai trong enum nf_inet_hooks: NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN, NF_INET_FORWARD, NF_INET_LOCAL_OUT, NF_INET_POST_ROUTING. Mỗi table của iptables, khi được nạp, đăng ký một hàm vào những hook mà nó có chain, kèm một số nguyên priority. Hook gọi lần lượt các hàm đã đăng ký theo priority tăng dần.
Nghĩa là thứ tự raw trước mangle trước nat trước filter không phải thuộc tính của table, cũng không phải quy ước của iptables. Nó là một dãy số, nằm nguyên văn trong kernel header linux/netfilter_ipv4.h:
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_RAW_BEFORE_DEFRAG = -450,
NF_IP_PRI_CONNTRACK_DEFRAG = -400,
NF_IP_PRI_RAW = -300,
NF_IP_PRI_SELINUX_FIRST = -225,
NF_IP_PRI_CONNTRACK = -200,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_SECURITY = 50,
NF_IP_PRI_NAT_SRC = 100,
NF_IP_PRI_SELINUX_LAST = 225,
NF_IP_PRI_CONNTRACK_HELPER = 300,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
NF_IP_PRI_LAST = INT_MAX,
};
Đọc dãy số này giải thích được mấy chuyện mà danh sách table ở trên không giải thích nổi.
Vì sao raw có thể NOTRACK được. raw ở -300, conntrack ở -200. Table raw chạy trước khi conntrack kịp tạo entry, nên nó là chỗ duy nhất đặt được quyết định "gói này đừng theo dõi". Đặt NOTRACK ở mangle tại -150 thì đã muộn.
Vì sao nat tách thành hai mốc. NAT_DST ở -100 và NAT_SRC ở 100, nằm hai bên FILTER ở 0. DNAT phải xong trước khi kernel quyết định định tuyến, còn SNAT phải chờ tới khi đã biết chắc gói ra interface nào. Hai nửa của cùng một table nat chạy ở hai thời điểm khác nhau.
Có những mốc không mang tên table nào. Chống phân mảnh ở -400, conntrack ở -200, conntrack helper ở 300, và conntrack confirm ở INT_MAX, tức luôn chạy cuối cùng. Những mốc này vô hình với người dùng iptables nhưng vẫn chen vào giữa các table, và chúng là lý do hành vi conntrack ở mục 6 diễn ra như vậy.
Một chi tiết đáng để ý khi đã đọc bài nftables: bảng từ khóa priority của nftables dùng đúng những con số này, raw là -300, mangle là -150, dstnat là -100, filter là 0, security là 50, srcnat là 100. nftables không phát minh thang priority mới, nó giữ nguyên thang của xtables để một base chain nftables và một table iptables cùng gắn vào một hook vẫn có thứ tự xác định.
4. Match và target là kernel module biên dịch sẵn
Một rule iptables là một chuỗi match rồi kết thúc bằng một target. Điểm quyết định tính chất của cả hệ thống nằm ở chỗ mỗi match và mỗi target là một đơn vị code riêng, biên dịch sẵn, tồn tại thành cặp: một kernel module và một shared object userspace.
Lấy ví dụ một rule có ba thành phần:
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
-p tcp --dport 22được phục vụ bởi kernel modulext_tcpudpvà parser userspacelibxt_tcp.so.-m conntrack --ctstate NEWđược phục vụ bởixt_conntrackvàlibxt_conntrack.so.-j ACCEPTlà verdict dựng sẵn trongip_tables, không cần module riêng.
Danh sách module đã nạp trên một máy đang chạy cho thấy đúng cấu trúc đó, mỗi tên xt_* là một khả năng match hoặc một target đang được dùng:
lsmod | grep -E "^xt_"
# Output minh họa (rút gọn cột)
xt_nat 12288 0
xt_tcpudp 16384 0
xt_conntrack 12288 0
xt_MASQUERADE 16384 0
xt_set 20480 0
xt_addrtype 12288 0
Mỗi module này, lúc nạp, tự đăng ký vào sổ của x_tables. Kernel duyệt rule bằng cách gọi lần lượt hàm C của từng match đã biên dịch, dừng ở match đầu tiên không khớp, và nếu khớp hết thì gọi hàm của target.
Hệ quả là chi phí mở rộng rất cao: thêm một kiểu match mới nghĩa là viết, biên dịch và phân phối một kernel module mới, cộng một shared object userspace đi kèm, cộng cơ chế thỏa thuận revision ở mục 2 để không vỡ tương thích. nftables đi theo hướng khác ở đúng điểm này: thay vì một module cho mỗi loại match, nó có một tập nhỏ expression nguyên thủy mà userspace ghép lại, nên một khả năng match mới thường chỉ cần tổ hợp lại thứ sẵn có (mục 4 bài nftables).
5. Ruleset là một blob liền khối
Đây là chỗ kiến trúc xtables khác nftables sâu nhất, và là nguyên nhân gốc của gần như mọi giới hạn vận hành của iptables.
Một rule trong kernel là struct ipt_entry, và cách nó được lưu nói lên tất cả:
struct ipt_entry {
struct ipt_ip ip;
unsigned int nfcache;
/* Size of ipt_entry + matches */
__u16 target_offset;
/* Size of ipt_entry + matches + target */
__u16 next_offset;
unsigned int comefrom;
struct xt_counters counters;
/* The matches (if any), then the target. */
unsigned char elems[];
};
Không có con trỏ tới rule kế tiếp, cũng không có danh sách liên kết. Chỉ có next_offset, một số byte. Các rule nằm sát nhau trong một mảng byte liền khối, và đi từ rule này sang rule sau nghĩa là cộng next_offset vào con trỏ hiện tại. Phần match và target thì nằm trong elems[], dài bao nhiêu tùy rule, định vị bằng target_offset.
Toàn bộ một table là một khối như vậy. Khi thay nó, userspace gửi xuống struct ipt_replace:
struct ipt_replace {
char name[XT_TABLE_MAXNAMELEN]; /* which table */
unsigned int valid_hooks; /* bitmask, "You can't change this" */
unsigned int num_entries;
unsigned int size;
/* Hook entry points, as byte offsets into the blob below. */
unsigned int hook_entry[NF_INET_NUMHOOKS];
unsigned int underflow[NF_INET_NUMHOOKS];
/* Number of counters (must be equal to current number of entries). */
unsigned int num_counters;
struct xt_counters *counters;
struct ipt_entry entries[]; /* hang off end */
};
Đọc struct này ra được mấy điều.
Chain không phải một đối tượng. hook_entry[] chỉ là mảng offset byte: INPUT là "bắt đầu đọc từ byte thứ N của khối". Chain built-in không tồn tại như một thực thể riêng, nó là một điểm vào trong cùng một mảng.
Mỗi thay đổi là ghi đè cả table. Không có lệnh "chèn một rule". Thêm một dòng vào INPUT nghĩa là: đọc cả table ra bằng IPT_SO_GET_INFO rồi IPT_SO_GET_ENTRIES, chèn vào bản sao trong bộ nhớ tiến trình, tính lại mọi offset, rồi gửi cả khối mới xuống bằng IPT_SO_SET_REPLACE. Một table có năm mươi nghìn rule thì mỗi lệnh iptables -A sao chép năm mươi nghìn rule hai lần. Đây là lý do iptables-restore tồn tại: nó gom mọi thay đổi vào đúng một lần replace.
Việc ghi là atomic, nhưng chu trình quanh nó thì không. Kernel đổi khối mới vào dưới table lock, nên một gói tin luôn thấy trọn khối cũ hoặc trọn khối mới, không bao giờ thấy nửa nọ nửa kia. Nhưng hãy đọc lại dòng comment trong header: num_counters phải bằng số entry hiện tại. Trường này tồn tại để chuyển counter cũ sang khối mới, và nó buộc bên gửi phải biết chính xác table đang có bao nhiêu rule. Nếu một tiến trình khác chèn thêm rule trong khoảng giữa bước đọc và bước ghi, con số không còn khớp, và kernel từ chối lần replace đó.
Đó là lý do iptables cần một lock ở tầng userspace. Man page ghi rõ: iptables lấy exclusive lock lúc khởi động để chặn hai instance chạy song song, không lấy được thì thoát luôn. Cờ -w cho phép chờ thay vì thoát, chờ vô hạn hoặc chờ số giây chỉ định. File lock là /run/xtables.lock, đổi được qua biến môi trường XTABLES_LOCKFILE.
💡 Hình dung như thế này: sửa một table iptables giống sửa một tài liệu không có chức năng chỉnh sửa trực tiếp. Mỗi lần đổi một dòng, phải tải cả tài liệu về máy, sửa bản sao, rồi upload đè lên bản gốc. Lúc upload thì nhanh và trọn vẹn, không ai đọc được bản nửa vời. Nhưng nếu người khác cũng vừa upload một bản khác trong lúc đang sửa, bản upload sau sẽ bị từ chối vì không còn khớp với cái đang có trên server.
So với giao dịch nguyên tử của nftables, khác biệt rõ ở hai mức. nftables có transaction thật phía kernel, gom nhiều loại object khác nhau trên nhiều table vào một batch, và dùng generation ID để rollback mà không cần khóa toàn cục. xtables chỉ atomic cho một table, và an toàn khi có nhiều tiến trình thì phải trông vào một file lock bên ngoài kernel.
6. Vì sao gói thứ hai không đi qua rule NAT nữa?
Man page nói table nat chỉ được tham vấn cho gói mở connection mới. Câu đó đúng nhưng chưa giải thích gì, và nó là nguồn của rất nhiều cấu hình NAT "cú pháp đúng mà không chạy".
Phía kernel, trạng thái một gói tin so với bảng theo dõi connection nằm trong enum ip_conntrack_info:
enum ip_conntrack_info {
IP_CT_ESTABLISHED, /* part of an established connection, either direction */
IP_CT_RELATED, /* like NEW, but related to an existing connection */
IP_CT_NEW, /* started a new connection to track */
IP_CT_IS_REPLY, /* >= this indicates reply direction */
IP_CT_ESTABLISHED_REPLY = IP_CT_ESTABLISHED + IP_CT_IS_REPLY,
IP_CT_RELATED_REPLY = IP_CT_RELATED + IP_CT_IS_REPLY,
/* No NEW in reply direction. */
IP_CT_NUMBER,
};
Ba trạng thái cơ bản, cộng biến thể cho chiều trả về, và một comment đáng chú ý: không có NEW ở chiều reply. Hai tên quen thuộc ở tầng dòng lệnh là INVALID và UNTRACKED không nằm trong enum này, chúng là khái niệm ở tầng userspace của module conntrack, không phải một giá trị state trong kernel.
Ghép với thang priority ở mục 3, vòng đời đầy đủ của một gói mở connection diễn ra như sau. Chống phân mảnh chạy ở -400. Table raw ở -300, nơi duy nhất kịp nói "đừng theo dõi gói này". Conntrack ở -200 tra bảng, không thấy entry nào khớp nên gán state NEW. Rồi mangle ở -150, nat nửa DNAT ở -100, filter ở 0, security ở 50, nat nửa SNAT ở 100. Cuối cùng conntrack confirm ở INT_MAX ghi entry mới vào bảng, kèm theo cả phép biến đổi địa chỉ mà table nat vừa quyết định.
Gói thứ hai của cùng connection đi lại đúng chuỗi hook đó, nhưng khi tới conntrack ở -200 thì lần này tra bảng có kết quả. State thành ESTABLISHED, và vì entry đã lưu sẵn phép biến đổi, kernel áp lại phép biến đổi đó trực tiếp. Table nat không được đánh giá nữa, không rule nào trong đó chạy.
Hai hệ quả vận hành rút ra từ đây. Thứ nhất, counter trên rule NAT đếm số connection, không phải số gói, nên một rule DNAT phục vụ hàng triệu gói vẫn có thể hiện số nhỏ. Thứ hai, sửa rule NAT không ảnh hưởng tới các connection đang mở, vì mỗi connection đã mang theo phép biến đổi cũ trong entry conntrack của riêng nó.
7. ipset: bản vá từ bên ngoài cho vấn đề O(n)
Ngôn ngữ của iptables không có khái niệm tập hợp. Muốn chặn năm mươi nghìn địa chỉ thì cách duy nhất trong iptables thuần là năm mươi nghìn entry, mỗi entry một địa chỉ.
Giá phải trả có hai mặt, và cả hai đều suy ra được từ mục 5. Về thực thi, kernel duyệt chain tuần tự nên trong trường hợp xấu nhất mỗi gói tin phải đi qua gần hết danh sách, độ phức tạp O(n) theo số rule. Về quản trị, khối byte phình to theo số rule, và mỗi lệnh iptables -A lại sao chép cả khối đó hai lần.
ipset ra đời để giải đúng chuyện này, bằng cách đứng hẳn bên ngoài. Nó là một kernel subsystem riêng với cấu trúc dữ liệu riêng (hash, bitmap, cây), có lệnh quản trị riêng là ipset. iptables nối vào nó qua một match extension bình thường, xt_set:
# tạo set và nạp phần tử, hoàn toàn ngoài ruleset iptables
ipset create denylist hash:ip
ipset add denylist 198.51.100.20
# trong iptables chỉ còn đúng một entry tham chiếu tới set
iptables -A INPUT -m set --match-set denylist src -j DROP
Một entry duy nhất trong khối byte, chi phí tra cứu không tăng theo số phần tử, và thêm bớt phần tử không đụng tới ruleset nên không kéo theo chu trình đọc ghi cả table.
Điểm cần nhớ là ipset không phải một phần của iptables. Nó là công cụ thứ hai, với bộ lệnh thứ hai, file lưu thứ hai, và vòng đời thứ hai phải tự lo khi backup hay khởi động lại máy. Đây đúng là chỗ nftables đi theo hướng khác: set và map là thành phần hạng nhất của chính ngôn ngữ, tham chiếu được ngay trong rule và nằm chung một ruleset (mục 6 bài nftables). Cũng đáng lưu ý là ipset nằm trong cùng danh sách deprecated của RHEL 9 với iptables-nft.
8. iptables-nft: cú pháp cũ trên kernel mới
Trên một Ubuntu hiện hành, /usr/sbin/iptables không phải một binary. Nó là một symlink đi qua hệ thống alternatives:
update-alternatives --display iptables
# Output minh họa (rút gọn)
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
/usr/sbin/iptables-legacy - priority 10
/usr/sbin/iptables-nft - priority 20
Hai backend cùng được cài, iptables-legacy ưu tiên 10 và iptables-nft ưu tiên 20, nên chế độ auto chọn backend nft. Cả hai đều là symlink tới một binary gộp, xtables-legacy-multi và xtables-nft-multi. Cách nhanh nhất để biết đang dùng cái nào là đọc chữ trong ngoặc của iptables -V:
iptables -V
# iptables v1.8.10 (nf_tables)
Lớp dịch này là lớp dịch thật, không phải một bản mô phỏng: lệnh iptables được chuyển thành object nftables tương đương và nằm trong cùng ruleset mà nft quản lý. Bằng chứng xuất hiện ngay cả trong thông báo lỗi, khi chạy không đủ quyền:
iptables -nvL
# iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)
"Rule set generation id" là khái niệm generation ID của nftables, đúng cơ chế double-buffering đã mô tả ở mục 5 bài nftables. Một lệnh iptables đang đi hỏi generation ID của nf_tables.
Phần đáng chú ý hơn là chuyện gì xảy ra với những extension mà nftables không có expression tương đương. Công cụ iptables-translate cho thấy rõ ranh giới. Với rule dịch được, nó xuất ra cú pháp native, và tự thêm counter vì rule iptables luôn mang counter:
iptables-translate -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# nft 'add rule ip filter INPUT ct state related,established counter accept'
iptables-translate -A INPUT -m limit --limit 5/second --limit-burst 10 -j ACCEPT
# nft 'add rule ip filter INPUT limit rate 5/second burst 10 packets counter accept'
Với rule không dịch được, nó xuất ra một dòng bị comment:
iptables-translate -A INPUT -m string --string "bad" --algo bm -j DROP
# nft # -A INPUT -m string --string bad --algo bm -j DROP
Nhưng rule như vậy vẫn chạy được qua backend nft, và cơ chế cho phép điều đó là module nft_compat. Nó cho nf_tables gọi thẳng vào kernel module xt_* nguyên bản. Quan hệ này nhìn thấy trong danh sách module đã nạp:
lsmod | grep -E "^(nf_tables|x_tables)"
# Output minh họa (rút gọn cột)
nf_tables 364544 2 nft_compat,nft_chain_nat
x_tables 65536 7 xt_conntrack,nft_compat,xt_tcpudp,xt_addrtype,xt_nat,xt_set,ip_tables
nf_tables đang được nft_compat dùng, và x_tables cũng đang được nft_compat dùng. Hạ tầng xtables của mục 1 vẫn sống, chỉ là giờ nó được gọi từ trong nf_tables.
Một chỗ dễ nhầm, nói rõ để khỏi suy luận sai: wiki netfilter ghi rằng match recent được nftables phục vụ bằng native set, nhưng iptables-translate lại không dịch được recent, nó xuất ra dòng comment. Hai chuyện khác nhau. Khả năng tồn tại trong nftables không có nghĩa là bộ dịch tự động biết cách chuyển sang khả năng đó.
9. So với nftables cho đúng bản chất
Gộp lại những gì tám mục trên dựng ra, khác biệt giữa hai thế hệ không nằm ở cú pháp mà ở bốn điểm cấu trúc.
| Khía cạnh | xtables | nf_tables |
|---|---|---|
| Thực thi | Gọi hàm C đã biên dịch của từng module match và target. Thêm khả năng mới là thêm kernel module. | Diễn giải một danh sách expression nguyên thủy dùng chung. Thêm khả năng mới thường chỉ cần tổ hợp lại. |
| Giao dịch | Một khối byte cho mỗi table, thay cả khối. Không gì gói hai table vào một thao tác. | Transaction thật phía kernel, gom nhiều loại object trên nhiều table vào một batch. |
| Tập hợp | Không có trong ngôn ngữ. ipset là công cụ rời bên ngoài. | Set và map là thành phần hạng nhất, nằm trong cùng ruleset. |
| Họ giao thức | Bốn công cụ cho bốn họ, policy IPv4 và IPv6 viết hai lần. | Một họ inet phục vụ cả hai, chain tạo bao nhiêu cũng được. |
Phần ít tài liệu nói tới là chiều ngược lại: có những thứ xtables làm được mà nftables chưa. Theo bảng so sánh chính thức trên wiki netfilter, cập nhật tháng 3 năm 2022, nftables chưa có tương đương cho match bpf, rateest, string, u32, và cho target CHECKSUM, CT, IDLETIMER, LED, RATEEST, TTL của IPv4, NPT của IPv6, arpreply của bridge.
Một nhóm khác thì có tương đương nhưng mang tên khác và hoạt động theo cơ chế khác, nên đọc tài liệu nftables mà tìm theo tên cũ sẽ không thấy:
hashlimittrở thành statementmeter.recentvàsettrở thành native set.statistictrở thànhnft_numgen, dùng cho load balancing.limittrở thànhnft_limit, một stateful object.commentdùng khả năng userdata dựng sẵn trên rule, có từ kernel 3.15.addrtypetrở thànhnft_fib, có từ kernel 4.10.
Và một nhóm nữa đã deprecated ngay trong chính thế giới xtables, nên gặp trong ruleset cũ thì nên thay chứ đừng học theo: physdev, quota (đã có nfacct thay thế), tos cùng TOS (thay bằng dscp và DSCP), CLUSTERIP (thay bằng match cluster), và ULOG đã bị bỏ khỏi kernel tree ở bản 3.17.
Góc nhìn từ phía nftables về cùng chủ đề này nằm ở mục 9 bài nftables.
10. Debug theo đúng ba tầng kiến trúc
Công cụ debug của iptables xếp gọn vào đúng ba tầng ở mục 2, đi từ rẻ nhất tới sâu nhất.
Tầng rẻ nhất là counter, và nó luôn có sẵn. Mỗi struct ipt_entry mang theo một struct xt_counters, nên mọi rule đều đang đếm gói và byte dù không khai gì thêm. Thêm -v vào lệnh liệt kê là đọc được:
iptables -nvL INPUT --line-numbers
# Output minh họa
Chain INPUT (policy DROP 24 packets, 1440 bytes)
num pkts bytes target prot opt in out source destination
1 412 31284 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
2 8821 1204K ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
3 17 1020 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
4 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
(Output minh họa, cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)
Cột pkts trả lời được câu hỏi hay gặp nhất khi một rule "không hoạt động": rule số 4 đang ở 0 gói, nghĩa là nó chưa từng khớp lần nào, khác hẳn với trường hợp nó khớp nhưng làm sai việc. Dòng policy DROP ở đầu cũng đếm riêng, cho biết bao nhiêu gói rơi tới cuối chain mà không khớp rule nào.
Tầng giữa là ghi log từ trong kernel. Target LOG ghi thông tin gói tin ra kernel log mỗi khi rule khớp. Target TRACE đi xa hơn một bậc: nó đánh dấu gói tin để kernel ghi lại từng table, từng chain, từng rule mà gói đó đi qua, không chỉ chỗ nó khớp. Đây là cách duy nhất trả lời chính xác "gói tin dừng ở đâu" khi ruleset có nhiều chain gọi chéo nhau.
TRACE có hai ràng buộc mà man page nói rõ và hay bị bỏ qua. Thứ nhất, nó chỉ dùng được trong table raw, đặt ở table khác là không hợp lệ. Hợp lý nếu nhớ thang priority ở mục 3: raw ở -300 là chỗ sớm nhất, đánh dấu từ đó mới theo được gói qua trọn hành trình.
Thứ hai, chỗ đọc kết quả phụ thuộc backend. Với iptables-legacy, cần nạp một logging backend như ipt_LOG hoặc nfnetlink_log, và output ra kernel log theo định dạng TRACE: tablename:chainname:type:rulenum, trong đó type là rule cho rule thường, return cho rule ngầm ở cuối chain tự định nghĩa, policy cho policy của chain built-in. Với iptables-nft, target này được dịch thành expression meta nftrace của nftables, kernel gửi trace event qua netlink lên userspace, và chỗ đọc là lệnh xtables-monitor --trace chứ không phải kernel log. Nhầm chỗ đọc là lý do hay gặp khiến TRACE trông như không hoạt động trên một máy backend nft.
Tầng sâu nhất là đọc đúng thứ kernel đang giữ. iptables-save in ra ruleset đã chuẩn hóa, loại bỏ mọi khác biệt về cách gõ lệnh, nên so hai bản dump là cách chắc chắn nhất để biết hai máy có cùng policy hay không. Trên backend nft thì còn một tầng nữa: nft list ruleset cho thấy object nftables mà lệnh iptables vừa sinh ra, và đây là chỗ phát hiện ra rule nào đi qua nft_compat thay vì thành expression native. Công cụ tương ứng ở phía nftables nằm ở mục 10 bài nftables.
Ba tầng trên nên dùng đúng theo thứ tự đó khi vận hành. Counter trả lời được phần lớn câu hỏi mà không cần bật thêm gì, vì nó vốn đã chạy sẵn. LOG và TRACE chỉ cần tới khi counter cho thấy rule có khớp nhưng kết quả vẫn sai. Còn tầng đọc object kernel để dành cho lúc nghi ngờ chính lớp dịch ở mục 8 hiểu sai ý định của lệnh gốc.
Nguồn tham khảo
- Năm table độc lập, chain built-in của từng table, hành vi chỉ tham vấn
natcho gói mở connection mới, cờ-w/-W, file lock/run/xtables.lockvà biếnXTABLES_LOCKFILE— 2026-10-23, man iptables(8), bản 1.8.13 - Danh sách match extension và target extension — 2026-10-23, man iptables-extensions(8), bản 1.8.13
- Hằng số
IPT_BASE_CTL,IPT_SO_SET_REPLACE,IPT_SO_SET_ADD_COUNTERS,IPT_SO_GET_INFO,IPT_SO_GET_ENTRIES,IPT_SO_GET_REVISION_MATCH,IPT_SO_GET_REVISION_TARGET, cùngstruct ipt_entry,struct ipt_replace,struct ipt_getinfo— 2026-10-23, kernel headerlinux/netfilter_ipv4/ip_tables.h, mã nguồn upstream enum nf_ip_hook_prioritiesvà giá trị số của từng mốc từ-450tớiINT_MAX— 2026-10-23, kernel headerlinux/netfilter_ipv4.henum nf_inet_hooksvới năm hook point, vàenum ip_conntrack_infovới các trạng thái conntrack phía kernel — 2026-10-23, kernel headerlinux/netfilter.hvàlinux/netfilter/nf_conntrack_common.h- Trạng thái legacy của bộ công cụ xtables/setsockopt từ tháng 6 năm 2018, công cụ
iptables-translatevàiptables-restore-translate, cảnh báo không dùng lẫnnftvới công cụ legacy — 2026-10-23, nftables wiki: Moving from iptables to nftables - Danh sách extension nftables chưa có tương đương, extension map sang cơ chế khác (
hashlimitsang meter,recentvàsetsang native set,statisticsangnft_numgen,limitsangnft_limit,commentsang userdata từ kernel 3.15,addrtypesangnft_fibtừ kernel 4.10), extension đã deprecated trong xtables — 2026-10-23, nftables wiki: Supported features compared to xtables, cập nhật tháng 3/2022 - Gói
ipsetvàiptables-nftdeprecated trên RHEL 9, khuyến nghị chuyển sangnfthoặcfirewalld, khả năng bị loại khỏi bản major kế tiếp — 2026-10-23, Red Hat solution 6739041 - iptables là công cụ userspace cấu hình packet filtering của kernel, bản 1.8.13 phát hành 2026-03-04 — 2026-10-23, netfilter: iptables project
- Mô hình NAT nguyên bản và lý do thiết kế chỉ xét gói đầu của connection — 2026-10-23, netfilter: NAT HOWTO, tài liệu chính thức thời kỳ kernel 2.4
- Target
TRACEchỉ dùng được trong tableraw; vớiiptables-legacycần nạp logging backendipt_LOGhoặcnfnetlink_logvà output theo định dạngTRACE: tablename:chainname:type:rulenumvớitypelàrule/return/policy; vớiiptables-nfttarget được dịch thành expressionmeta nftracevà đọc bằngxtables-monitor --trace— 2026-10-23, man iptables-extensions(8), bản 1.8.13, manxtables-monitor(8)bản 1.8.10 - Cơ chế bảng theo dõi connection của netfilter — 2026-10-23, Pablo Neira Ayuso, Netfilter's Connection Tracking System