OVS Phần 8: VLAN Trunking & Isolation — 5 vlan_mode và QinQ
Phần 8/21 trong loạt Open vSwitch (OVS) — nối tiếp Phần 7, nơi VLAN chỉ xuất hiện đúng một câu
set port vnet0 tag=100áp lại nguyên xi cú pháp Bài 5. Bài 5 Mục 3 đã nhắc tên cộtvlan_modevà nói nó quyết định 5 hành vi khác nhau, nhưng chỉ dừng ở hai trường hợp cơ bản (access,trunk) rồi hẹn đào sâu ở đây. Bài này mở đúng cái hẹn đó: 5 giá trịvlan_modeđầy đủ, một trunk port thật nối hai OVS bridge, vàdot1q-tunnel— chế độ QinQ dễ hiểu nhầm nhất trong toàn bộ schema Port.
Mục tiêu bài học: sau bài này bạn phân biệt chính xác cả 5 giá trị vlan_mode (không chỉ 2 giá trị cơ bản Bài 5 đã dùng), tự dựng được một trunk port thật giữ nguyên VLAN tag giữa hai OVS bridge, và hiểu đúng dot1q-tunnel/QinQ giải quyết bài toán gì — kể cả biết trước cái bẫy cấu hình khiến QinQ "chạy nhưng sai" nếu bỏ qua một tùy chọn ít ai để ý.
Ba câu hỏi mà bài này trả lời:
- 5 giá trị
vlan_modekhác nhau chính xác ở đâu — đặc biệtnative-taggedvànative-untagged, cặp dễ nhầm nhất? - Trunk port thật giữa hai OVS bridge hoạt động thế nào, và VLAN tag có thật sự "sống sót" nguyên vẹn qua uplink không?
dot1q-tunnel(QinQ) trên OVS giải quyết bài toán gì, và cấu hình đúng cần thêm tùy chọn nào ngoàitag=/cvlans=?
Mục lục
- 1. Nhắc lại: tag và trunks đã đủ để mô tả VLAN chưa?
- 2. Năm giá trị vlan_mode: access, trunk và ba biến thể ít ai để ý
- 3. Lab: trunk port thật giữa hai OVS bridge, VLAN tag có sống sót qua uplink không?
- 4. native-tagged và native-untagged: khi nào cần một VLAN native kiểu switch vật lý
- 5. dot1q-tunnel (QinQ): khi một lớp tag không đủ, phải đóng gói VLAN trong VLAN
- Bài tập cuối bài
1. Nhắc lại: tag và trunks đã đủ để mô tả VLAN chưa?
Bài 5 Mục 3 đã dựng hai trường hợp cơ bản nhất: đặt tag=10 mà không đặt trunks → OVS tự hiểu là access port VLAN 10; đặt trunks=10,20 mà không đặt tag → OVS tự hiểu là trunk port cho VLAN 10 và 20. Bài đó cũng đã trích đúng một câu quan trọng từ tài liệu schema (ovs-vswitchd.conf.db): khi cột vlan_mode để trống, "a default mode is selected as follows: if tag contains a value, the port is an access port... Otherwise, the port is a trunk port".
Câu đó ngầm nói một điều: vlan_mode không phải cột phụ, nó là cột quyết định thật sự hành vi của port — tag/trunks chỉ là dữ liệu đầu vào, còn vlan_mode mới là cái quy định cách dữ liệu đó được diễn giải. Hai trường hợp Bài 5 dùng chỉ là 2 trong 5 giá trị hợp lệ. Ba giá trị còn lại — native-tagged, native-untagged, dot1q-tunnel — không tự suy ra được từ tag/trunks một cách mặc định, bắt buộc phải gõ tường minh vlan_mode=..., và đây chính xác là ba giá trị gây nhầm lẫn nhiều nhất khi mới học OVS, kể cả với người đã quen VLAN vật lý.
2. Năm giá trị vlan_mode: access, trunk và ba biến thể ít ai để ý
Tài liệu schema chính thức (ovs-vswitchd.conf.db(5)) mô tả chính xác hành vi ingress (packet đi vào port) và egress (packet đi ra port) cho từng giá trị. Dịch sát nghĩa, giữ nguyên các cụm kỹ thuật quan trọng:
access: "An access port carries packets on exactly one VLAN specified in the tag column. Packets egressing on an access port have no 802.1Q header." — đúng như Bài 5 đã dùng: một VLAN duy nhất, egress luôn không có tag.trunk: "A trunk port carries packets on one or more specified VLANs specified in the trunks column (often, on every VLAN). A packet that ingresses on a trunk port is in the VLAN specified in its 802.1Q header, or VLAN 0 if the packet has no 802.1Q header." — điểm hay bị bỏ sót: packet không tag đi vào một trunk port không bị từ chối, nó được xếp vào VLAN 0 (thường không có thiết bị nào thật sự dùng VLAN 0, nên trong thực tế coi như "bị bỏ qua" trừ khi bạn cố tình trunk cả VLAN 0).native-tagged: "A native-tagged port resembles a trunk port, with the exception that a packet without an 802.1Q header that ingresses on a native-tagged port is in the native VLAN (specified in the tag column)." — vẫn là trunk (nhiều VLAN quatrunks), nhưng thêm một VLAN "mặc định" (tag) dành cho packet không tag đi vào.native-untagged: "A native-untagged port resembles a native-tagged port, with the exception that a packet that egresses on a native-untagged port in the native VLAN will not have an 802.1Q header." — giống hệtnative-tagged, chỉ khác đúng một chỗ: khi packet thuộc VLAN native đi ra khỏi port, nó bị bỏ tag trước khi ra.dot1q-tunnel: theo mô tả man page, đây là chế độ "giống access port" ở chỗ chỉ khai một VLAN duy nhất trong cộttag— nhưng khác hẳnaccessở cách xử lý packet đã có sẵn 802.1Q header đi vào: thay vì coi đó là lỗi hay bỏ tag, port này đối xử với nó như một frame double-tagged, dùngtaglàm outer "service VLAN". Đây chính là QinQ, đào sâu ở Mục 5.
Ba giá trị native-* và dot1q-tunnel không nằm trong quy tắc suy luận mặc định — bỏ trống vlan_mode không bao giờ tự ra một trong ba giá trị này, dù tag và trunks có được đặt cùng lúc thế nào đi nữa. Muốn dùng, bắt buộc gõ tường minh vlan_mode=native-tagged (hoặc native-untagged, dot1q-tunnel).
3. Lab: trunk port thật giữa hai OVS bridge, VLAN tag có sống sót qua uplink không?
Bài 5 Mục 3 mới tạo một trunk port đơn lẻ (uplink0, trunks=10,20) và dừng ở đó — "chưa cần traffic thật đi qua nó". Giờ là lúc cho traffic thật đi qua, đúng kịch bản một trunk port thật sự tồn tại để làm: nối hai switch, giữ nguyên nhiều VLAN qua đúng một sợi cáp (hoặc một veth pair mô phỏng uplink vật lý, đúng phong cách lab từ Bài 4).
Dựng hai OVS bridge br-a/br-b (mô phỏng hai OVS host khác nhau, đặt cùng máy cho gọn — thực tế đây là hai uplink port trên hai hypervisor riêng biệt nối qua switch vật lý), nối chúng bằng một veth pair đóng vai trò uplink, cả hai đầu đặt vlan_mode=trunk:
sudo ovs-vsctl add-br br-a
sudo ovs-vsctl add-br br-b
# veth pair mô phỏng đường uplink vật lý nối 2 OVS host
sudo ip link add trunk-a type veth peer name trunk-b
sudo ip link set trunk-a up
sudo ip link set trunk-b up
# Trunk cả 2 đầu, cho qua đúng VLAN 10 và 20 (không trunk "mọi VLAN" để tránh rò rỉ)
sudo ovs-vsctl add-port br-a trunk-a -- set port trunk-a vlan_mode=trunk trunks=10,20
sudo ovs-vsctl add-port br-b trunk-b -- set port trunk-b vlan_mode=trunk trunks=10,20
Mỗi bên gắn hai namespace access port, một VLAN 10 và một VLAN 20 — bên A đặt IP .11, bên B đặt IP .12 để phân biệt:
# Bên A: 2 namespace VLAN 10 và VLAN 20
for v in 10 20; do
sudo ip netns add a$v
sudo ip link add veth-a$v type veth peer name veth-a$v-ns
sudo ip link set veth-a$v-ns netns a$v
sudo ip netns exec a$v ip link set veth-a$v-ns name eth0
sudo ip netns exec a$v ip addr add 10.0.$v.11/24 dev eth0
sudo ip netns exec a$v ip link set eth0 up
sudo ip link set veth-a$v up
sudo ovs-vsctl add-port br-a veth-a$v tag=$v
done
# Bên B: y hệt, đổi IP .12
for v in 10 20; do
sudo ip netns add b$v
sudo ip link add veth-b$v type veth peer name veth-b$v-ns
sudo ip link set veth-b$v-ns netns b$v
sudo ip netns exec b$v ip link set veth-b$v-ns name eth0
sudo ip netns exec b$v ip addr add 10.0.$v.12/24 dev eth0
sudo ip netns exec b$v ip link set eth0 up
sudo ip link set veth-b$v up
sudo ovs-vsctl add-port br-b veth-b$v tag=$v
done
Kiểm chứng đúng điều Mục tiêu bài học đã hứa — VLAN tag phải giữ nguyên qua trunk, cùng VLAN thì thông dù khác bridge, khác VLAN thì cô lập dù đi qua cùng một sợi trunk:
# Cùng VLAN 10, khác bridge (a10 -> b10) — PHẢI thông
sudo ip netns exec a10 ping -c 3 10.0.10.12
# Khác VLAN (a10 -> b20) — PHẢI timeout, dù đi qua đúng trunk vừa dựng
sudo ip netns exec a10 ping -c 3 -W 1 10.0.20.12
# Output minh họa — a10 tới b10 (cùng VLAN 10)
3 packets transmitted, 3 received, 0% packet loss, time 2044ms
# Output minh họa — a10 tới b20 (khác VLAN)
3 packets transmitted, 0 received, 100% packet loss, time 2041ms
(Output minh họa — cấu trúc đúng theo lệnh thật, số liệu cụ thể tùy máy.) Không có gì huyền bí ở đây, nhưng đáng để bạn nhìn kỹ một lần đúng chuyện gì vừa xảy ra trên dây: gói ICMP từ a10 rời veth-a10 không tag (vì đó là access port), br-a gắn thêm 802.1Q tag VLAN 10 khi đẩy ra trunk-a, gói đi qua veth pair mang nguyên tag đó sang trunk-b, br-b đọc đúng tag VLAN 10 và forward vào đúng veth-b10 — access port bên B lại bóc tag ra trước khi tới eth0 trong namespace b10. VLAN "sống sót" không phải vì nó là một khái niệm trừu tượng, mà vì cả hai đầu trunk port cùng đồng ý đọc/ghi đúng một con số trong đúng một trường của header 802.1Q.
Dùng ovs-vsctl show hoặc ovsdb-client dump Open_vSwitch Port (Bài 5 Mục 4) trên cả hai bridge để bạn tự xác nhận trunk-a/trunk-b có trunks: [10, 20] còn bốn access port kia có đúng tag tương ứng — thói quen "đừng chỉ tin ping chạy đúng, đọc luôn OVSDB để chắc chắn vì sao nó đúng" nên giữ từ đây trở đi.
4. native-tagged và native-untagged: khi nào cần một VLAN native kiểu switch vật lý
💡 Hình dung như thế này: một trunk port thường giống cổng soát vé chung của một buổi hòa nhạc nhiều khu vực — mỗi khách phải đeo vòng tay đúng màu (VLAN tag) của khu mình được vào, khách không đeo vòng tay bị mặc định coi là đi lạc (VLAN 0), gần như chắc chắn bị chặn.
native-tagged/native-untaggedthêm một khu "phổ thông" mặc định cho khách không đeo vòng tay, thay vì chặn họ — chỉ khác nhau ở khâu ra cổng:native-taggedvẫn bắt khách khu phổ thông đeo vòng tay khi đi ra (để soát vé audit dễ, đồng nhất với các khu khác), cònnative-untaggedcho khách khu phổ thông tháo vòng tay ra khi rời cổng — với họ, buổi hòa nhạc trông y hệt như không hề có khái niệm "nhiều khu" nào cả.
Đây chính là khái niệm switch vật lý gọi là native VLAN trên trunk port (Cisco, Juniper đều có khái niệm tương đương) — OVS không phát minh ra ý tưởng mới, chỉ tách nó thành hai biến thể tường minh thay vì gộp chung một hành vi cố định. Ví dụ chính thức từ FAQ VLAN của dự án, dựng một cổng uplink vật lý cần vừa trunk nhiều VLAN vừa có một VLAN mặc định cho traffic không tag đi vào:
sudo ovs-vsctl add-br br0
sudo ovs-vsctl add-port br0 eth0 tag=9 vlan_mode=native-tagged
Tài liệu FAQ ghi thêm ngay sau ví dụ này: "In this situation, 'native-untagged' mode will probably work equally well." — với nhiều switch vật lý phía đối diện (nơi VLAN native theo mặc định luôn đi untagged, đúng hành vi 802.1Q gốc), native-untagged thường là lựa chọn tương thích hơn; native-tagged hữu ích khi bạn muốn nhìn thấy VLAN native tường minh trong mọi gói tin đi qua dump-flows/tcpdump, kể cả gói thuộc VLAN mặc định, để không phải đoán "gói không tag này đang thuộc VLAN nào".
Tự lab nhanh trên đúng br-a vừa dựng ở Mục 3 — đổi veth-a10 (đang là access VLAN 10) thành cổng nhận cả traffic không tag lẫn VLAN 20 trunk qua, coi VLAN 10 là native:
sudo ovs-vsctl set port veth-a10 tag=10 trunks=10,20 vlan_mode=native-untagged
Từ giờ, veth-a10 vừa nhận đúng gói tag VLAN 20 (forward vào namespace nếu namespace đó lắng nghe VLAN 20 — thường không, chỉ để minh họa cơ chế) vừa nhận gói không tag (coi là VLAN 10, và ra khỏi port cũng không tag, đúng định nghĩa native-untagged). Đổi lại access (vlan_mode=access, bỏ trunks) sau khi bạn lab xong, để không phá vỡ bài tập ở Mục 3.
5. dot1q-tunnel (QinQ): khi một lớp tag không đủ, phải đóng gói VLAN trong VLAN
💡 Hình dung như thế này: một VLAN tag 802.1Q thường giống địa chỉ ghi trên phong bì thư — đủ để bưu điện nội địa phân loại. Nhưng khi một nhà mạng (hoặc một OVS bridge đóng vai trò uplink dùng chung cho nhiều khách hàng) cần vận chuyển hàng nghìn phong bì đã có sẵn địa chỉ riêng của nhiều khách hàng khác nhau qua cùng một tuyến, cách làm không phải mở từng phong bì ra đọc — mà bỏ nguyên cả phong bì đó vào một túi vận chuyển lớn hơn, dán đúng một nhãn tuyến (VLAN dịch vụ) lên túi. Người vận chuyển tuyến chỉ cần đọc nhãn túi ngoài, không cần biết (và không được đọc) địa chỉ ghi trên từng phong bì bên trong. Tới nơi, túi được bóc ra, từng phong bì lấy lại nguyên vẹn địa chỉ gốc như chưa từng bị đóng gói.
dot1q-tunnel chính là cơ chế đó, đúng chuẩn 802.1ad, thường gọi bằng cái tên quen thuộc hơn: QinQ ("VLAN trong VLAN"). Man page mô tả nó "somewhat like an access port" — chỉ khai một VLAN duy nhất trong tag — nhưng khác access ở chỗ nó không coi 802.1Q header sẵn có trên packet đi vào là điều bất thường; nó coi packet đó là "double-tagged with the outer service VLAN tag", tức là gắn thêm đúng một lớp tag mới (outer, hay còn gọi S-TAG — service tag) bọc quanh tag gốc của khách hàng (inner, C-TAG — customer tag) mà không đụng vào C-TAG đó.
Cột đi kèm bắt buộc phải biết: cvlans — man page định nghĩa: "For a dot1q-tunnel port, the customer VLANs that this port includes. If this is empty, the port includes all customer VLANs." Nói cách khác, tag quyết định "cái túi vận chuyển" mang nhãn gì (outer S-VLAN), còn cvlans quyết định những "phong bì" (VLAN khách hàng) nào được phép bỏ vào túi đó — để trống nghĩa là chấp nhận mọi VLAN khách hàng.
Hai chi tiết cấu hình dễ bỏ sót, cả hai đều nằm ở bảng Open_vSwitch (cấu hình toàn cục, không phải trên từng Port):
other_config:qinq-ethtype— chọn EtherType/TPID (Tag Protocol Identifier, 2 byte đầu tiên của header 802.1Q/802.1ad, giá trị này báo cho thiết bị nhận biết "đây là một VLAN tag, không phải payload") cho lớp S-TAG ngoài cùng. Man page ghi: "optional string, either 802.1ad or 802.1q" — mặc định là802.1ad(TPID0x88a8, đúng chuẩn QinQ chính thức); đổi sang802.1q(TPID0x8100, cùng giá trị với tag khách hàng thông thường) chỉ cần thiết khi thiết bị phía đối diện không hỗ trợ đọc0x88a8.other_config:vlan-limit— đây là chỗ dễ khiến QinQ "cấu hình đúng theo hướng dẫn nhưng vẫn không hoạt động đúng" nếu bỏ qua. Man page ghi rõ: "Limits the number of VLAN headers that can be matched to the specified number. Further VLAN headers will be treated as payload", và mặc định: "If this value is absent, the default is currently 1. This maintains backward compatibility with controllers that were designed for use with Open vSwitch versions earlier than 2.8, which only supported one VLAN." Nói thẳng ra: mặc định OVS chỉ parse một lớp VLAN header; với một portdot1q-tunnel, muốn OVS thật sự nhìn xuyên qua được cả C-TAG bên trong (để match/log/troubleshoot đúng), phải nâng giới hạn này lên bằng tay:
sudo ovs-vsctl set Open_vSwitch . other_config:vlan-limit=0
Tạo một port dot1q-tunnel hoàn chỉnh, gộp cả tag/cvlans/vlan_mode vào đúng một transaction (đúng thói quen Bài 5 Mục 5 đã dạy):
sudo ovs-vsctl add-br br-provider
sudo ovs-vsctl \
-- add-port br-provider cust-uplink \
-- set port cust-uplink tag=100 vlan_mode=dot1q-tunnel cvlans=10,20 \
-- set Open_vSwitch . other_config:vlan-limit=0
Đọc lại đúng cấu hình vừa tạo để tự xác nhận, thay vì chỉ tin lệnh chạy không báo lỗi:
sudo ovsdb-client dump Open_vSwitch Port
# Output minh họa
Port table
_uuid ... name tag trunks cvlans vlan_mode
--------------- --- --------------- ---- ------- -------- --------------
7f2a1e00-.... ... "cust-uplink" 100 [] [10, 20] "dot1q-tunnel"
(Output minh họa — cấu trúc đúng theo lệnh thật, số liệu cụ thể tùy máy; số cột thật nhiều hơn, đã lược bớt để dễ đọc.) trunks để rỗng — đúng, dot1q-tunnel không dùng trunks, việc lọc VLAN khách hàng là việc của cvlans, hai cột phục vụ hai vai trò khác nhau dù nhìn qua bạn có thể tưởng chúng giống nhau.
Cần verify thêm nếu triển khai production thật: dot1q-tunnel được thêm vào OVS từ bản 2.8.0 (2017) và cần kernel Linux 4.9+ để datapath kernel xử lý đúng — nếu bridge đang chạy datapath_type=netdev (userspace, Bài 3/14) thay vì kernel datapath mặc định, hành vi giới hạn kernel version này không áp dụng theo cùng cách, nên tự kiểm tra lại theo đúng loại datapath đang dùng trước khi đưa vào production.
Nếu chỉ nhớ được một điều sau bài này, hãy nhớ: vlan_mode mới là cột quyết định thật sự — tag/trunks/cvlans chỉ là dữ liệu đầu vào, còn vlan_mode quy định OVS diễn giải dữ liệu đó theo nghĩa nào. Và riêng dot1q-tunnel là giá trị duy nhất trong năm giá trị đó "khai đúng vẫn chạy sai" nếu thiếu một bước ở bảng Open_vSwitch: quên nâng other_config:vlan-limit lên khỏi mặc định 1, QinQ vẫn double-tag đúng trên dây, nhưng OVS sẽ không bao giờ tự nhìn xuyên qua được lớp C-TAG bên trong để bạn match/troubleshoot nó.
Bài tập cuối bài
- Dựng lại đúng lab Mục 3 (hai bridge
br-a/br-b, trunk qua veth pair) nhưng đổi trunk port sangvlan_mode=native-untaggedvớitag=10(VLAN 10 làm native) thay vìtrunkthường. Từ một namespace mới không gắn access port (dùng thẳng một veth cắm trực tiếp cạnh trunk port, gửi packet không tag), xác nhận packet đó được xử lý như thuộc VLAN 10 — trong khi ở cấu hìnhvlan_mode=trunkthuần Mục 3, packet không tag tương tự sẽ bị coi là VLAN 0 và không tới được namespace nào. - Dựng port
dot1q-tunnelnhư Mục 5, sau đó cố tình gửi một frame đã có sẵn tag VLAN 30 (không nằm trongcvlans=10,20) qua port đó bằngip link add ... type vlan id 30trên đầu gửi. Dùngtcpdump -e -nở đầu nhận (hoặcovs-appctl dpctl/dump-flowsnếu chạy kernel datapath) để tự quan sát: frame VLAN 30 có bị port từ chối/không match, hay vẫn lọt qua theo cách khác — đối chiếu lại đúng câu định nghĩacvlansđã trích ở Mục 5.
Nguồn tham khảo
- Định nghĩa đầy đủ 5 giá trị
Port.vlan_mode(access, trunk, native-tagged, native-untagged, dot1q-tunnel), cộttag/trunks/cvlans, và quy tắc suy luận mặc định khi bỏ trốngvlan_mode— 2026, ovs-vswitchd.conf.db(5), openvswitch.org. Open_vSwitch.other_config:qinq-ethtype: "optional string, either 802.1ad or 802.1q", TPID0x88a8/0x8100tương ứng, mặc định802.1ad— cùng nguồnovs-vswitchd.conf.db(5)ở trên.Open_vSwitch.other_config:vlan-limit: giới hạn số lớp VLAN header datapath match được, mặc định 1 để tương thích ngược với controller viết cho OVS trước bản 2.8, cần đặt0hoặc2để QinQ match được cả VLAN header thứ hai — cùng nguồnovs-vswitchd.conf.db(5)ở trên (chi tiết verify thêm ngoài danh sách research ban đầu, không có trong outline gốc).- Ví dụ
tag=9 vlan_mode=native-taggedcho uplink port, và ghi chúnative-untagged"probably work equally well" trong tình huống tương tự — 2026, VLAN Configuration Samples FAQ, docs.openvswitch.org. dot1q-tunnel(802.1ad/QinQ) được thêm vào Open vSwitch từ bản 2.8.0 (31/8/2017) — OVS NEWS, v2.8.0, github.com/openvswitch/ovs.- QinQ trên OVS dùng làm giải pháp nhẹ thay thế VXLAN/GENEVE/GRE trong một số trường hợp, tận dụng được NIC hardware offload, overhead thấp hơn tunnel (chỉ +4 byte/frame so với VLAN thường); cấu hình
vlan-limit(0 hoặc 2) là bước bắt buộc để bật QinQ đầy đủ — 6/6/2017, Open vSwitch: Overview of 802.1ad (QinQ) Support, developers.redhat.com. - Cần kernel Linux 4.9+ để kernel datapath hỗ trợ đúng QinQ VLAN — theo tóm tắt đã verify từ Red Hat Developer blog cùng chủ đề (cần verify thêm nếu số kernel version thay đổi ở bản phát hành mới hơn).
Tiếp theo: Module 2, Bài 9 — Tunnel: VXLAN/GRE/Geneve trên OVS, đối chiếu trực tiếp với VXLAN/EVPN vật lý.