Skip to main content

OVS Phần 12: OpenFlow Pipeline Sâu — Multi-Table, Priority Và Cách Đọc Dump-Flows Thực Chiến

Phần 12/21 trong loạt Open vSwitch (OVS) — nối tiếp Phần 11, nơi bạn đã dựng hai cơ chế QoS (ingress_policingQoS/Queue với linux-htb) để kiểm soát băng thông — bài cuối cùng của Module 2 — Trung cấp. Bài này mở đầu Module 3 — Nâng cao, và quay thẳng lại công cụ đã học sơ ở Bài 6: ovs-ofctl. Bài 6 mới dừng ở đúng một flow table (table=0), add-flow/dump-flows cơ bản, và action NORMAL. Bài này đào sâu điều Bài 6 chỉ nhắc lướt qua một câu — pipeline OpenFlow thật của OVS có tới hàng trăm table nối tiếp nhau, và biết cách packet nhảy qua chúng, đọc đúng output dump-flows thực chiến, cùng công cụ trace packet giả lập, là kỹ năng nền tảng cho mọi thứ Module 3 xây tiếp (conntrack, DPDK, hardware offload). Đọc xong, Bài 13 sẽ sang connection tracking (conntrack).

Mục tiêu bài học: sau bài này bạn phân biệt được goto_table (instruction chuẩn OpenFlow 1.1+, chỉ đi tới table lớn hơn) với resubmit (extension riêng của OVS, đi được tới bất kỳ table nào kể cả quay lại), tự tay dựng được một pipeline nhiều table thật (ACL ở một table, forward L2 ở table khác), đọc chính xác từng cột trong output ovs-ofctl dump-flows — đặc biệt không còn nhầm idle_age với duration — và biết dùng ovs-appctl ofproto/trace để giả lập một packet cụ thể, xem nó đi qua pipeline như thế nào mà không cần gửi traffic thật.

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

  1. goto_tableresubmit khác nhau ở đâu — cái nào là chuẩn OpenFlow, cái nào là extension riêng của OVS, và ràng buộc "chỉ đi tới table lớn hơn" áp dụng cho cái nào?
  2. Đọc một output dump-flows thật từ một bridge nhiều table, từng cột (cookie, duration, table, n_packets, n_bytes, idle_age, priority, actions) nói lên điều gì, và vì sao priority không tự động ưu tiên theo độ cụ thể của match?
  3. ovs-appctl ofproto/trace cho phép giả lập một packet cụ thể đi qua toàn bộ pipeline mà không cần traffic thật — cú pháp chính xác là gì, và output của nó đọc ra sao?

Mục lục


1. Nhắc lại Bài 6: giới hạn của một flow table đơn

Bài 6 đã khắc sâu ranh giới OVSDB (config state) và flow table OpenFlow (flow state), rồi thực hành đúng một table duy nhất: br0 mới tạo đã có sẵn một flow priority=0 actions=NORMAL (nhờ fail_mode=standalone mặc định), bạn tự tay thêm một flow priority=100,in_port=1,icmp,actions=drop, và thấy flow priority cao hơn luôn thắng bất kể thêm trước hay sau. Toàn bộ thao tác đó — add-flow, dump-flows, del-flows — đều ngầm định chạy trên đúng một bảng, table=0, dù cột table đã xuất hiện sẵn trong mọi dòng output dump-flows từ Bài 6.

Bài 6 cố tình bỏ qua câu hỏi: nếu table là một cột có sẵn trong mỗi flow, vậy table=1, table=2... ở đâu, và dùng để làm gì? Đó chính xác là chủ đề bài này.


2. Multi-table pipeline: vì sao một bridge thật không dừng ở một table duy nhất

💡 Hình dung như thế này: một table OpenFlow đơn giống một tờ access-list dài trên switch vật lý — mọi điều kiện (chặn theo IP, forward theo MAC, xử lý VLAN...) đều dồn chung vào một danh sách, xếp theo priority. Việc này vẫn chạy được với vài chục dòng, nhưng khi danh sách phình lên hàng trăm/nghìn dòng phục vụ nhiều mục đích khác nhau (ACL, L2 forward, L3 route, QoS), một danh sách phẳng trở nên khó bảo trì và tính priority thủ công dễ sai. Multi-table pipeline giống việc tách tờ access-list đó thành nhiều "trạm kiểm soát" nối tiếp — trạm 1 chỉ lo ACL, trạm 2 chỉ lo L2, trạm 3 chỉ lo L3 — mỗi trạm có priority riêng, dễ quản lý hơn hẳn so với nhồi tất cả vào một trạm.

Tài liệu chính thức xác nhận số lượng table khả dụng khá lớn: output ovs-ofctl show br0 ở Bài 6 đã in sẵn dòng n_tables:254 mà bài đó chưa giải thích — đó chính là số flow table tối đa OVS cung cấp cho một bridge (table=0 đến table=253), còn table=255 được ovs-ofctl(8) dành riêng làm giá trị đặc biệt: "If not specified (or if 255 is specified as table), then flows in all tables are dumped" — tức 255 không phải một table thật, mà là ký hiệu "tất cả các table" dùng cho lệnh dump/xóa.

Con số 254 không phải ngẫu nhiên — soi thẳng vào mã nguồn (ofproto-dpif.h) sẽ thấy OVS thật ra dựng nội bộ 255 table (N_TABLES = 255), nhưng table cuối cùng (TBL_INTERNAL, số hiệu 254) bị đánh dấu ẩn và chỉ-đọc (OFTABLE_HIDDEN | OFTABLE_READONLY), dành riêng cho ovs-vswitchd dùng nội bộ, không lộ ra cho controller/ovs-ofctl. Vì vậy con số n_tables:254 bạn thấy trong OFPT_FEATURES_REPLY chính là phần "254 table còn lại" mà bạn thật sự được quyền dùng (table=0 đến table=253) — table=254 tồn tại thật trong OVS, chỉ là bạn không nhìn thấy hay tự tay thao tác được.

Packet đi vào một bridge nhiều table luôn bắt đầu ở table=0, rồi chỉ tiếp tục sang table khác nếu chính flow đang match packet đó ra lệnh chuyển tiếp — không có gì tự động "rơi" sang table kế tiếp. Nếu flow ở table=0 chỉ có actions=drop hoặc actions=output:N mà không hề nhắc tới table nào khác, packet dừng lại ngay đó, table=1 trở đi coi như không tồn tại với packet này. Đây là lý do một pipeline nhiều table cần một hành động chuyên biệt để "gọi tiếp" sang table sau — chính là goto_tableresubmit, chủ đề Mục 3.

Multi-Table Pipeline — mỗi table lo đúng một việcPacket chỉ sang table sau nếu flow hiện tại ra lệnh goto_table — không có gì "tự rơi"table=0ACLmatch: nw_src,in_port, tp_dstgoto_table:1table=1L2 forwardmatch: dl_dst,dl_vlangoto_table:2table=2L3 routematch: nw_dst,nw_protooutput:NPacket luôn bắt đầu ở table=0 — không có cơ chế "tự động rơi" sang table kế tiếpNếu flow match ở table=0 chỉ có actions=drop/output, packet dừng ngay tại đó,các table còn lại (1, 2, ...) không được packet đó "nhìn thấy"table=255 không phải một table thật — chỉ là ký hiệu "tất cả table"dùng cho dump-flows/del-flows khi không chỉ định table cụ thể(sơ đồ minh họa 3 table điển hình — số table thật tùy pipeline bạn tự thiết kế, tối đa 254)

3. goto_table (OpenFlow chuẩn) và resubmit (OVS extension): khác nhau ở đâu

💡 Hình dung như thế này: goto_table giống việc chuyển hồ sơ giữa các phòng ban theo đúng quy trình một chiều — hồ sơ từ phòng tiếp nhận (table thấp) chuyển sang phòng thẩm định (table cao hơn), không bao giờ được gửi ngược lại phòng trước. resubmit giống việc bạn tự tay mang hồ sơ đi hỏi ý kiến bất kỳ phòng nào — kể cả quay lại đúng phòng vừa mới hỏi — miễn là bạn (chứ không phải quy trình chính thức) chủ động quyết định đi đâu tiếp theo.

Đây là hai cơ chế khác nguồn gốc hoàn toàn, dễ gây nhầm lẫn nhất khi mới học multi-table:

  • goto_table:N — một instruction chuẩn của OpenFlow 1.1 trở lên, không phải action riêng của OVS. ovs-actions(7) mô tả resubmit rồi so sánh ngược lại: "the goto_table instruction in OpenFlow 1.1 and later can be viewed as a kind of restricted resubmit" — chữ "restricted" ở đây chính là chìa khóa: goto_table bị giới hạn đúng một chiều duy nhất, chỉ được trỏ tới một table có số hiệu lớn hơn table hiện tại. Đây là ràng buộc thuộc về đặc tả OpenFlow chuẩn (từ bản 1.1 trở đi), áp dụng cho mọi switch tuân thủ chuẩn, không riêng OVS — lý do kỹ thuật: pipeline OpenFlow được thiết kế để luôn tiến về phía trước, tránh khả năng một packet bị kẹt lặp vô hạn giữa các table.
  • resubmit — một extension riêng của Open vSwitch, tồn tại từ trước khi OpenFlow chuẩn có khái niệm multi-table. ovs-actions(7) nói thẳng: "The resubmit action is an Open vSwitch extension", định nghĩa: "Searches an OpenFlow flow table for a matching flow and executes the actions found, if any, before continuing to the following action in the current flow entry." Cú pháp có hai dạng: resubmit:port (resubmit sang chính table hiện tại, đổi in_port giả lập) và đầy đủ hơn — resubmit([port],[table][,ct]), đúng cú pháp resubmit(port,table) cho phép chỉ định rõ cả port lẫn table đích. Vì không bị ràng buộc bởi đặc tả OpenFlow chuẩn, resubmit đi được tới bất kỳ table nào — kể cả quay lại một table nhỏ hơn hoặc chính table đang đứng — và tài liệu xác nhận rõ: "Recursive resubmit actions are permitted."

Ràng buộc một chiều của goto_table không phải chỉ là mô tả suông — thử trực tiếp sẽ thấy switch từ chối ngay lập tức. Bộ test chính thức của dự án OVS (tests/ofproto.at) có đúng hai case minh họa cạnh nhau: đi tới table lớn hơn thành công, đi tới table bằng hoặc nhỏ hơn thất bại với mã lỗi OFPBIC_BAD_TABLE_ID:

# Case 1 — hợp lệ: từ table=1 tới table=2 (lớn hơn)
AT_CHECK([ovs-ofctl add-flow -O OpenFlow11 br0 table=1,action=goto_table:2])

# Case 2 — bị từ chối: từ table=1 "goto" về chính table=1
AT_CHECK([ovs-ofctl add-flow -O OpenFlow11 br0 table=1,action=goto_table:1],
[1], [],
[ovs-ofctl: actions are invalid with specified match (OFPBIC_BAD_TABLE_ID)
])

(Trích từ bộ test chính thức tests/ofproto.at trong mã nguồn OVS — không phải output minh họa tự tạo, mà đúng là assertion thật trong test suite của dự án.) Mục 8 sẽ tự tay tái hiện đúng lỗi này trên lab của bạn.

Một câu hỏi hay bị bỏ qua: vì sao trong mọi ví dụ, goto_table luôn đứng cuối cùng trong danh sách action, không bao giờ xen giữa? Vì bản thân nó không phải một "action" ngang hàng với drop/output/set_field — nó là một instruction, một khái niệm cấp cao hơn mà OpenFlow 1.1 mới đưa thêm vào. Kể từ 1.1, action trong một flow luôn nằm gọn bên trong một instruction, và các instruction có đúng một thứ tự thực thi cố định theo đặc tả: Meter → Apply-Actions → Clear-Actions → Write-Actions → Write-Metadata → Stat-Trigger (OVS không hỗ trợ) → Goto-Table. ovs-actions(7) nói rõ: "The most important instruction is Apply-Actions. This instruction encapsulates any number of actions, which the instruction executes. Open vSwitch does not explicitly represent Apply-Actions. Instead, any action by itself is implicitly part of an Apply-Actions instructions." — nghĩa là mọi action trần trụi bạn gõ (drop, output:1, set_queue:0...) đều tự động nằm trong Apply-Actions, không cần gọi tên; chỉ có goto_table (đứng thứ 7, cuối bảng) mới cần gọi đích danh, và cú pháp ovs-ofctl bắt buộc giữ đúng thứ tự này — đặt goto_table lên trước một action khác sẽ bị báo lỗi.

Một chi tiết vận hành quan trọng khác: goto_table là instruction chuẩn OpenFlow 1.1+, trong khi OVS mặc định chỉ bật OpenFlow 1.0 khi bạn không truyền cờ -O — đúng điều Bài 6 đã trích từ Using OpenFlow FAQ: "All current versions of ovs-ofctl enable only OpenFlow 1.0 by default. Use the -O option to enable support for later versions of OpenFlow in ovs-ofctl." Muốn dùng goto_table, bắt buộc phải thêm -O OpenFlow11 trở lên vào mọi lệnh ovs-ofctl. Ngược lại, cột table= trong match (không phải trong action) lại có một lối tắt riêng ngay cả khi bạn chưa bật -O gì cả — tài liệu gọi đây là "OpenFlow10+table_id": "This is the standard OpenFlow 1.0 flow format plus a Nicira extension that allows ovs-ofctl to specify the flow table in which a particular flow should be placed", và "ovs-ofctl automatically enables the extension when table is specified and OpenFlow 1.0 is used". Nói cách khác: chỉ định table=1 khi thêm flow thì tự động chạy được dưới OF1.0 nhờ extension ngầm này, nhưng action goto_table:N thì không — nó đòi hỏi tường minh -O OpenFlow11 trở lên, vì đây là instruction thuộc đặc tả chuẩn chứ không phải một Nicira extension.

goto_table vs resubmitCùng mục đích "gọi tiếp table khác", khác hẳn về nguồn gốc và ràng buộctable=0table=1table=2goto_table— chỉ tiến, không lùi— OpenFlow 1.1+ chuẩntable=0table=2resubmit(,0) — quay lại đượcresubmit— tiến/lùi/quay lại đều được— OVS extension riêngThử goto_table:1 từ chính table=1 (đi lùi/đứng yên) → switch từ chối ngay:"actions are invalid with specified match (OFPBIC_BAD_TABLE_ID)"(trích test suite thật của OVS — tests/ofproto.at)

Nói ngắn gọn để không quên: goto_table an toàn, đơn giản, đúng chuẩn — dùng làm xương sống chính cho một pipeline tuyến tính (ACL → L2 → L3). resubmit linh hoạt hơn hẳn — cần khi bạn muốn "gọi lại" một table đã đi qua (ví dụ re-check ACL sau khi một action đã sửa field của packet), hoặc khi cần tương thích một OVS đời cũ/OpenFlow1.0 chưa có goto_table. Cả hai cùng bị giới hạn chống lặp vô hạn — ovs-actions(7): "Open vSwitch limits the total number of resubmit actions that a given packet can execute to 4,096. For this purpose, goto_table instructions and output to the table port are treated like resubmit." Riêng nhánh luôn tiến về table lớn hơn (dù bằng goto_table hay resubmit) được miễn trừ khỏi giới hạn độ sâu đệ quy: "A resubmit from one table to a later one (or, equivalently, a goto_table instruction) does not count against the depth limit because resubmits to strictly monotonically increasing tables will eventually terminate."


4. Match field phổ biến khi pipeline có nhiều table

Bài 6 đã giới thiệu in_port, dl_type/eth_type, nw_src/nw_dst, nw_proto. Với pipeline nhiều table — nơi mỗi table thường lọc theo một nhóm field riêng (ACL theo L3/L4, forward theo L2) — đáng nắm thêm các field ovs-fields(7) định nghĩa:

FieldBit-widthPrerequisiteÝ nghĩa
in_port16-bitkhông"The OpenFlow port on which the packet being processed arrived"
dl_src / dl_dst48-bitkhôngĐịa chỉ MAC nguồn/đích ("Ethernet source addresses" / "Ethernet destination addresses")
dl_vlan16-bit (chỉ 12 bit thấp có nghĩa)có VLAN tagID VLAN — chỉ match được khi vlan_tci=0x1000/0x1000 ("i.e. a VLAN header is present")
vlan_tci16-bitkhôngToàn bộ trường VLAN Tag Control Information (gộp priority + VLAN ID), "applicable... but have a designated value or bit that indicates whether a VLAN header is present"
nw_src / nw_dst32-bitIPv4Địa chỉ IPv4 nguồn/đích, hỗ trợ mask tùy ý (CIDR)
nw_proto8-bitIPv4/IPv6"the IP protocol type carried in the IPv4 header's protocol field or the IPv6 header's next header field", chỉ exact-match
tp_src / tp_dst16-bitTCP/UDP/SCTPPort nguồn/đích tầng transport
ct_state32-bitkhông (read-only)"holds several flags that can be used to determine the state of the connection to which the packet belongs" — dùng dạng +trk+new/+trk+est... (dấu +/- biểu thị flag phải bật/tắt)

ct_state chỉ nhắc sơ ở đây — 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.

Một mẫu số chung đáng nhớ: field nào cũng có prerequisite — ví dụ dl_vlan chỉ hợp lệ khi gói đã có VLAN header, tp_src/tp_dst chỉ hợp lệ khi giao thức tầng 4 đã xác định là TCP/UDP/SCTP. Match một field mà chưa thỏa prerequisite của nó (ví dụ tp_dst=80 mà không kèm tcp/udp) không gây lỗi cú pháp, nhưng có thể không hoạt động như bạn kỳ vọng — luôn khai kèm điều kiện tầng thấp hơn khi dùng field tầng cao hơn, đúng cách Bài 6 đã dùng shorthand icmp để bao hàm sẵn eth_type=0x0800,ip_proto=1.


5. Đọc dump-flows thực chiến: từng cột trong output nghĩa là gì

Bài 6 đã đọc cookie, duration, n_packets, n_bytes, idle_age, priority trên một flow đơn. Với pipeline nhiều table, hai cột thêm cần chú ý là table (đã nói ở Mục 2) và idle_timeout/hard_timeout — hai cột chỉ xuất hiện khi bạn chủ động khai báo, khác hẳn idle_age vốn luôn tự có mặt.

Điểm dễ nhầm nhất, đúng như tên bài đã cảnh báo: durationidle_age đo hai thứ hoàn toàn khác nhau.

  • duration=secs — man page: "the time, in seconds, that the entry has been in the table". Đây là tuổi thọ của chính flow entry, tính từ lúc nó được cài vào, không liên quan gì tới việc có packet đi qua hay không. Một flow duration=3600.5s nghĩa là nó đã tồn tại trong bảng đúng 1 tiếng, kể cả nếu chưa từng có packet nào match nó.
  • idle_age=secs — man page: "the integer number of seconds that have passed without any packets passing through the flow". Đây là "đã bao lâu rồi không ai đụng tới flow này". Một flow có thể duration=3600s nhưng idle_age=2 — tồn tại đã một tiếng, nhưng vừa có packet match cách đây 2 giây.

Hai cột định nghĩa hành vi hết hạn, khác hẳn hai cột đo lường ở trên:

  • idle_timeout — man page khi thêm flow: "Causes the flow to expire after the given number of seconds of inactivity. A value of 0 (the default) prevents a flow from expiring due to inactivity." — nếu bạn đặt idle_timeout=30, flow tự động biến mất khỏi bảng khi idle_age chạm mốc 30 giây.
  • hard_timeout"Causes the flow to expire after the given number of seconds, regardless of activity. A value of 0 (the default) gives the flow no hard expiration deadline." — đếm từ lúc flow được tạo (giống duration), xóa flow bất kể còn đang có traffic hay không.

Cả hai đều mặc định 0 (không tự hết hạn) nếu bạn không khai báo — đúng lý do các flow tĩnh trong lab từ Bài 4 đến giờ chưa bao giờ tự biến mất.

Đọc lại đúng một dòng dump-flows thật, giờ đủ nghĩa cho cả pipeline nhiều table:

cookie=0x0, duration=612.409s, table=0, n_packets=41, n_bytes=3948, idle_age=1, priority=100 actions=goto_table:1
  • cookie=0x0 — định danh tự gán, không khai báo thì mặc định 0x0 (Bài 6, Mục 2).
  • duration=612.409s — flow này đã nằm trong table=0 đúng 612.4 giây kể từ lúc add-flow.
  • table=0 — thuộc bảng đầu tiên trong pipeline.
  • n_packets=41, n_bytes=3948 — bộ đếm packet/byte đã thật sự khớp entry này.
  • idle_age=1 — cách đây 1 giây có packet match, dù flow đã tồn tại hơn 10 phút (duration=612s) — hai con số này không liên quan tới nhau.
  • priority=100 — mức priority tường minh của flow.
  • actions=goto_table:1 — packet match dòng này sẽ được đẩy tiếp sang table=1, đúng cơ chế Mục 3 vừa giải thích.
Tách Từng Cột Của Một Dòng dump-flows Thậtcookie=0x0, duration=612.409s, table=0, n_packets=41, n_bytes=3948, idle_age=1, priority=100 actions=goto_table:1cookie=0x0định danh tự gán, mặc định 0x0 (Bài 6)duration=612.409stuổi thọ FLOW ENTRY, từ lúc add-flowidle_age=1giây từ lần cuối có PACKET match — khác durationtable=0bảng chứa flow này (Mục 2)n_packets=41,n_bytes=3948bộ đếm packet/byte đã thật sự khớppriority=100mức ưu tiên, chỉ có nghĩa trong table=0 (Mục 6)actions=goto_table:1hành động khi match — đẩy sang table=1duration và idle_age đo hai thứ khác nhau — một flow có thể duration=3600s nhưng idle_age=2priority chỉ có ý nghĩa trong phạm vi MỘT table — không so sánh priority giữa hai table khác nhau

6. Priority: không tự động ưu tiên theo độ cụ thể, chỉ theo con số bạn khai báo

Bài 6 đã trích đúng quy tắc cốt lõi: "priority=value... value is a number between 0 and 65535, inclusive. A higher value will match before a lower one", mặc định 32768 nếu không khai báo, và exact-match có priority ngầm định 65535 — cao hơn cả giá trị tối đa bạn tự đặt tay. Với pipeline nhiều table, điều quan trọng cần nhắc lại dứt khoát: hai flow trong cùng một table, một flow match nhiều điều kiện cụ thể hơn (ít wildcard hơn) không tự động thắng flow match ít điều kiện hơn — trừ đúng trường hợp exact-match tuyệt đối vừa nêu. OVS/OpenFlow không có khái niệm "longest prefix match" ngầm định kiểu bảng định tuyến IP; priority hoàn toàn là con số bạn tự khai, và nếu hai flow cùng priority cùng khớp một packet, "OpenFlow leaves behavior undefined when two or more flows with the same priority can match a single packet" — không có gì đảm bảo flow nào được chọn.

Điều này càng quan trọng hơn trong multi-table, vì priority chỉ có ý nghĩa trong phạm vi một tablepriority=100table=0priority=100table=1 không "cạnh tranh" gì với nhau, vì hai table được xét hoàn toàn tách biệt, tuần tự theo đúng đường đi goto_table/resubmit mà packet đã đi qua. Nhầm lẫn phổ biến của người mới: tưởng rằng đặt priority cao ở một table sẽ "thắng" một flow priority thấp hơn ở table khác — điều đó không tồn tại, vì hai flow đó thậm chí có thể không bao giờ cùng được xét cho một packet cụ thể, tùy con đường packet đã đi.


7. Công cụ ofproto trace: giả lập một packet, xem nó đi qua pipeline thế nào

💡 Hình dung như thế này: ovs-appctl ofproto/trace giống việc bạn cầm một "gói tin giấy" mô tả sẵn mọi trường (nguồn, đích, port, giao thức) rồi tự tay đi bộ qua từng trạm kiểm soát trong Mục 2, ghi lại ở mỗi trạm nó bị dừng hay được cho đi tiếp — không cần thật sự gửi một packet mạng nào, không cần VM/namespace nào phải tồn tại. Đây là công cụ debug quan trọng nhất khi pipeline có hơn một table, vì dump-flows chỉ cho bạn thấy flow nào đang tồn tại, không cho thấy một packet cụ thể thật sự đi qua đường nào.

Cú pháp cơ bản: ovs-appctl ofproto/trace <bridge> <các trường packet>, trong đó các trường packet dùng đúng cú pháp match field đã học (Mục 4, và Bài 6), field nào không khai báo mặc định là toàn số 0. Tài liệu tracing chính thức của dự án minh họa một ví dụ đầy đủ:

ovs-appctl ofproto/trace br0 in_port=3,tcp,nw_src=192.0.2.2,tcp_dst=22
Flow: tcp,in_port=3,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,nw_src=192.0.2.2,nw_dst=0.0.0.0,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=0,tp_dst=22,tcp_flags=0

bridge("br0")
-------------
0. ip,in_port=3,nw_src=192.0.2.0/24, priority 32768
resubmit(,2)
2. tcp,tp_dst=22, priority 32768
output:1

Final flow: unchanged
Megaflow: recirc_id=0,tcp,in_port=3,nw_src=192.0.2.0/24,nw_frag=no,tp_dst=22
Datapath actions: 1

(Trích nguyên văn từ tài liệu tracing chính thức của dự án OVS.) Đọc từng khối: dòng Flow: in ra toàn bộ trường packet sau khi đã điền các giá trị mặc định; khối bridge("br0") liệt kê đúng theo thứ tự packet thật sự đi qua — mỗi dòng số là một table, kèm flow đã match (rút gọn, chỉ hiện field liên quan) và priority của nó, dòng ngay dưới là action tương ứng; ở đây packet match table=0 rồi resubmit(,2) nhảy thẳng sang table=2 (bỏ qua table=1 vì không flow nào ở đó match) rồi output:1. Ba dòng cuối là kết quả tính toán ở tầng datapath — Megaflow là điều kiện đã được cache lại để các packet tương tự sau này khớp thẳng trong kernel (đúng khái niệm datapath cache Bài 3 đã giới thiệu), Datapath actions là hành động cuối cùng thực thi. Field đầu tiên trong mọi dòng Megaflowrecirc_id — đánh dấu packet đang ở "lượt" xử lý nào trong datapath: bản thân datapath (kernel hay userspace) vật lý chỉ có một bảng flow duy nhất, nên khi cần mô phỏng hành vi phức tạp hơn một lượt tra bảng (ví dụ sau khi pop một MPLS label, hay khi cần chọn lại member của một bond), packet được "recirculate" — đẩy trở lại đầu datapath với một recirc_id mới, coi như đang bắt đầu một lượt tra cứu khác. recirc_id=0 trong mọi ví dụ ở bài này (và gần như mọi lab đơn giản) nghĩa là packet chỉ cần đúng một lượt duy nhất — pipeline nhiều table ở tầng OpenFlow không tự động kéo theo nhiều lượt recirculation ở tầng datapath, vì toàn bộ goto_table/resubmit đã được ovs-vswitchd "biên dịch" gọn thành đúng một megaflow trước khi cài vào kernel.

Với action NORMAL (mô phỏng học-MAC như Bài 6), output của ofproto/trace in thêm dòng mô tả quyết định của bộ mô phỏng MAC-learning: forwarding to learned port khi đã học được MAC đích, hoặc no learned MAC for destination, flooding khi chưa học được (Mục 8 sẽ thấy trực tiếp dòng này).


8. CLI lab: dựng pipeline 2 table thật, trace và verify bằng ping

Lab này dùng lại đúng br0/ns1 (10.0.0.1, veth0-ovs = port 1)/ns2 (10.0.0.2, veth1-ovs = port 2) đã dựng từ Bài 4-6 — nếu môi trường đó không còn, dựng lại theo đúng các bước ở Bài 4 trước khi tiếp tục. Toàn bộ 5 bước sắp làm khớp đúng sơ đồ dưới đây:

Lab Mục 8 — Pipeline 2 Table Thậttable=0 lọc theo IP nguồn (ACL) — table=1 làm L2 forward (NORMAL)ns1 10.0.0.1port 1table=0 — ACLpriority=200nw_src=10.0.0.99 → droppriority=100catch-all → goto_table:1✕ DROPdừng hẳn ở table=0goto_table:1table=1 — priority=0actions=NORMALns2port 2Bước 3: ping ns1→ns2, dump-flows xác nhận n_packets tăng đúng ở catch-all + NORMALBước 4: table=1 goto_table:0 (đi lùi) → OFPBIC_BAD_TABLE_IDBước 5: ofproto/trace giả lập cả 2 trường hợp — hợp lệ và bị chặn — không cần traffic thật

Bước 1 — xóa sạch flow hiện có, kể cả flow NORMAL mặc định, để bắt đầu từ một pipeline trống:

sudo ovs-ofctl -O OpenFlow13 del-flows br0

(Dùng -O OpenFlow13 cho mọi lệnh ovs-ofctl từ đây — bản OpenFlow phổ biến nhất trong thực tế, luôn thỏa yêu cầu tối thiểu OpenFlow 1.1 mà goto_table cần, theo đúng Mục 3.)

Bước 2 — dựng pipeline 2 table: table=0 làm ACL, table=1 làm L2 forward:

# table=0 — ACL: chặn đúng 1 source IP "blacklist" giả lập (10.0.0.99 không tồn tại thật trong lab,
# chỉ dùng để minh họa cơ chế), mọi traffic còn lại (catch-all, priority thấp hơn) goto_table sang table=1
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=0,priority=200,ip,nw_src=10.0.0.99,actions=drop"
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=0,priority=100,actions=goto_table:1"

# table=1 — L2 forward: tái tạo đúng hành vi NORMAL mặc định của Bài 6, nhưng đặt riêng ở một table
sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=0,actions=NORMAL"

Xác nhận bằng dump-flows:

sudo ovs-ofctl -O OpenFlow13 dump-flows br0
# Output minh họa
cookie=0x0, duration=8.221s, table=0, n_packets=0, n_bytes=0, idle_age=8, priority=200,ip,nw_src=10.0.0.99 actions=drop
cookie=0x0, duration=8.198s, table=0, n_packets=0, n_bytes=0, idle_age=8, priority=100 actions=goto_table:1
cookie=0x0, duration=8.176s, table=1, n_packets=0, n_bytes=0, idle_age=8, priority=0 actions=NORMAL

(Output minh họa — cấu trúc đúng theo lệnh thật, số liệu tùy máy. Ba flow này nằm ở hai table khác nhau — đúng cách đọc cột table đã học ở Mục 2/5.)

Bước 3 — verify data-plane thật bằng ping, rồi xác nhận counter tăng đúng đường đi kỳ vọng (table=0 catch-all → table=1 NORMAL):

sudo ip netns exec ns1 ping -c 3 -W 1 10.0.0.2
sudo ovs-ofctl -O OpenFlow13 dump-flows br0
# Output minh họa — sau khi ping xong
cookie=0x0, duration=41.221s, table=0, n_packets=0, n_bytes=0, idle_age=41, priority=200,ip,nw_src=10.0.0.99 actions=drop
cookie=0x0, duration=41.198s, table=0, n_packets=6, n_bytes=588, idle_age=1, priority=100 actions=goto_table:1
cookie=0x0, duration=41.176s, table=1, n_packets=6, n_bytes=588, idle_age=1, priority=0 actions=NORMAL

(Output minh họa — số liệu tùy máy.) Đúng như kỳ vọng: flow chặn 10.0.0.99 vẫn n_packets=0 (không ai gửi từ IP đó), còn flow catch-all ở table=0 và flow NORMALtable=1 cùng tăng n_packets=6 (3 request + 3 reply) — bằng chứng trực tiếp packet ping thật đã đi đúng đường table=0goto_table:1table=1.

Bước 4 — tự tay tái hiện lỗi OFPBIC_BAD_TABLE_ID đã nói ở Mục 3, để thấy ràng buộc một chiều của goto_table không phải lý thuyết suông:

sudo ovs-ofctl -O OpenFlow13 add-flow br0 "table=1,priority=200,actions=goto_table:0"
# Output minh họa — dựa theo hành vi thật đã ghi trong test suite chính thức OVS (Mục 3)
ovs-ofctl: actions are invalid with specified match (OFPBIC_BAD_TABLE_ID)

Switch từ chối thẳng — table=1 không được phép goto_table ngược về table=0. Muốn làm việc tương tự (quay lại một table nhỏ hơn), phải dùng resubmit(,0) thay vì goto_table:0.

Bước 5 — dùng ofproto/trace giả lập hai packet, không cần gửi traffic thật:

Packet hợp lệ (giống ns1 ping ns2):

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
Flow: icmp,in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,nw_src=10.0.0.1,nw_dst=10.0.0.2,nw_tos=0,nw_ecn=0,nw_ttl=0,icmp_type=0,icmp_code=0

bridge("br0")
-------------
0. priority 100
goto_table:1
1. priority 0
NORMAL
no learned MAC for destination, flooding

Final flow: unchanged
Megaflow: recirc_id=0,icmp,in_port=1,nw_src=10.0.0.1,nw_dst=10.0.0.2,nw_frag=no
Datapath actions: 2

(Output minh họa — cấu trúc đúng theo tài liệu tracing chính thức đã trích ở Mục 7, số liệu/port tùy máy.) Packet match đúng flow catch-all ở table=0, bị đẩy sang table=1 qua goto_table:1, rồi rơi vào NORMAL — dòng no learned MAC for destination, flooding xuất hiện nếu đây là gói đầu tiên (ovs-vswitchd chưa học được MAC của ns2); chạy lại trace sau khi đã có ít nhất một packet thật đi qua sẽ thấy dòng đổi thành forwarding to learned port, đúng cơ chế MAC-learning Bài 6 đã giải thích.

Packet bị chặn (giả lập traffic từ "IP blacklist"):

sudo ovs-appctl ofproto/trace br0 "in_port=1,ip,nw_src=10.0.0.99,nw_dst=10.0.0.2"
# Output minh họa
Flow: ip,in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,nw_src=10.0.0.99,nw_dst=10.0.0.2,nw_tos=0,nw_ecn=0,nw_ttl=0,nw_frag=no

bridge("br0")
-------------
0. ip,nw_src=10.0.0.99, priority 200
drop

Final flow: unchanged
Megaflow: recirc_id=0,ip,nw_src=10.0.0.99,nw_frag=no
Datapath actions: drop

(Output minh họa.) Packet dừng ngay tại table=0 — không bao giờ tới table=1, đúng cơ chế "không có gì tự động rơi sang table sau" đã nói ở Mục 2. Đây chính là giá trị thực chiến của ofproto/trace: xác nhận được hành vi pipeline trước khi cần dựng cả một host thật gửi packet từ 10.0.0.99 để test.

Nếu chỉ nhớ được một điều sau bài này, hãy nhớ: pipeline nhiều table không có gì huyền bí — packet chỉ đi tiếp khi chính flow đang match nó ra lệnh rõ ràng (goto_table một chiều, hoặc resubmit tùy ý), dump-flows cho biết flow nào tồn tại nhưng không cho biết packet nào đã đi đường nào, và ofproto/trace chính là công cụ lấp đúng khoảng trống đó — giả lập một packet cụ thể, in ra từng bước nó đi qua, không cần một byte traffic thật nào.


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

  1. Trên br0 (lab Mục 8), thêm một table=2 đóng vai trò "log/đếm" đặt trước table=0 về mặt logic (tức bạn cho packet đi table=0table=2 trước rồi mới goto_table sang table=1, hoặc bất kỳ cách sắp xếp nào cần quay lại một table nhỏ hơn ở giữa chừng). Thử dùng goto_table để làm việc này trước — quan sát lỗi OFPBIC_BAD_TABLE_ID xảy ra ở đúng bước nào, rồi sửa lại bằng resubmit(,N). Verify bằng ovs-appctl ofproto/trace xác nhận packet đã đi đúng thứ tự table mong muốn.
  2. Tạo tình huống priority conflict thật: trong cùng table=1, thêm hai flow cùng match in_port=1,icmp nhưng cùng priority (ví dụ cả hai priority=150, một actions=NORMAL, một actions=drop). Chạy ovs-ofctl -O OpenFlow13 dump-flows br0 xác nhận cả hai cùng tồn tại, sau đó dùng ovs-appctl ofproto/trace br0 "in_port=1,icmp,nw_src=10.0.0.1,nw_dst=10.0.0.2" nhiều lần liên tiếp — quan sát xem flow được chọn có nhất quán giữa các lần chạy hay không, tự đối chiếu với câu trích ở Mục 6: "OpenFlow leaves behavior undefined when two or more flows with the same priority can match a single packet". Sửa lại bằng cách gán hai priority khác nhau, verify hành vi trở nên xác định (deterministic).
  3. Đổi idle_timeout=10 cho flow catch-all ở table=0 (ovs-ofctl -O OpenFlow13 add-flow br0 "table=0,priority=100,idle_timeout=10,actions=goto_table:1" — nhớ del-flows flow cũ cùng match trước khi thêm lại). Ngừng ping trong hơn 10 giây, chạy dump-flows xác nhận flow đó tự biến mất khỏi bảng đúng như idle_timeout mô tả, đối lập với việc flow table=1,actions=NORMAL (không đặt idle_timeout) vẫn còn nguyên dù không có traffic — tự rút ra khác biệt thực hành giữa "có timeout" và "không có timeout" (mặc định 0).

Nguồn tham khảo

  • table=table cho lệnh dump/xóa flow: "For flow dump commands, limits the flows dumped to those in table, which may be expressed as a number between 0 and 255... If not specified (or if 255 is specified as table), then flows in all tables are dumped"; idle_timeout: "Causes the flow to expire after the given number of seconds of inactivity. A value of 0 (the default) prevents a flow from expiring due to inactivity"; hard_timeout: "Causes the flow to expire after the given number of seconds, regardless of activity. A value of 0 (the default) gives the flow no hard expiration deadline"; duration=secs: "the time, in seconds, that the entry has been in the table"; idle_age=secs: "the integer number of seconds that have passed without any packets passing through the flow"; priority: "a number between 0 and 65535, inclusive. A higher value will match before a lower one", mặc định "32768", exact-match "implicit priority value of 65535", và "OpenFlow leaves behavior undefined when two or more flows with the same priority can match a single packet"; flow format "OpenFlow10+table_id... a Nicira extension that allows ovs-ofctl to specify the flow table", tự bật khi "table is specified and OpenFlow 1.0 is used"; OpenFlow 1.0 mặc định chọn table=0: "The Open vSwitch software switch always chooses table 0"ovs-ofctl(8), openvswitch.org.
  • resubmit: "Searches an OpenFlow flow table for a matching flow and executes the actions found, if any, before continuing to the following action in the current flow entry", cú pháp resubmit:portresubmit([port],[table][,ct]), "Recursive resubmit actions are permitted", "The resubmit action is an Open vSwitch extension. However, the goto_table instruction in OpenFlow 1.1 and later can be viewed as a kind of restricted resubmit"; giới hạn đệ quy: "Open vSwitch limits the total number of resubmit actions that a given packet can execute to 4,096. For this purpose, goto_table instructions and output to the table port are treated like resubmit", và "A resubmit from one table to a later one (or, equivalently, a goto_table instruction) does not count against the depth limit because resubmits to strictly monotonically increasing tables will eventually terminate"; thứ tự cố định 7 instruction OpenFlow 1.1+ (Meter, Apply-Actions, Clear-Actions, Write-Actions, Write-Metadata, Stat-Trigger (not supported by Open vSwitch), Goto-Table), và "Open vSwitch does not explicitly represent Apply-Actions. Instead, any action by itself is implicitly part of an Apply-Actions instructions"ovs-actions(7), openvswitch.org.
  • N_TABLES = 255 nhưng TBL_INTERNAL = N_TABLES - 1 (table 254) được đánh dấu OFTABLE_HIDDEN | OFTABLE_READONLY, giải thích vì sao ovs-ofctl show báo n_tables:254 thay vì 255 — mã nguồn, ofproto/ofproto-dpif.h, github.com/openvswitch/ovs.
  • Test case chính thức xác nhận goto_table chỉ đi tới table lớn hơn, đi ngang/lùi bị từ chối với OFPBIC_BAD_TABLE_ID — mã nguồn, tests/ofproto.at, github.com/openvswitch/ovs.
  • Định nghĩa match field in_port, dl_src/dl_dst (48-bit), dl_vlan (16-bit, prerequisite VLAN tag), vlan_tci (16-bit), nw_src/nw_dst (32-bit IPv4), nw_proto (8-bit, prerequisite IPv4/IPv6), tp_src/tp_dst (16-bit, prerequisite TCP/UDP/SCTP), ct_state (32-bit, "holds several flags that can be used to determine the state of the connection to which the packet belongs") — ovs-fields(7), openvswitch.orgovs-fields(7), Debian manpages.
  • Cú pháp và output mẫu ovs-appctl ofproto/trace br0 in_port=3,tcp,nw_src=192.0.2.2,tcp_dst=22, cấu trúc Flow:/bridge(...)/Final flow:/Megaflow:/Datapath actions:Tracing packets inside Open vSwitch, docs.openvswitch.org / github.com/openvswitch/ovs Documentation/topics/tracing.rst.
  • OpenFlow mặc định của ovs-ofctl khi không dùng -O là 1.0: "All current versions of ovs-ofctl enable only OpenFlow 1.0 by default. Use the -O option to enable support for later versions of OpenFlow in ovs-ofctl"Using OpenFlow FAQ, docs.openvswitch.org.
  • n_tables:254 quan sát trên output ovs-ofctl show br0 — cùng nguồn ovs-ofctl(8) ở trên, đối chiếu thực tế đã thấy ở Bài 6.
  • Recirculation (recirc_id): kỹ thuật cho phép một packet quay lại xử lý ở datapath (chỉ có một bảng flow vật lý duy nhất) để mô phỏng hành vi phức tạp hơn một lượt tra bảng, ví dụ sau khi pop MPLS label hoặc khi cần chọn lại member của một bond — Overview of OVS's datapaths, blog.vsq.cz.

Tiếp theo: Module 3, Bài 13 — Connection tracking (conntrack) trên OVS.