Skip to main content

OVS Phần 13: Connection Tracking — Stateful Firewall Và NAT Bằng ct()

Phần 13/21 trong loạt Open vSwitch (OVS) — nối tiếp Phần 12, nơi Mục 4 đã liệt kê sơ field ct_state (32-bit, read-only, dùng dạng +trk+new/+trk+est...) rồi hẹn lại đúng câu: "packet chưa đi qua action ct() thì mọi field connection-tracking đều bằng 0 (trạng thái 'untracked'); Bài 13 sẽ đào sâu toàn bộ cơ chế conntrack, bao gồm chính xác các flag trk/new/est/rel nghĩa là gì và khi nào dùng". Đây là bài thứ hai của Module 3 — Nâng cao, tiếp tục dùng đúng khái niệm multi-table pipeline vừa học ở Bài 12 — rule stateful gần như luôn được tách riêng ra một table trong pipeline, không nhồi chung với ACL hay L2 forward. Đọc xong, Bài 14 sẽ chuyển sang DPDK — userspace datapath tăng tốc.

Mục tiêu bài học: sau bài này bạn biết chính xác OVS triển khai connection tracking dựa trên cơ chế nào (kernel Linux nf_conntrack hay một bộ máy userspace riêng, tùy loại datapath), viết đúng cú pháp đầy đủ action ct() (table, zone, commit, force, exec, alg, nat), đọc chính xác cả 8 flag của ct_state và biết vì sao trk luôn phải là điều kiện tiên quyết trước khi đọc bất kỳ flag nào khác, và tự tay dựng được một pipeline nhiều table kết hợp ct() để mô phỏng một security-group/stateful ACL thật.

Ba câu hỏi mà bài này trả lời:

  1. OVS triển khai connection tracking dựa trên cơ chế nào — có phải lúc nào cũng là nf_conntrack của kernel Linux, và khác gì giữa datapath kernel với datapath userspace/DPDK?
  2. Action ct() có những đối số nào, zone giải quyết bài toán gì, và cú pháp NAT (nat(src=...)/nat(dst=...)) viết chính xác ra sao?
  3. ct_state có đúng bao nhiêu flag, ý nghĩa từng flag là gì, và vì sao thiếu flag trk thì mọi flag khác coi như vô nghĩa?

Mục lục


1. Từ zone-based firewall vật lý tới connection tracking trên OVS

💡 Hình dung như thế này: một firewall zone-based vật lý (kiểu Juniper SRX, Cisco ASA) chia mạng thành các zone (trust, untrust, dmz...), và mỗi security policy chỉ cần khai một chiều — "cho phép zone trust khởi tạo kết nối sang zone untrust". Traffic phản hồi (response) tự động được cho qua mà không cần một policy ngược riêng, vì thiết bị giữ một session table nội bộ ghi nhớ "kết nối này đã được permit theo chiều nào". ACL kiểu cũ trên router/switch truyền thống (dùng keyword established) chỉ mô phỏng được rất thô ý tưởng đó — nó không giữ bảng trạng thái thật, chỉ nhìn cờ TCP (ACK/RST) của từng packet riêng lẻ và đoán "packet có cờ ACK chắc là đã có handshake trước đó", một phỏng đoán per-packet, không phải bộ nhớ có trạng thái, và có thể bị đánh lừa bởi một packet tự bật sẵn cờ ACK dù chưa hề bắt tay.

ct() trên OVS đi theo đúng mô hình session-table thật của SRX/ASA, không phải mô hình đoán-qua-cờ-TCP của ACL cũ. Man page ovs-actions(7) nói thẳng đây là hướng được khuyến nghị: "Open vSwitch is often used to implement a firewall. The preferred way to implement a firewall is connection tracking, that is, to keep track of the connection state of individual TCP sessions. The ct action described in this section, added in Open vSwitch 2.5, implements connection tracking. For new deployments, it is the recommended way to implement firewalling with Open vSwitch."

Tài liệu cũng thuật lại chính xác giải pháp "thô" mà OVS từng dùng trước khi có ct() — gần giống hệt ACL established kiểu cũ vừa nói ở trên: "Before ct was added, Open vSwitch did not have built-in support for connection tracking. Instead, Open vSwitch supported the learn action, which allows a received packet to add a flow to an OpenFlow flow table. This could be used to implement a primitive form of connection tracking: packets passing through the firewall in one direction could create flows that allowed response packets back through the firewall in the other direction." Nói cách khác: trước OVS 2.5, muốn có "stateful" phải tự tay lập trình bằng action learn để tạo flow ngược — một cách vòng, không có khái niệm state thật. ct() thay thế hẳn cách làm đó bằng một bộ máy theo dõi kết nối đúng nghĩa.

Zone-Based Firewall Vật Lý vs Connection Tracking Trên OVSCùng cốt lõi: giữ một bảng trạng thái thật, không đoán qua cờ TCP từng packetZone-based firewall (SRX/ASA)OVS — action ct()Đơn vị cô lập: zone(trust / untrust / dmz)Đơn vị cô lập: zone=N(16-bit id, field ct_zone)Trạng thái: session tablegiữ ngay trong thiết bịTrạng thái: connection tracker(nf_conntrack kernel hoặc bộ riêng)Return traffic: tự permitnếu khớp session đã mởReturn traffic: matchct_state=+trk+est, không cần ACL ngượcKhác ACL "established" kiểu cũ — chỉ đoán qua cờ TCP từng packet, không có bảng trạng thái thật

2. Cơ chế bên dưới: kernel nf_conntrack hay tự triển khai riêng?

Câu trả lời chính xác: tùy loại datapath — OVS không có một "bộ máy conntrack" duy nhất, mà có hai bộ triển khai độc lập, thống nhất với nhau chỉ ở tầng cú pháp OpenFlow (ct(), ct_state...), không phải ở tầng thực thi.

  • Datapath kernel (module openvswitch.ko, đã vào Linux mainline từ kernel 3.3 theo Bài 1) không tự viết lại connection tracking — nó tận dụng thẳng hạ tầng nf_conntrack sẵn có của Linux, đúng framework mà iptables/netfilter cũng dùng. Bằng chứng trực tiếp từ mã nguồn kernel Linux: file net/openvswitch/conntrack.c — nơi cài đặt action ct cho datapath kernel — #include thẳng các header của tầng netfilter: net/netfilter/nf_conntrack_core.h, nf_conntrack_labels.h, nf_conntrack_zones.h, nf_conntrack_helper.h, nf_conntrack_timeout.h, và cả net/netfilter/nf_nat.h khi kernel bật CONFIG_NF_NAT. Đây không phải suy đoán — là quan sát trực tiếp từ đúng file mã nguồn đó trong cây kernel Linux chính thức.
  • Datapath userspace (dpif-netdev — chạy với NIC thường, AF_XDP, hoặc DPDK) không đụng gì tới nf_conntrack của kernel, vì bản thân nó không chạy trong kernel. OVS tự viết một bộ conntrack riêng hoàn toàn bằng userspace, nằm trong lib/conntrack.c của mã nguồn OVS, độc lập với Linux netfilter. Toàn bộ chi tiết implementation khác biệt này được che giấu sau một lớp trừu tượng chung tên ct-dpif (conntrack datapath interface) — nhờ vậy cú pháp ct(...)/ct_state bạn viết trong flow giống hệt nhau dù bridge chạy datapath nào, ovs-vswitchd tự chọn đúng bộ triển khai bên dưới.

Tài liệu FAQ chính thức xác nhận rõ đây là hai lộ trình phát triển tính năng tách biệt, không đồng thời: bảng tính năng theo datapath ghi "Connection tracking [Linux kernel] 4.3 [Userspace] 2.6" — nghĩa là kernel Linux 4.3 trở lên mới có đủ hạ tầng nf_conntrack để openvswitch.ko dùng action ct, còn bản thân datapath userspace tự viết (không phụ thuộc kernel) có được conntrack riêng kể từ OVS 2.6. Cùng tài liệu đó mô tả datapath userspace: "This datapath supports conventional system devices as well as DPDK and AF_XDP devices when support for those is built. This is the only datapath that works on NetBSD, FreeBSD and Mac OSX." — xác nhận thêm: DPDK chỉ là một trong các loại port mà datapath userspace hỗ trợ, không phải một datapath thứ ba tách biệt; conntrack userspace tự viết ở trên áp dụng chung cho cả port thường lẫn port DPDK/AF_XDP trong cùng datapath đó.

Trước khi đi tiếp, tự kiểm tra xem br0 của bạn (Bài 4) đang chạy datapath nào — cột datapath_type trên bảng Bridge quyết định điều đó, đúng theo schema: "Name of datapath provider. The kernel datapath has type system. The userspace datapath has type netdev."

sudo ovs-vsctl get bridge br0 datapath_type
# Output minh họa — bridge tạo mặc định từ Bài 4, chưa từng chỉ định datapath_type
""

(Output minh họa — chuỗi rỗng nghĩa là giá trị mặc định, tương đương system/kernel datapath, theo đúng phần "Cơ chế bên dưới" vừa nêu ở trên. Nếu bridge được tạo với -- set bridge br0 datapath_type=netdev, lệnh này sẽ trả về netdev — nghĩa là toàn bộ conntrack của bridge đó chạy bằng lib/conntrack.c tự viết, không đụng gì tới nf_conntrack của kernel.)

Một chi tiết vận hành đáng nhớ khi dùng datapath kernel: ovs-actions(7) cảnh báo có ít nhất một sysctl toàn cục của Linux ảnh hưởng hành vi ct: "With the Linux datapath, global sysctl options affect ct behavior. In particular, if net.netfilter.nf_conntrack_helper is enabled, which it is by default until Linux 4.7, then application layer gateway helpers may be executed even if alg is not specified. For security reasons, the netfilter team recommends users disable this option." — một minh chứng nữa cho thấy datapath kernel thật sự chia sẻ chung hạ tầng với phần còn lại của hệ thống Linux (kể cả các sysctl vốn dành cho iptables/netfilter), điều hoàn toàn không tồn tại ở datapath userspace vì nó không đụng gì tới netfilter.

Hai Bộ Conntrack Độc Lập, Thống Nhất Qua ct-dpifCùng cú pháp ct()/ct_state — nhưng thực thi hoàn toàn khác nhau bên dướiKernel — datapath_type=systemUserspace — datapath_type=netdevactions=ct(...)actions=ct(...)openvswitch.ko (kernel module)dpif-netdevnf_conntrack / nf_nat(Linux netfilter, dùng chung với iptables)lib/conntrack.c(OVS tự viết, độc lập netfilter)Thống nhất qua lớp ct-dpif — cú pháp ct()/ct_state giống hệt nhau dù chạy datapath nào

3. Cú pháp ct(): tracking, commit, zone, table, force, exec, alg

Man page ovs-actions(7) định nghĩa hai dạng cú pháp:

ct([argument]...)
ct(commit[,argument]...)

"The action has two modes of operation, distinguished by whether commit is present." — đây là điểm quan trọng nhất cần nắm trước khi đọc bất kỳ đối số nào: không có commit, ct() chỉ tra cứu/khởi tạo trạng thái tạm thời rồi rẽ nhánh (fork) pipeline; commit, ct() mới thật sự ghi kết nối vào bảng theo dõi, tồn tại lâu hơn vòng đời của một packet.

Ở dạng không có commit, tài liệu mô tả chính xác cơ chế fork: "In this form, ct forks the OpenFlow pipeline: In one fork, ct passes the packet to the connection tracker. Afterward, it reinjects the packet into the OpenFlow pipeline with the connection tracking fields initialized... In the other fork, the original instance of the packet continues independent processing following the ct action. The ct_state field and other connection tracking metadata are cleared." Nói ngắn gọn: một packet đi qua ct() biến thành hai bản — bản gốc tiếp tục pipeline hiện tại nhưng field connection-tracking bị xóa sạch (nếu không còn action nào khác phía sau, bản này coi như bị bỏ), và một bản được gửi qua connection tracker rồi tiêm lại (reinject) vào table chỉ định, mang đầy đủ ct_state/ct_zone/ct_mark/ct_label đã điền.

ct() Rẽ Nhánh (Fork) Pipeline Thành HaiPacket chưa track (ct_state=0, khớp ct_state=-trk) đi vào flow có actions=ct(table=N)ct(table=N)fork gốcfork qua trackerBản gốc (untracked)tiếp tục action list hiện tại,ct_state bị xóa về 0→ hết action = bị bỏBản qua connection trackertra/tạo entry, rồi reinjectvào table=N với ct_state,ct_zone, ct_mark, ct_label đã điềntable=table: bắt buộc để reinject; không khai báo thì packet không được reinject đi đâu cả

Với dạng không có commit, các đối số hợp lệ là:

Đối sốÝ nghĩa (trích ovs-actions(7))
zone=value"A zone is a 16-bit id that isolates connections into separate domains, allowing overlapping network addresses in different zones. If a zone is not provided, then the default is 0."
table=table"Sets the OpenFlow table where the packet is reinjected. The table must be a number between 0 and 254 inclusive, or a table's name. If table is not specified, then the packet is not reinjected."
nat / nat(type=addrs[:ports][,flag]...)Khai báo SNAT/DNAT cho kết nối — chỉ thật sự có hiệu lực khi kết nối được commit sau đó. Chi tiết đầy đủ ở Mục 5.

Với dạng commit, tài liệu mô tả: "With commit, the connection tracker commits the connection to the connection tracking module. The commit flag should only be used from the pipeline within the first fork of ct without commit. Information about the connection is stored beyond the lifetime of the packet in the pipeline. Some ct_state flags are only available for committed connections." — câu cuối rất quan trọng, sẽ quay lại ở Mục 4 khi giải thích vì sao est/rpl/snat/dnat chỉ có ý nghĩa với kết nối đã commit. Các đối số chỉ dùng được cùng commit:

Đối sốÝ nghĩa (trích ovs-actions(7))
force"A committed connection always has the directionality of the packet that caused the connection to be committed in the first place... If a connection is already committed, but it is in the wrong direction, force effectively terminates the existing connection and starts a new one in the current direction."
exec(action...)"Perform each action within the context of connection tracking. Only actions which modify the ct_mark or ct_label fields are accepted within exec action." — ví dụ set_field:value->ct_mark để gắn nhãn 32-bit cho kết nối.
alg=alg"Specify application layer gateway alg to track specific connection types." Hỗ trợ ftptftp — tự động phát hiện kết nối con (ví dụ FTP data connection) sinh ra từ kết nối control, gắn flag rel cho các kết nối con đó.

Một action bổ trợ đáng biết, ra đời ngay sau ct (OVS 2.7): ct_clear — không có đối số, chỉ làm đúng một việc theo ovs-actions(7): "Clears connection tracking state from the flow, zeroing ct_state, ct_zone, ct_mark, and ct_label." Dùng khi cần "quên" hẳn trạng thái đã track giữa pipeline — ví dụ một packet đã đi qua ct() ở một table sớm, nhưng một rule sau đó (do đổi header, đổi tunnel...) muốn packet được đánh giá lại từ đầu như thể chưa từng tracked, thay vì mang theo ct_state/ct_zone cũ đi tiếp không còn đúng ngữ cảnh nữa.

zone xứng đáng dừng lại thêm một chút vì đây là khái niệm dễ bị bỏ qua nhất khi mới học: nó không phải VLAN, không phải VRF, mà là một không gian tra cứu độc lập bên trong chính connection trackerovs-fields(7) xác nhận thêm: "A connection tracking zone, the zone value passed to the most recent ct action. Each zone is an independent connection tracking context, so tracking the same packet in multiple contexts requires using the ct action multiple times." Ứng dụng thực tế rõ nhất: multi-tenant — nếu hai tenant cùng dùng dải IP 10.0.0.0/24 trùng nhau (chuyện bình thường khi mỗi tenant có VRF/logical network riêng), gộp chung một bảng conntrack (zone=0 mặc định cho tất cả) sẽ khiến OVS hiểu nhầm hai kết nối từ hai tenant khác nhau nhưng trùng 5-tuple là "cùng một kết nối". Gán mỗi tenant một zone= riêng (ví dụ zone=100 cho tenant A, zone=200 cho tenant B) tách hẳn hai bảng tra cứu, đúng như tài liệu đã nói: cho phép "overlapping network addresses in different zones" tồn tại song song mà không xung đột.


4. ct_state: 8 flag đầy đủ, và vì sao trk là điều kiện tiên quyết

Đây chính là phần Bài 12 đã hẹn lại. ovs-fields(7) định nghĩa field ct_state là 32-bit, "read-only", và giải thích cách đọc: "Matches on this field are most conveniently written in terms of symbolic names (listed below), each preceded by either + for a flag that must be set, or - for a flag that must be unset, without any other delimiters between the flags. Flags not mentioned are wildcarded." — đúng cú pháp +trk+est/-trk mà Bài 12 đã dùng mà chưa giải thích.

Toàn bộ 8 flag hiện có, trích nguyên văn định nghĩa từng flag:

FlagGiá trị bitÝ nghĩa (trích ovs-fields(7))
new0x01"A new connection. Set to 1 if this is an uncommitted connection."
est0x02"Part of an existing connection. Set to 1 if packets of a committed connection have been seen by conntrack from both directions."
rel0x04"Related to an existing connection, e.g. an ICMP 'destination unreachable' message or an FTP data connections. This flag will only be 1 if the connection to which this one is related is committed."
rpl0x08"This packet is in the reply direction, meaning that it is in the opposite direction from the packet that initiated the connection. This flag will only be 1 if the connection is committed."
inv0x10"The state is invalid, meaning that the connection tracker couldn't identify the connection." — catch-all cho lỗi (module protocol chưa nạp, packet dị dạng, sai độ dài).
trk0x20"This packet is tracked, meaning that it has previously traversed the connection tracker. If this flag is not set, then no other flags will be set. If this flag is set, then the packet is tracked and other flags may also be set."
snat0x40"This packet was transformed by source address/port translation by a preceding ct action. Open vSwitch 2.6 added this flag."
dnat0x80"This packet was transformed by destination address/port translation by a preceding ct action. Open vSwitch 2.6 added this flag."

Đúng như định nghĩa của trk vừa trích, đây chính là điều kiện tiên quyết cho mọi flag còn lại — và ovs-fields(7) liệt kê tường minh các ràng buộc theo thứ tự ưu tiên giảm dần, xác nhận lại đúng điều Bài 12 đã hẹn:

  1. "If trk is unset, no other flags are set." — packet chưa từng đi qua ct() thì ct_state toàn số 0, đúng nguyên văn lời hứa ở Bài 12.
  2. "If trk is set, one or more other flags may be set."
  3. "If inv is set, only the trk flag is also set."
  4. "new and est are mutually exclusive."
  5. "new and rpl are mutually exclusive."
  6. "rel may be set in conjunction with any other flags."

Nói cách khác: không bao giờ có chuyện match ct_state=+est mà quên +trk — về mặt logic nó vẫn đúng (vì ràng buộc 1 đã tự động đảm bảo est không thể bật nếu trk tắt), nhưng khai tường minh +trk+est giúp người đọc flow sau này không phải nhớ ràng buộc ngầm này, đúng cách các ví dụ chính thức luôn khai đầy đủ ct_state=+trk+new/ct_state=+trk+est thay vì chỉ +new/+est.

Vòng Đời Một Kết Nối Qua ct_statetrk là điều kiện tiên quyết — chưa qua ct() thì mọi flag khác đều bằng 0Untrackedct_state=0match: -trkct()New+trk+newchưa commitcommitEstablished+trk+estđã thấy cả 2 chiềuRelated+trk+relFTP data / ICMP unreach.Invalid+trk+invchỉ trk được bật kèmnew và est không bao giờ cùng bật (mutually exclusive) — cũng như new và rplrel là kết nối RIÊNG, phải tự commit lại, không tự động thừa hưởng trạng thái từ kết nối gốcsnat/dnat chỉ bật khi packet thực sự bị ct() dịch địa chỉ nguồn/đích (Mục 5)Sơ đồ minh họa nhánh chính "new → est → có thể rel" — inv là nhánh lỗi tách biệt, không nằm trên đường đi bình thường(est/rpl/snat/dnat chỉ có ý nghĩa đầy đủ với kết nối đã commit — Mục 3)

5. NAT qua conntrack: nat(src=...) và nat(dst=...)

ovs-actions(7) định nghĩa NAT là một đối số của ct() khi ở dạng không-commit, nhưng chỉ thật sự có hiệu lực khi kết nối được commit ở bước tiếp theo:

nat
nat(type=addrs[:ports][,flag]...)

"Specify address and port translation for the connection being tracked. The type must be src, for source address/port translation (SNAT), or dst, for destination address/port translation (DNAT). Setting up address translation for a new connection takes effect only if the connection is later committed with ct(commit ...)." — đây là chi tiết dễ bỏ sót nhất: viết nat(src=...) mà quên commit ngay sau đó (hoặc ở một ct() kế tiếp) thì NAT không xảy ra.

Cú pháp đầy đủ của addrsports, trích nguyên văn:

  • addrs"The IP address addr or range addr1-addr2 from which the translated address should be selected. If only one address is given, then that address will always be selected, otherwise the address selection can be informed by the optional persistent flag... IPv6 addresses must be bracketed with [ and ] if a port range is also given."
  • ports"The L4 port or range port1-port2 from which the translated port should be selected. When a port range is specified, fallback to ephemeral ports does not happen, else, it will. The port number selection can be informed by the optional random and hash flags."

Ví dụ SNAT dịch cả dải địa chỉ lẫn dải port, đúng cú pháp tài liệu mô tả:

actions=ct(commit,nat(src=10.0.0.1-10.0.0.10:1024-65535))

Và DNAT dịch về một địa chỉ/port cố định (một "virtual IP" chỉ thẳng vào một real server):

actions=ct(commit,nat(dst=10.0.0.2:80))

Ba flag tùy chọn đi kèm addrs/ports, trích nguyên văn:

  • random"The selection of the port from the given range should be done using a fresh random number. This flag is mutually exclusive with hash."
  • hash"The selection of the port from the given range should be done using a datapath specific hash of the packet's IP addresses and the other, non-mapped port number. This flag is mutually exclusive with random."
  • persistent"The selection of the IP address from the given range should be done so that the same mapping can be provided after the system restarts."

Một trường hợp đặc biệt đáng nhớ khi làm SNAT giả lập kiểu "masquerade": "For SNAT, there is a special case when the src IP address is configured as all 0's, i.e., nat(src=0.0.0.0). In this case, when a source port collision is detected during the commit, the source port will be translated to an ephemeral port. If there is no collision, no SNAT is performed." — tức nat(src=0.0.0.0) không dịch địa chỉ, chỉ dịch port và duy chỉ khi có xung đột port thật sự.

Về mặt phiên bản, hai nguồn chính thức ghi hai mốc khác nhau vì đo hai thứ khác nhau — không mâu thuẫn: ovs-actions(7) ghi "Open vSwitch 2.6 introduced nat. Linux 4.6 was the earliest upstream kernel that implemented ct support for nat." (mốc cho datapath kernel, dựa vào kernel Linux có hỗ trợ NAT trong nf_conntrack), còn bảng tính năng theo datapath ở FAQ ghi "Conntrack NAT 4.6 2.8" — nghĩa là datapath userspace tự viết riêng (Mục 2) chỉ có NAT kể từ OVS 2.8, chậm hơn hai bản so với lúc cú pháp nat xuất hiện trong action.

SNAT Và DNAT Qua ct(commit,nat(...)) — Trước/SauChỉ có hiệu lực khi đi kèm commit — thiếu commit thì địa chỉ giữ nguyênSNAT — trước ct()src=192.168.1.5:33241dst=8.8.8.8:53ct(commit,nat(src=10.0.0.1-10.0.0.10:1024-65535))SNAT — sau ct()src=10.0.0.3:5210 (đã dịch)dst=8.8.8.8:53 (không đổi)DNAT — trước ct()dst=10.0.0.100:80 (VIP)src=10.0.0.1:44210ct(commit,nat(dst=10.0.0.2:80))DNAT — sau ct()dst=10.0.0.2:80 (real server)src=10.0.0.1:44210 (không đổi)SNAT đổi địa chỉ/port NGUỒN — nhiều client share một dải IP ra ngoàiDNAT đổi địa chỉ/port ĐÍCH — một virtual IP trỏ vào một real server phía sau

Cuối cùng, alg (Mục 3) và nat có thể phối hợp: "If alg is specified for the committing ct action that also includes nat with a src or dst attribute, then the datapath tries to set up the helper to be NAT-aware. This functionality is datapath specific and may not be supported by all datapaths." — ví dụ FTP: ALG cần "hiểu" được là địa chỉ/port đã bị NAT để tự sửa lại payload FTP PORT/PASV chứa địa chỉ IP dạng text, một bài toán ALG cổ điển không tự nhiên có được nếu chỉ NAT ở tầng IP/TCP header.


6. CLI lab: pipeline 3 table — ACL, conntrack, forward

Lab này dùng lại đúng topology br0/ns1 (10.0.0.1, veth0-ovs = port 1)/ns2 (10.0.0.2, veth1-ovs = port 2) từ Bài 4, đúng cách Bài 12 Mục 8 đã tái sử dụng — nếu môi trường không còn, dựng lại theo Bài 4 trước khi tiếp tục. Mục tiêu: mô phỏng một security-group/stateful ACL — chỉ traffic khởi tạo từ ns1 (outbound) mới được commit và cho qua, ns2 chỉ được trả lời (established) chứ không được tự ý khởi tạo kết nối mới, đúng tinh thần ví dụ firewall chính thức trong ovs-actions(7):

table=0,priority=1,action=drop
table=0,priority=10,arp,action=normal
table=0,priority=100,ip,ct_state=-trk,action=ct(table=1)
table=1,in_port=1,ip,ct_state=+trk+new,action=ct(commit),2
table=1,in_port=1,ip,ct_state=+trk+est,action=2
table=1,in_port=2,ip,ct_state=+trk+new,action=drop
table=1,in_port=2,ip,ct_state=+trk+est,action=1

(Trích nguyên văn ví dụ chính thức trong ovs-actions(7).) Lab dưới đây tách thêm một table=2 riêng để làm L2 forward bằng NORMAL — đúng tinh thần "mỗi table lo đúng một việc" đã học ở Bài 12 — thay vì action=2/action=1 xuất thẳng ra port như ví dụ gốc.

Lab Mục 6 — Pipeline 3 Table: ACL → Conntrack → Forwardns1 (port 1) khởi tạo được, ns2 (port 2) chỉ được trả lời — không được tự khởi tạons110.0.0.1table=0 — ACL trước ctprio=10 arp → normalprio=100 ip,-trk→ ct(table=1)prio=1 catch-all dropreinjecttable=1 — phân loại theo ct_statein_port=1,+trk+newct(commit) → table=2in_port=1,+trk+est → table=2in_port=2,+trk+est → table=2in_port=2,+trk+new→ DROP (chặn khởi tạo ngược)goto_table:2table=2L2 forwardactions=normal(Bài 12)ns210.0.0.2ping ns1→ns2 (mới, được commit) thành công — ping ns2→ns1 khi chưa có kết nối (mới, port 2) bị drop

Bước 1 — xóa sạch flow hiện có:

sudo ovs-ofctl -O OpenFlow13 del-flows br0

Bước 2 — dựng đủ 3 table:

# table=0 — ACL trước conntrack: cho ARP đi bình thường, đẩy IP chưa track vào ct(), còn lại drop
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=0,priority=1,actions=drop"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=0,priority=10,arp,actions=normal"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=0,priority=100,ip,ct_state=-trk,actions=ct(table=1)"

# table=1 — phân loại theo ct_state: chỉ new/outbound từ ns1 (port 1) được commit, ns2 (port 2) không được tự khởi tạo
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=100,in_port=1,ip,ct_state=+trk+new,actions=ct(commit),goto_table:2"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=100,in_port=1,ip,ct_state=+trk+est,actions=goto_table:2"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=100,in_port=2,ip,ct_state=+trk+est,actions=goto_table:2"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=100,in_port=2,ip,ct_state=+trk+new,actions=drop"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=1,actions=drop"

# table=2 — L2 forward, tái dùng NORMAL đúng như Bài 12
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=2,priority=0,actions=normal"

Bước 3 — verify chiều được phép: ns1 ping ns2 (khởi tạo mới từ port 1):

sudo ip netns exec ns1 ping -c 3 -W 1 10.0.0.2
# Output minh họa — 3/3 packet thành công
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.061 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.048 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=0.044 ms

Gói ICMP echo request đầu tiên khớp table=1,in_port=1,+trk+new → được ct(commit) rồi goto_table:2NORMAL chuyển tới ns2. Gói echo reply đi ngược lại khớp table=1,in_port=2,+trk+est (đã est vì conntrack đã thấy cả hai chiều) → cho qua goto_table:2.

Bước 4 — verify chiều bị chặn: ns2 chủ động ping ns1 khi chưa có kết nối nào tồn tại theo hướng đó:

sudo ip netns exec ns2 ping -c 3 -W 1 10.0.0.1
# Output minh họa — mất gói hoàn toàn
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
--- 10.0.0.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2043ms

Gói ICMP echo request từ ns2 khớp table=1,in_port=2,+trk+new (kết nối mới, khởi tạo từ port 2) → actions=drop — đúng mô hình security-group chỉ cho phép outbound khởi tạo từ một phía.

Bước 5 — xem entry thật trong bảng conntrack:

sudo ovs-appctl dpctl/dump-conntrack
# Output minh họa — cấu trúc icmp đúng theo dòng thật trích từ test suite chính thức OVS (tests/system-traffic.at),
# id cụ thể do kernel gán ngẫu nhiên cho mỗi phiên ping, số liệu tùy máy
icmp,orig=(src=10.0.0.1,dst=10.0.0.2,id=<id>,type=8,code=0),reply=(src=10.0.0.2,dst=10.0.0.1,id=<id>,type=0,code=0),zone=0

Đây chính là bằng chứng trực tiếp: bảng conntrack ghi nhận đúng một kết nối do ns1 khởi tạo (orig là hướng đi, reply là hướng phản hồi tự động sinh ra) — không có entry nào ứng với lần ns2 ping ns1 ở Bước 4, vì gói đó bị drop ngay ở table=1 trước khi kịp ct(commit).

Bước 6 — xác nhận qua dump-flows rằng counter tăng đúng nhánh kỳ vọng:

sudo ovs-ofctl -O OpenFlow13 dump-flows br0
# Output minh họa — số liệu tùy máy
cookie=0x0, duration=30.1s, table=0, n_packets=1, n_bytes=42, priority=1 actions=drop
cookie=0x0, duration=30.1s, table=0, n_packets=0, n_bytes=0, priority=10,arp actions=normal
cookie=0x0, duration=30.1s, table=0, n_packets=6, n_bytes=588, priority=100,ip,ct_state=-trk actions=ct(table=1)
cookie=0x0, duration=30.1s, table=1, n_packets=1, n_bytes=98, priority=100,in_port=1,ip,ct_state=+trk+new actions=ct(commit),goto_table:2
cookie=0x0, duration=30.1s, table=1, n_packets=2, n_bytes=196, priority=100,in_port=1,ip,ct_state=+trk+est actions=goto_table:2
cookie=0x0, duration=30.1s, table=1, n_packets=3, n_bytes=294, priority=100,in_port=2,ip,ct_state=+trk+est actions=goto_table:2
cookie=0x0, duration=30.1s, table=1, n_packets=3, n_bytes=126, priority=100,in_port=2,ip,ct_state=+trk+new actions=drop
cookie=0x0, duration=30.1s, table=1, n_packets=0, n_bytes=0, priority=1 actions=drop
cookie=0x0, duration=30.1s, table=2, n_packets=6, n_bytes=588, priority=0 actions=normal

Đọc kỹ hai dòng dễ nhầm nhất: in_port=2,+trk+newn_packets=3 — đúng 3 gói ping bị chặn ở Bước 4; in_port=1,+trk+new chỉ có n_packets=1 — đúng một gói ICMP request đầu tiên của Bước 3 (các gói sau đã là +trk+est vì conntrack đã ghi nhận, khớp dòng in_port=1,+trk+est với n_packets=2).

Bước 7 — dùng ofproto/trace giả lập gói NEW hợp lệ và gói NEW bị chặn, không cần gửi traffic thật:

ofproto/trace không tra bảng conntrack thật khi giả lập — mã nguồn công cụ này (ofproto/ofproto-dpif-trace.c) in thẳng một dòng cảnh báo mỗi khi gặp một điểm packet cần "quay lại" từ connection tracker: "resume conntrack with default ct_state=trk|new (use --ct-next to customize)" — nghĩa là mặc định trace luôn giả định gói vừa qua ct() trở thành trk|new, trừ khi bạn tự truyền --ct-next để ép một trạng thái khác.

Gói hợp lệ (giả lập ns1 gửi tới ns2 lần đầu):

sudo ovs-appctl ofproto/trace br0 "in_port=1,icmp,nw_src=10.0.0.1,nw_dst=10.0.0.2"
# Output minh họa — cấu trúc đúng theo cơ chế trace đã trích ở trên, số liệu/port tùy máy
bridge("br0")
-------------
0. ip,ct_state=-trk, priority 100
ct(table=1)

===============================================================================
recirc(0x1) - resume conntrack with default ct_state=trk|new (use --ct-next to customize)
1. in_port=1,ct_state=+trk+new, priority 100
ct(commit)
goto_table:2
2. priority 0
normal
no learned MAC for destination, flooding

Datapath actions: ct(commit),2

Packet mặc định được trace coi là +trk+new ngay sau ct() → khớp đúng rule commit ở table=1 → chuyển tiếp table=2NORMAL.

Gói bị chặn (giả lập ns2 chủ động khởi tạo, không có kết nối trước đó):

sudo ovs-appctl ofproto/trace br0 "in_port=2,icmp,nw_src=10.0.0.2,nw_dst=10.0.0.1"
# Output minh họa
bridge("br0")
-------------
0. ip,ct_state=-trk, priority 100
ct(table=1)

===============================================================================
recirc(0x1) - resume conntrack with default ct_state=trk|new (use --ct-next to customize)
1. in_port=2,ct_state=+trk+new, priority 100
drop

Datapath actions: drop

Vì trace mặc định luôn coi bất kỳ gói nào cũng "mới" (trk|new) khi không có gì tùy chỉnh, gói giả lập từ in_port=2 khớp đúng rule drop — xác nhận đúng hành vi Bước 4 mà không cần gửi ping thật.

Nếu chỉ nhớ được một điều sau bài này, hãy nhớ: ct() không phải một action đơn lẻ mà là một cánh cửa rẽ nhánh sang hẳn một bộ máy khác (kernel nf_conntrack hoặc userspace tự viết, tùy datapath) — packet đi vào cửa đó bị tách làm hai, chỉ bản đã qua tracker mới mang theo ct_state có nghĩa, và trk luôn là chốt chặn đầu tiên trước khi đọc bất kỳ flag nào khác. Thiếu commit thì mọi thứ (trạng thái, NAT) chỉ tồn tại đúng vòng đời một packet rồi biến mất — đúng nền tảng để dựng bất kỳ security-group hay stateful ACL nào trên OVS sau này.


Bài tập cuối bài

  1. Trên br0 (lab Mục 6), gửi liên tục ns1 ping -c 5 10.0.0.2 trong khi một cửa sổ khác chạy watch -n1 "ovs-appctl dpctl/dump-conntrack". Quan sát protoinfo/timeout của entry ICMP thay đổi ra sao theo thời gian, rồi ngừng ping và tiếp tục quan sát cho tới khi entry tự biến mất khỏi bảng — đối chiếu với khái niệm timeout của conntrack (khác idle_timeout/hard_timeout của OpenFlow flow đã học ở Bài 12, đây là timeout của chính connection tracker, không phải của flow entry).
  2. Thêm một flow DNAT: dùng ip netns exec ns1 ip link show veth0 để lấy sẵn MAC của ns2 (ip netns exec ns2 ip link show veth1), gán một ARP tĩnh trên ns1 trỏ một "virtual IP" 10.0.0.100 thẳng vào MAC đó (ip netns exec ns1 ip neigh add 10.0.0.100 lladdr <MAC-veth1> dev veth0 nud permanent) để né việc ns1 phải ARP-resolve một địa chỉ không tồn tại thật. Sau đó thêm một flow table=1 ưu tiên cao hơn khớp in_port=1,ip,nw_dst=10.0.0.100,ct_state=+trk+new,actions=ct(commit,nat(dst=10.0.0.2)),goto_table:2 (nhớ giữ nguyên commit — Mục 5 đã nhấn mạnh thiếu commit thì NAT không có hiệu lực). Ping từ ns1 tới 10.0.0.100, đồng thời tcpdump -i veth0-ovs (chưa NAT, vẫn thấy dst=10.0.0.100) song song với ip netns exec ns2 tcpdump -i veth1 (đã NAT, phải thấy dst=10.0.0.2) — so sánh hai điểm bắt gói để tự mắt thấy ct(commit,nat(...)) thật sự dịch địa chỉ đích.
  3. Đổi flow chặn ở table=1,in_port=2,ip,ct_state=+trk+new từ drop sang ct(commit,zone=200),goto_table:2 (thêm một zone riêng khác 0 cho nhánh này), rồi so sánh ovs-appctl dpctl/dump-conntrack trước/sau — quan sát entry mới xuất hiện có gắn zone=200 tách biệt hoàn toàn khỏi các entry zone=0 của nhánh ns1, minh chứng trực tiếp cho tính chất "isolates connections into separate domains" của zone đã trích ở Mục 3, dù trong lab đơn giản này cả hai zone vẫn cùng chạy trên một bridge.

Nguồn tham khảo

  • ct action: syntax ct([argument]...) / ct(commit[,argument]...); "The preferred way to implement a firewall is connection tracking... The ct action described in this section, added in Open vSwitch 2.5, implements connection tracking. For new deployments, it is the recommended way to implement firewalling with Open vSwitch"; cơ chế trước khi có ct: "Before ct was added, Open vSwitch did not have built-in support for connection tracking. Instead, Open vSwitch supported the learn action..."; cơ chế fork: "In this form, ct forks the OpenFlow pipeline: In one fork... the other fork, the original instance of the packet continues independent processing... The ct_state field and other connection tracking metadata are cleared"; đối số zone=value, table=table, force, exec(action...), alg=alg; ví dụ firewall chính thức 7 dòng flow; sysctl net.netfilter.nf_conntrack_helperovs-actions(7), openvswitch.org.
  • NAT sub-syntax nat(type=addrs[:ports][,flag]...), đối số addrs/ports/random/hash/persistent, trường hợp đặc biệt nat(src=0.0.0.0), mốc "Open vSwitch 2.6 introduced nat. Linux 4.6 was the earliest upstream kernel that implemented ct support for nat", tương tác alg + nat NAT-aware helper — ovs-actions(7), openvswitch.org.
  • Action ct_clear: "Clears connection tracking state from the flow, zeroing ct_state, ct_zone, ct_mark, and ct_label.", thêm từ Open vSwitch 2.7 — ovs-actions(7), openvswitch.org.
  • ct_state (32-bit, read-only): định nghĩa 8 flag new/est/rel/rpl/inv/trk/snat/dnat cùng giá trị bit và mô tả nguyên văn từng flag; 6 ràng buộc thứ tự ưu tiên (trk là điều kiện tiên quyết); ct_zone (16-bit, "Each zone is an independent connection tracking context"); ct_mark, ct_labelovs-fields(7), openvswitch.org.
  • OVS Conntrack Tutorial chính thức: topology mẫu 2 namespace + br0, cách bắt đầu tracking bằng ct_state=-trk rồi ct(table=N), ví dụ 5 flow TCP setup/teardown, mẫu output ovs-appctl dpctl/dump-conntrack cho TCP (protoinfo=(state=SYN_SENT/ESTABLISHED/FIN_WAIT_1/LAST_ACK/TIME_WAIT)) — OVS Conntrack Tutorial, docs.openvswitch.org.
  • Kernel Linux triển khai action ct cho datapath kernel dựa trên hạ tầng nf_conntrack/nf_nat có sẵn của netfilter (#include net/netfilter/nf_conntrack_core.h, nf_conntrack_zones.h, nf_nat.h...) — mã nguồn, net/openvswitch/conntrack.c, github.com/torvalds/linux.
  • Datapath userspace có bộ conntrack riêng độc lập với kernel, nằm trong lib/conntrack.c của OVS, thống nhất qua lớp ct-dpif — mã nguồn, lib/conntrack.c, github.com/openvswitch/ovs.
  • Bảng tính năng theo datapath: "Connection tracking [Linux kernel] 4.3 [Userspace] 2.6", "Conntrack NAT [Linux kernel] 4.6 [Userspace] 2.8"; mô tả datapath userspace: "This datapath supports conventional system devices as well as DPDK and AF_XDP devices when support for those is built. This is the only datapath that works on NetBSD, FreeBSD and Mac OSX"Releases FAQ, docs.openvswitch.org.
  • Cột datapath_type trên bảng Bridge: "Name of datapath provider. The kernel datapath has type system. The userspace datapath has type netdev."ovs-vswitchd.conf.db(5), openvswitch.org.
  • ovs-appctl ofproto/trace mặc định giả lập trạng thái conntrack sau mỗi lần packet đi qua ct()trk|new, in dòng "resume conntrack with default ct_state=trk|new (use --ct-next to customize)", có thể ép trạng thái khác qua cờ --ct-next — mã nguồn, ofproto/ofproto-dpif-trace.c, github.com/openvswitch/ovs.
  • Mẫu output ovs-appctl dpctl/dump-conntrack cho kết nối ICMP, dạng icmp,orig=(src=...,dst=...,id=...,type=...,code=...),reply=(...),zone=N — trích test suite chính thức, tests/system-traffic.at, github.com/openvswitch/ovs.
  • Topology lab br0/ns1 (10.0.0.1, veth0-ovs)/ns2 (10.0.0.2, veth1-ovs) dựng từ Bài 4, tái sử dụng cách đọc dump-flows/goto_table đã học ở Bài 12.

Tiếp theo: Module 3, Bài 14 — DPDK.