iptables hay nftables trên Ubuntu: kiểm tra thế nào?
Trên Ubuntu 20.04+, iptables thường ghi rule nftables. Kiểm tra backend, đọc ruleset native và tránh xung đột giữa ufw với Docker trên VPS của bạn.
iptables và nftables trên Ubuntu: máy chủ của bạn đang chạy cái nào?
Trên Ubuntu 20.04 trở lên, lệnh iptables là một front end ghi các rule nftables. Trong kernel chỉ có một bộ lọc packet đang chạy là nftables, còn hai lệnh ở user space cùng lập trình cho nó. Một dòng iptables -A INPUT vẫn hoạt động chính xác như trước, và rule mà nó tạo ra là một rule nftables có thể được nft in ra.
Hãy xác nhận điều đó trên chính server của bạn trước khi tin vào nhận định này.
iptables -V
sudo update-alternatives --display iptables
sudo nft list rulesetTrên Ubuntu 24.04 (iptables 1.8.10, tính đến tháng 8 năm 2026), iptables -V in ra iptables v1.8.10 (nf_tables). Tên trong ngoặc vuông là backend. (nf_tables) nghĩa là lệnh đang giao tiếp với nftables. (legacy) nghĩa là backend x_tables cũ, backend mà Ubuntu vẫn phát hành dưới tên iptables-legacy và kernel vẫn duy trì như một ruleset hoàn toàn riêng biệt. update-alternatives in ra symlink đứng sau lựa chọn đó: link currently points to /usr/sbin/iptables-nft.
Trên một VPS mới chưa cấu hình firewall, sudo nft list ruleset không in gì. Output rỗng đó là baseline của bạn. Hãy thêm một rule theo cách cũ rồi kiểm tra lại.
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo nft list ruleset# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
chain INPUT {
type filter hook input priority filter; policy accept;
tcp dport 22 counter packets 0 bytes 0 accept
}
chain FORWARD {
type filter hook forward priority filter; policy accept;
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
}
}Rule iptables của bạn là một rule nftables. iptables-nft đánh dấu các table mà nó tạo ra, còn nft in cảnh báo đó khi thấy dấu đánh dấu này, vì chỉnh sửa table như vậy bằng nft sẽ khiến hai công cụ cùng quản lý một ruleset. Hãy xem một lệnh đã tạo ra gì: một table mà bạn không đặt tên và các chain mà bạn không yêu cầu. Đó là mô hình cũ, và đây là điều đầu tiên thay đổi khi bạn ghi trực tiếp các rule nftables.
Những gì iptables -L che giấu
iptables -L chỉ hiển thị bảng filter. Các rule NAT (network address translation) cần iptables -t nat -L, còn các rule mangle cần -t mangle. IPv6 dùng một command riêng là ip6tables, với bản sao riêng của mọi rule. Vì vậy, một máy có thể trông sạch trong một danh sách, trong khi một bảng mà bạn chưa kiểm tra lại đang drop hoặc rewrite packet.
sudo nft list ruleset in toàn bộ family, table, chain và rule trong một output. Trên server do người khác dựng, đây là command nhanh nhất để biết thực sự đang được load những gì. Thêm -a để in rule handle; bạn cần các handle này nếu muốn xóa một rule thay vì xóa cả chain.
Có hai thói quen nên sửa ngay. iptables -L phân giải địa chỉ và port thành tên, nên trên máy có resolver bị lỗi, command này trông như đã bị treo: hãy dùng iptables -nvL. Đồng thời xác nhận legacy back end không còn rule nào bằng sudo iptables-legacy -nvL, vì nếu cả hai back end đều có rule, kernel sẽ evaluate cả hai và không danh sách nào cho bạn thấy toàn bộ trạng thái.
Các table và chain bạn tự tạo, không phải kế thừa
nftables bắt đầu với cấu hình trống. Không có table filter cho đến khi bạn tạo nó, và từ filter chỉ là tên do bạn đặt. Một chain chỉ nhận packet khi bạn gán cho nó type, hook và priority; khi đó nó trở thành base chain. Chain không có các thuộc tính này chỉ được xử lý khi có jump hoặc goto tường minh, nên không tốn tài nguyên cho đến khi có rule nhảy đến nó.
Thay đổi lớn khác là family inet. Một table inet xử lý cả IPv4 và IPv6 bằng cùng một bộ rule. Điều này loại bỏ một nhóm lỗi thường gặp, trong đó một port bị đóng ở iptables nhưng lại mở hoàn toàn ở ip6tables. Sự không nhất quán này phổ biến đến mức có riêng một failure mode trên các máy dùng ufw.
Đây là một ruleset hoàn chỉnh cho server. Lưu nó vào /etc/nftables.conf.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set admin_ips {
type ipv4_addr
flags interval
elements = { 203.0.113.5, 198.51.100.0/24 }
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
ip protocol icmp accept
meta l4proto ipv6-icmp accept
tcp dport 22 ip saddr @admin_ips counter accept
tcp dport { 80, 443 } counter accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}Đọc kỹ dòng 2 hai lần. flush ruleset xóa mọi table trên máy, kể cả các table do ufw và Docker tự tạo. Hãy đọc tiếp trước khi chạy lệnh này trên server đang hoạt động.
Rule đầu tiên trong input chain xử lý phần lớn công việc. ct state established,related accept cho phép packet phản hồi của các connection do bạn khởi tạo quay trở lại, nên phần còn lại của chain chỉ cần quyết định về các connection mới. ct state invalid drop loại bỏ các packet không khớp với connection đã biết và cũng không phải là khởi đầu hợp lệ. Mọi thứ sau đó đều là các ngoại lệ được cho phép tường minh, còn policy drop xử lý phần còn lại.
Hãy kiểm tra file trước khi load và giữ một phiên SSH thứ hai mở trong lúc thực hiện. policy drop cộng với chỉ một lỗi đánh máy trong rule SSH có thể khiến bạn bị khóa khỏi chính server của mình.
sudo nft -c -f /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo nft list rulesetnft -c -f phân tích file và báo lỗi mà không load bất kỳ thứ gì. Nếu cú pháp hợp lệ, lệnh không in ra output nào.
Các set thay thế danh sách rule dài
tcp dport { 80, 443 } là một set ẩn danh: chỉ cần một rule và một lần tra cứu, thay vì một rule cho từng port. Một set có tên như admin_ips còn linh hoạt hơn, vì bạn có thể thay đổi set khi firewall vẫn đang chạy.
sudo nft add element inet filter admin_ips { 203.0.113.9 }
sudo nft list set inet filter admin_ips
sudo nft delete element inet filter admin_ips { 203.0.113.9 }Không cần reload, không cần đánh số lại rule, và thao tác match vẫn chỉ là một lần tra cứu dù set chứa năm địa chỉ hay năm mươi nghìn địa chỉ. flags interval cho phép set chứa các dải địa chỉ và prefix CIDR (classless inter-domain routing) như 198.51.100.0/24. Nếu không có flag đó, set chỉ nhận từng địa chỉ riêng lẻ và việc nạp prefix sẽ thất bại.
Set cũng có thể tự hết hạn các phần tử.
set banned {
type ipv4_addr
flags timeout
timeout 1h
}Với một rule ip saddr @banned drop, mỗi phần tử sẽ tự xóa sau một giờ kể từ khi được thêm vào. Đó là cách action nftables trong fail2ban trên Ubuntu 24.04 chặn một địa chỉ: nó thêm một phần tử vào set, không thêm một rule. Nếu port vẫn còn mới với bạn, hãy bắt đầu từ port thực sự là gì trên Linux.
Có một khác biệt dễ gây nhầm lẫn khi chuyển đổi. nftables không đếm packet trừ khi bạn yêu cầu. iptables -nvL luôn hiển thị counter cho mọi rule. Trong nftables, chỉ các rule có keyword counter mới có số đếm, vì vậy hãy đặt counter vào mọi rule mà bạn dự kiến sẽ debug sau này.
Cách hook và priority quyết định thứ tự
Một base chain khai báo một hook, tức là điểm trong đường đi của packet nơi chain đó chạy. prerouting chạy trước quyết định định tuyến. input chạy với các packet có đích là máy này. forward chạy với các packet được định tuyến đi qua máy này. output chạy với các packet do process cục bộ tạo ra. postrouting chạy sau cùng, ngay trước khi packet rời khỏi máy.
Priority sắp xếp các chain trong cùng một hook, theo thứ tự số nhỏ trước. nftables đặt tên cho các giá trị kinh điển: raw là -300, mangle là -150, dstnat là -100, filter là 0, srcnat là 100. Viết priority filter; tương đương với viết priority 0;.
Phần sau quyết định việc dùng chung các công cụ có hoạt động hay không. Mọi base chain được đăng ký trên một hook đều chạy theo thứ tự priority. Một packet được accept trong chain của bạn vẫn chưa kết thúc: accept chỉ kết thúc chain đó, còn packet tiếp tục đi đến base chain tiếp theo trên cùng hook. drop luôn có hiệu lực cuối cùng và dừng packet ngay lập tức. Vì vậy, một rule cho phép trong table của bạn không thể hủy một drop trong table của ufw, bất kể chain nào chạy trước, và accept cũng không bảo vệ được bạn khỏi một chain chạy sau.
Hai base chain trên cùng một hook với cùng priority sẽ chạy theo thứ tự đăng ký. Thứ tự này phụ thuộc vào service nào khởi động trước. Thứ tự đó có thể thay đổi sau mỗi lần reboot. Nếu phải chạy table của riêng bạn cùng với ufw, hãy gán cho table đó một priority riêng để thứ tự được xác định rõ thay vì phụ thuộc vào việc service nào đăng ký trước.
Vì sao không có rule reverse NAT để ghi?
Đây là câu hỏi mà nhiều người trả lời sai nhất, nên câu trả lời trực tiếp là: connection tracking tự ghi bản dịch ngược cho bạn. Bạn không cần thêm rule thứ hai.
Một nat table thực hiện cả hai phần việc thường gặp trên VPS có dạng như sau.
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "enp1s0" tcp dport 8080 dnat ip to 10.0.0.5:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.0.0/24 oifname "enp1s0" masquerade
}
}Chỉ packet đầu tiên của một connection được kiểm tra theo nat chain. Khi một rule khớp, kernel lưu bản dịch đó vào connection tracking table cùng với entry của connection. Mọi packet tiếp theo, theo cả hai hướng, đều được rewrite dựa trên entry đã lưu; không có rule nào được đọc lại. Cài tool conntrack rồi xem một entry đang hoạt động.
sudo apt install -y conntrack
sudo conntrack -L -p tcptcp 6 431999 ESTABLISHED src=198.51.100.20 dst=203.0.113.10 sport=54321 dport=8080 src=10.0.0.5 dst=198.51.100.20 sport=80 dport=54321 [ASSURED] mark=0 use=1Hãy đọc kết quả này thành hai tuple. 4 field đầu tiên là connection theo thông tin client đã gửi, với đích là 203.0.113.10:8080, địa chỉ public của bạn. 4 field tiếp theo là reply mà kernel chờ nhận, đã được đảo chiều và dịch sẵn, đến từ 10.0.0.5:80, backend thực. Tuple thứ hai chính là reverse rule. Kernel đã ghi nó khi packet đầu tiên khớp.
Vì vậy, không được viết rule cho chiều trả về. Rule đó không thể khớp vì packet trả về thuộc một connection đã được thiết lập và không bao giờ đi qua nat chain. Nếu bằng cách nào đó nó vẫn khớp, bạn sẽ dịch một packet mà kernel đã xử lý sẵn.
Vị trí của rewrite cũng được xác định bởi cùng cơ chế này. Destination translation phải chạy trong prerouting, trước quyết định routing, vì routing phải thấy destination mới; nếu không, packet sẽ đi sai nơi. Traffic do chính box tạo ra được xử lý trong hook output vì cùng lý do. Source translation, bao gồm cả rewrite source port, phải chạy trong postrouting, sau khi routing đã chọn outgoing interface. masquerade lấy địa chỉ từ interface đó, mà interface chỉ được xác định sau khi routing chạy.
Vì vậy, rule như dưới đây phải nằm ở cuối đường đi và không được đặt ở vị trí khác.
ip saddr 10.0.0.0/24 oifname "enp1s0" snat ip to 203.0.113.10:20000-30000Port range sẽ rewrite source port cùng với source address. Đây là điều bạn cần khi nhiều client nội bộ dùng chung một public address và source port của chúng bị trùng. Reply đến một port trong range đó, conntrack đối chiếu packet với entry, rồi khôi phục source port ban đầu trước khi chuyển packet đến nơi nhận. Một lần nữa, không cần rule thứ hai.
Một hệ quả thực tế là thay đổi NAT rule không làm thay đổi các connection đã tồn tại, vì bản dịch của chúng đã được lưu. Chúng tiếp tục hoạt động theo cấu hình cũ cho đến khi entry hết hạn. sudo conntrack -D -p tcp --dport 8080 xóa các entry khớp và sudo conntrack -F xóa toàn bộ entry. Hãy thận trọng với lệnh thứ hai trên NAT box, vì các bản dịch đã lưu này giữ cho connection hiện tại hoạt động; flush chúng sẽ làm mọi connection đi qua box bị ngắt cùng lúc.
ufw và Docker đều tự ghi rule
ufw là front end của iptables, còn trên Ubuntu, iptables là front end của nftables. Vì vậy, một máy chạy ufw có một bảng ip filter chứa đầy các chain có tên ufw-before-input, ufw-user-input, v.v., cùng với một bản sao ip6 filter của cùng cấu trúc đó. Dùng sudo nft list ruleset | grep ufw để xem. Các chain này được tạo từ những file trong /etc/ufw, và ufw reload ghi lại chúng từ đầu. Vì vậy, rule iptables viết thủ công thêm vào phía trên sẽ biến mất ở lần reload tiếp theo. Kiến thức cơ bản về ufw cho VPS giải thích cấu trúc các file đó.
Docker tự cấu hình firewall và không tham chiếu đến ufw. Khi publish một port bằng -p 80:80, Docker ghi một rule DNAT vào bảng nat và một rule accept vào đường forward. Cả hai đều chạy trước các chain do người dùng cấu hình trong ufw. Kết quả thường khiến mọi người bất ngờ một lần: ufw deny 80 đã được load nhưng container vẫn có thể truy cập từ Internet. Cách xử lý nằm ở chain DOCKER-USER mà Docker dành riêng cho các rule của bạn; bài vì sao container Docker bỏ qua ufw giải thích chi tiết. Dùng sudo nft list ruleset | grep -i docker để xem các rule trên máy.
Bây giờ đọc lại dòng flush ruleset trong cấu hình ở trên. Dòng này xóa mọi table, bao gồm cả các table do hai công cụ đó quản lý. Trên máy chạy Docker, các port đã publish sẽ ngừng hoạt động cho đến khi sudo systemctl restart docker dựng lại các chain. Đây là cách phổ biến nhất khiến người dùng tự làm các service của mình ngừng hoạt động khi dọn dẹp firewall.
Các rule vẫn còn sau khi reboot
Không ruleset nào tự có tính persistent. Kernel xóa toàn bộ khi shutdown, và mỗi bên giải quyết việc này bằng một package riêng.
Với nftables, /etc/nftables.conf được nftables.service đọc. Ubuntu mặc định tắt service đó, nên hãy kiểm tra trước khi tin rằng nó đang hoạt động.
systemctl is-enabled nftables
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftablesVới iptables, package là iptables-persistent. Package này cài netfilter-persistent và lưu vào /etc/iptables/rules.v4 cùng /etc/iptables/rules.v6.
sudo apt install -y iptables-persistent
sudo netfilter-persistent saveKhông chạy cả hai. Hai file cùng được dùng để lưu firewall sẽ dần lệch nhau, và ruleset được load sau sẽ thắng theo cách không thể xác định chỉ bằng cách đọc một trong hai file.
Dump một ruleset đang chạy cũng có một bẫy liên quan. sudo nft -s list ruleset > /etc/nftables.conf ghi lại mọi thứ đang được load tại thời điểm đó, bao gồm các table của ufw và Docker. Nếu restore bản dump đó khi boot, bạn sẽ có một bản sao cố định của các rule mà những công cụ này vốn tự xây dựng, rồi có thêm một bản sao thứ hai khi chúng khởi động. Chỉ dump table của bạn bằng sudo nft -s list table inet filter. Flag -s loại bỏ các counter, vì chúng không thuộc về file cấu hình.
Bạn có nên bật native firewall trên VPS không?
Cứ để ufw nếu bạn không cần tính năng mà ufw không thể biểu diễn. ufw đáp ứng nhu cầu thông thường trên VPS: mặc định từ chối toàn bộ và chỉ mở một vài cổng. Thay ufw bằng một ruleset tự viết chỉ để thay thế sẽ cho bạn cùng một firewall, nhưng phải bảo trì thêm một thứ.
Hãy chuyển sang native khi bạn cần những tính năng nằm ngoài mô hình của ufw: NAT và port forwarding, các set có thể cập nhật lúc runtime, một rule áp dụng cho cả hai address family, hoặc thứ tự ưu tiên của chain do bạn tự chọn. Đây là những lý do chính đáng, và ufw không có cách đáp ứng chúng.
Nếu chuyển sang native, hãy chuyển hoàn toàn. Chạy sudo ufw disable và sudo systemctl disable --now ufw, dùng sudo nft list ruleset để xác nhận các table của ufw đã biến mất, sau đó load file của bạn. Một máy chạy ufw đồng thời có table tự viết vẫn cho phép traffic đi qua, nhưng policy đang hoạt động lúc này là hợp của hai ruleset được đánh giá theo thứ tự do quá trình khởi động service quyết định. Không ai chỉ đọc một trong hai file có thể biết chính xác máy đang xử lý traffic như thế nào.
Di chuyển ruleset iptables hiện có
iptables-translate chuyển đổi một rule và in ra dạng nftables. Lệnh này không thay đổi gì trên máy chủ.
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPTnft add rule ip filter INPUT tcp dport 22 counter acceptiptables-restore-translate -f /etc/iptables/rules.v4 thực hiện tương tự với toàn bộ ruleset đã lưu. Hãy xem output của lệnh này là bản nháp đầu tiên. Việc chuyển đổi chỉ mang tính cơ học và xử lý từng rule, vì vậy kết quả vẫn giữ tên table và chain cũ, tạo thành hai ruleset riêng cho IPv4 và IPv6, đồng thời không có các set vốn là lý do đáng để chuyển sang nftables. Hãy tự viết lại thành một table inet duy nhất, rồi kiểm tra bằng nft -c -f trước khi áp dụng lên server đang hoạt động.
Các địa chỉ trong những ví dụ này lấy từ các dải địa chỉ dành cho tài liệu 203.0.113.0/24 và 198.51.100.0/24, còn enp1s0 là tên interface. Hãy lấy giá trị của bạn từ ip route show default và ip -br addr thay vì sao chép các giá trị của tôi, vì các image Ubuntu hiện nay hiếm khi đặt tên interface là eth0.
FAQ
iptables trên Ubuntu đã lỗi thời chưa?
Lệnh này vẫn không bị loại bỏ và vẫn hoạt động trên Ubuntu 24.04. Thay đổi nằm ở phần bên dưới: iptables là front end ghi các rule nftables thông qua back end iptables-nft. Kiểm tra hệ thống của bạn bằng iptables -V. Trên 24.04, lệnh này in ra iptables v1.8.10 (nf_tables). Back end x_tables cũ vẫn được phát hành dưới dạng iptables-legacy và có ruleset hoàn toàn riêng. Vì vậy, hãy đặt rule vào một back end duy nhất, không đặt vào cả hai.
Tôi có cần thêm một rule để hoàn tác NAT trên đường quay về không?
Không. Connection tracking lưu bản dịch khi packet đầu tiên của một connection khớp với rule nat. Mọi packet tiếp theo theo cả hai hướng đều được rewrite từ entry đã lưu đó. sudo conntrack -L hiển thị việc này dưới dạng hai tuple cho mỗi connection: hướng ban đầu, sau đó là reply đã được đảo ngược. Rule viết cho hướng quay về không có tác dụng, vì các packet quay về không bao giờ đi qua nat chain.
Tôi có thể chạy ufw và rule nftables riêng cùng lúc không?
Có thể, nhưng cách này sẽ tạo ra vấn đề. Mọi base chain trên một hook đều được chạy. Vì vậy, policy đang hoạt động là kết hợp của cả hai ruleset, được sắp xếp theo priority và, nếu priority bằng nhau, theo service khởi động trước. Một drop trong một trong hai ruleset là quyết định cuối cùng. accept trong ruleset của bạn không ngăn ruleset còn lại drop cùng packet. Hãy chọn một công cụ. Nếu chọn nftables, trước tiên hãy disable ufw và xác nhận các table của ufw đã bị xóa khỏi sudo nft list ruleset.
Làm cách nào để rule nftables vẫn còn sau khi reboot Ubuntu?
Đặt ruleset vào /etc/nftables.conf, kiểm tra bằng sudo nft -c -f /etc/nftables.conf, sau đó chạy sudo systemctl enable --now nftables. Service này không được enable mặc định, vì vậy nên chạy systemctl is-enabled nftables một lần. Khi tạo file đó, chỉ dump table của bạn bằng sudo nft -s list table inet filter, vì lệnh dump list ruleset đầy đủ cũng thu thập các table mà ufw và Docker tự quản lý.