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 actionct()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 flagtrk/new/est/relnghĩ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:
- 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_conntrackcủa kernel Linux, và khác gì giữa datapath kernel với datapath userspace/DPDK? - Action
ct()có những đối số nào,zonegiả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? ct_statecó đúng bao nhiêu flag, ý nghĩa từng flag là gì, và vì sao thiếu flagtrkthì 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
- 2. Cơ chế bên dưới: kernel nf_conntrack hay tự triển khai riêng?
- 3. Cú pháp ct(): tracking, commit, zone, table, force, exec, alg
- 4. ct_state: 8 flag đầy đủ, và vì sao trk là điều kiện tiên quyết
- 5. NAT qua conntrack: nat(src=...) và nat(dst=...)
- 6. CLI lab: pipeline 3 table — ACL, conntrack, forward
- Bài tập cuối bài
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 keywordestablished) 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.
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ầngnf_conntracksẵn có của Linux, đúng framework màiptables/netfiltercũng dùng. Bằng chứng trực tiếp từ mã nguồn kernel Linux: filenet/openvswitch/conntrack.c— nơi cài đặt actionctcho datapath kernel —#includethẳ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.hkhi kernel bậtCONFIG_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ớinf_conntrackcủ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 tronglib/conntrack.ccủ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ênct-dpif(conntrack datapath interface) — nhờ vậy cú phápct(...)/ct_statebạn viết trong flow giống hệt nhau dù bridge chạy datapath nào,ovs-vswitchdtự 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.
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; có 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.
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 có 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ợ ftp và tftp — 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 tracker — ovs-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:
| Flag | Giá trị bit | Ý nghĩa (trích ovs-fields(7)) |
|---|---|---|
new | 0x01 | "A new connection. Set to 1 if this is an uncommitted connection." |
est | 0x02 | "Part of an existing connection. Set to 1 if packets of a committed connection have been seen by conntrack from both directions." |
rel | 0x04 | "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." |
rpl | 0x08 | "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." |
inv | 0x10 | "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). |
trk | 0x20 | "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." |
snat | 0x40 | "This packet was transformed by source address/port translation by a preceding ct action. Open vSwitch 2.6 added this flag." |
dnat | 0x80 | "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:
- "If trk is unset, no other flags are set." — packet chưa từng đi qua
ct()thìct_statetoàn số 0, đúng nguyên văn lời hứa ở Bài 12. - "If trk is set, one or more other flags may be set."
- "If inv is set, only the trk flag is also set."
- "new and est are mutually exclusive."
- "new and rpl are mutually exclusive."
- "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.
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 addrs và ports, 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.
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.
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:2 → NORMAL 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+new có n_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=2 → NORMAL.
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
- Trên
br0(lab Mục 6), gửi liên tụcns1 ping -c 5 10.0.0.2trong khi một cửa sổ khác chạywatch -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ácidle_timeout/hard_timeoutcủ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). - Thêm một flow DNAT: dùng
ip netns exec ns1 ip link show veth0để lấy sẵn MAC củans2(ip netns exec ns2 ip link show veth1), gán một ARP tĩnh trênns1trỏ một "virtual IP"10.0.0.100thẳng vào MAC đó (ip netns exec ns1 ip neigh add 10.0.0.100 lladdr <MAC-veth1> dev veth0 nud permanent) để né việcns1phải ARP-resolve một địa chỉ không tồn tại thật. Sau đó thêm một flowtable=1ưu tiên cao hơn khớpin_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êncommit— Mục 5 đã nhấn mạnh thiếucommitthì NAT không có hiệu lực). Ping từns1tới10.0.0.100, đồng thờitcpdump -i veth0-ovs(chưa NAT, vẫn thấydst=10.0.0.100) song song vớiip netns exec ns2 tcpdump -i veth1(đã NAT, phải thấydst=10.0.0.2) — so sánh hai điểm bắt gói để tự mắt thấyct(commit,nat(...))thật sự dịch địa chỉ đích. - Đổi flow chặn ở
table=1,in_port=2,ip,ct_state=+trk+newtừdropsangct(commit,zone=200),goto_table:2(thêm mộtzoneriêng khác 0 cho nhánh này), rồi so sánhovs-appctl dpctl/dump-conntracktrước/sau — quan sát entry mới xuất hiện có gắnzone=200tách biệt hoàn toàn khỏi các entryzone=0của nhánhns1, minh chứng trực tiếp cho tính chất "isolates connections into separate domains" củazoneđã 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
ctaction: syntaxct([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; sysctlnet.netfilter.nf_conntrack_helper— ovs-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ệtnat(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ácalg+natNAT-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 flagnew/est/rel/rpl/inv/trk/snat/dnatcù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_label— ovs-fields(7), openvswitch.org.- OVS Conntrack Tutorial chính thức: topology mẫu 2 namespace +
br0, cách bắt đầu tracking bằngct_state=-trkrồict(table=N), ví dụ 5 flow TCP setup/teardown, mẫu outputovs-appctl dpctl/dump-conntrackcho 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
ctcho datapath kernel dựa trên hạ tầngnf_conntrack/nf_natcó 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.ccủa OVS, thống nhất qua lớpct-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_typetrê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/tracemặc định giả lập trạng thái conntrack sau mỗi lần packet đi quact()là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-conntrackcho kết nối ICMP, dạngicmp,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 đọcdump-flows/goto_tableđã học ở Bài 12.
Tiếp theo: Module 3, Bài 14 — DPDK.