Skip to content

Commit 26edfbf

Browse files
committed
changing to use ports and protocols
1 parent be6d5cf commit 26edfbf

4 files changed

Lines changed: 147 additions & 64 deletions

File tree

krkn/scenario_plugins/network_chaos_ng/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,14 @@ def validate(self) -> list[str]:
6666

6767
@dataclass
6868
class NetworkFilterConfig(BaseNetworkChaosConfig):
69-
ports: list[int]
70-
protocols: list[str]
69+
ports: list[int] = None
70+
protocols: list[str] = None
71+
72+
def __post_init__(self):
73+
if self.ports is None:
74+
self.ports = []
75+
if self.protocols is None:
76+
self.protocols = ["tcp", "udp"]
7177

7278
def validate(self) -> list[str]:
7379
errors = super().validate()

krkn/scenario_plugins/network_chaos_ng/modules/utils_network_filter.py

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -135,38 +135,28 @@ def apply_tc_vmi_chaos(
135135
namespace: str,
136136
pid: str,
137137
iface: str,
138+
config: NetworkFilterConfig,
138139
parallel: bool,
139140
vmi_name: str,
140-
):
141-
"""Block all traffic on the VMI's tap interface using tc.
141+
) -> Tuple[list[str], list[str]]:
142+
"""Apply iptables rules inside the virt-launcher netns via nsenter.
142143
143-
Targets tap0 (the VM-facing end of the KubeVirt bridge) rather than the
144-
bridge slave (ovn-udn1-nic). Blocking the bridge slave also cuts OVN's
145-
BFD heartbeats and causes a node-wide network reconvergence; tap0 only
146-
connects to QEMU so blocking it isolates only this VMI.
144+
Targets the tap interface (tap0) rather than the bridge slave (ovn-udn1-nic)
145+
so that OVN's BFD heartbeats on the bridge are unaffected. Rules are applied
146+
inside the virt-launcher's network namespace using nsenter, matching the same
147+
iptables approach used by pod_network_filter and node_network_filter.
147148
148-
tc operates at the device layer below iptables and works without br_netfilter:
149-
- root netem loss 100% -> drops traffic sent toward the VM
150-
- ingress + matchall -> drops traffic sent by the VM
151-
Only one pid is needed because all processes in the container share a netns.
149+
Returns (input_rules, output_rules) needed for cleanup.
152150
"""
153-
ns = f"nsenter --target {pid} --net --"
154-
log_info(f"applying tc block on {iface} (egress netem + ingress drop)", parallel, vmi_name)
155-
kubecli.exec_cmd_in_pod(
156-
[f"{ns} tc qdisc add dev {iface} root netem loss 100%"],
157-
chaos_pod_name,
158-
namespace,
159-
)
160-
kubecli.exec_cmd_in_pod(
161-
[f"{ns} tc qdisc add dev {iface} ingress"],
162-
chaos_pod_name,
163-
namespace,
164-
)
165-
kubecli.exec_cmd_in_pod(
166-
[f"{ns} tc filter add dev {iface} parent ffff: protocol all matchall action drop"],
167-
chaos_pod_name,
168-
namespace,
151+
log_info(
152+
f"applying iptables rules on {iface} "
153+
f"(ports:{config.ports}, protocols:{config.protocols})",
154+
parallel,
155+
vmi_name,
169156
)
157+
input_rules, output_rules = generate_namespaced_rules([iface], config, [pid])
158+
apply_network_rules(kubecli, input_rules, output_rules, chaos_pod_name, namespace, parallel, vmi_name)
159+
return input_rules, output_rules
170160

171161

172162
def clean_tc_vmi_chaos(
@@ -175,16 +165,12 @@ def clean_tc_vmi_chaos(
175165
namespace: str,
176166
pid: str,
177167
iface: str,
168+
input_rules: list[str],
169+
output_rules: list[str],
178170
):
179-
"""Remove tc qdiscs applied by apply_tc_vmi_chaos."""
180-
ns = f"nsenter --target {pid} --net --"
181-
for cmd in [
182-
f"{ns} tc qdisc del dev {iface} root",
183-
f"{ns} tc qdisc del dev {iface} ingress",
184-
]:
185-
try:
186-
kubecli.exec_cmd_in_pod([cmd], chaos_pod_name, namespace)
187-
except Exception:
188-
pass
171+
"""Remove iptables rules applied by apply_tc_vmi_chaos."""
172+
clean_network_rules_namespaced(
173+
kubecli, input_rules, output_rules, chaos_pod_name, namespace, [pid]
174+
)
189175

190176

krkn/scenario_plugins/network_chaos_ng/modules/vmi_network_filter.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ def _rollback(
5353
network_chaos_pod_name: str,
5454
netns_pid: str = None,
5555
iface: str = None,
56+
input_rules: list = None,
57+
output_rules: list = None,
5658
):
57-
if netns_pid and iface:
59+
if netns_pid and iface and input_rules is not None and output_rules is not None:
5860
clean_tc_vmi_chaos(
5961
self.kubecli.get_lib_kubernetes(),
6062
network_chaos_pod_name,
6163
namespace,
6264
netns_pid,
6365
iface,
66+
input_rules,
67+
output_rules,
6468
)
6569
self.kubecli.get_lib_kubernetes().delete_pod(
6670
network_chaos_pod_name, namespace
@@ -75,6 +79,8 @@ def run(self, target: str, error_queue: queue.Queue = None):
7579
network_chaos_pod_name = None
7680
netns_pid = None
7781
iface = None
82+
input_rules = None
83+
output_rules = None
7884

7985
try:
8086
namespace, vmi_name = target.split("/", 1)
@@ -241,36 +247,32 @@ def run(self, target: str, error_queue: queue.Queue = None):
241247
network_chaos_pod_name,
242248
)
243249

244-
# Use tc (traffic control) to block traffic on the bridge slave.
245-
# iptables FORWARD rules are ineffective here because L2-bridged
246-
# traffic bypasses iptables unless br_netfilter is loaded and
247-
# net.bridge.bridge-nf-call-iptables=1, which is not guaranteed.
248-
# tc netem/ingress operates below iptables at the device level.
249-
apply_tc_vmi_chaos(
250+
input_rules, output_rules = apply_tc_vmi_chaos(
250251
self.kubecli.get_lib_kubernetes(),
251252
network_chaos_pod_name,
252253
namespace,
253254
netns_pid,
254255
iface,
256+
scoped_config,
255257
parallel,
256258
vmi_name,
257259
)
258260

259261
log_info(
260-
f"waiting {self.config.test_duration} seconds before removing tc rules",
262+
f"waiting {self.config.test_duration} seconds before removing iptables rules",
261263
parallel,
262264
network_chaos_pod_name,
263265
)
264266

265267
time.sleep(self.config.test_duration)
266268

267-
log_info("removing tc rules", parallel, network_chaos_pod_name)
269+
log_info("removing iptables rules", parallel, network_chaos_pod_name)
268270

269-
self._rollback(namespace, network_chaos_pod_name, netns_pid, iface)
271+
self._rollback(namespace, network_chaos_pod_name, netns_pid, iface, input_rules, output_rules)
270272

271273
except Exception as e:
272274
if network_chaos_pod_name:
273-
self._rollback(namespace, network_chaos_pod_name, netns_pid, iface)
275+
self._rollback(namespace, network_chaos_pod_name, netns_pid, iface, input_rules, output_rules)
274276
if error_queue is None:
275277
raise e
276278
else:

tests/test_vmi_network_filter.py

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def _patch_run(self):
210210
# ------------------------------------------------------------------ success
211211

212212
@patch(f"{MODULE}.clean_tc_vmi_chaos")
213-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
213+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
214214
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
215215
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
216216
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@@ -237,7 +237,7 @@ def test_run_success(
237237
self.mock_kubernetes.delete_pod.assert_called_once()
238238

239239
@patch(f"{MODULE}.clean_tc_vmi_chaos")
240-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
240+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
241241
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
242242
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
243243
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@@ -352,7 +352,7 @@ def test_run_strips_container_id_prefix(self, mock_log, mock_deploy, mock_find):
352352
self.mock_kubernetes.get_pod_pids.return_value = ["100"]
353353

354354
with patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0"), \
355-
patch(f"{MODULE}.apply_tc_vmi_chaos"), \
355+
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])), \
356356
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
357357
patch(f"{MODULE}.time.sleep"):
358358
self.module.run("virt-density-udn-3/virt-server-3")
@@ -395,7 +395,7 @@ def test_run_error_queue_captures_exception(self, mock_log, mock_deploy):
395395
self.assertIn("not found", error_queue.get())
396396

397397
@patch(f"{MODULE}.clean_tc_vmi_chaos")
398-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
398+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
399399
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
400400
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
401401
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@@ -412,7 +412,7 @@ def test_run_no_error_queue_raises_directly(
412412
# ------------------------------------------------------------------ apply / clean called correctly
413413

414414
@patch(f"{MODULE}.clean_tc_vmi_chaos")
415-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
415+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
416416
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
417417
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
418418
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@@ -432,7 +432,7 @@ def test_run_apply_and_clean_called_with_tap_and_pid(
432432
self.assertEqual(clean_args[4], "tap0") # iface
433433

434434
@patch(f"{MODULE}.clean_tc_vmi_chaos")
435-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
435+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], []))
436436
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
437437
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
438438
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@@ -479,21 +479,31 @@ def setUp(self):
479479
# ------------------------------------------------------------------ _rollback directly
480480

481481
def test_rollback_calls_clean_then_delete_when_tc_applied(self):
482+
input_rules = ["nsenter ... iptables -I INPUT 1 -i tap0 -p tcp -j DROP"]
483+
output_rules = ["nsenter ... iptables -I OUTPUT 1 -p tcp -j DROP"]
482484
with patch(f"{MODULE}.clean_tc_vmi_chaos") as mock_clean:
483-
self.module._rollback("ns", "chaos-pod", "101", "tap0")
485+
self.module._rollback("ns", "chaos-pod", "101", "tap0", input_rules, output_rules)
484486

485487
mock_clean.assert_called_once_with(
486-
self.mock_kubernetes, "chaos-pod", "ns", "101", "tap0"
488+
self.mock_kubernetes, "chaos-pod", "ns", "101", "tap0", input_rules, output_rules
487489
)
488490
self.mock_kubernetes.delete_pod.assert_called_once_with("chaos-pod", "ns")
489491

490-
def test_rollback_skips_clean_when_tc_not_applied(self):
492+
def test_rollback_skips_clean_when_no_rules(self):
491493
with patch(f"{MODULE}.clean_tc_vmi_chaos") as mock_clean:
492494
self.module._rollback("ns", "chaos-pod")
493495

494496
mock_clean.assert_not_called()
495497
self.mock_kubernetes.delete_pod.assert_called_once_with("chaos-pod", "ns")
496498

499+
def test_rollback_skips_clean_when_rules_none_but_pid_set(self):
500+
"""Chaos pod deployed, netns_pid found, but rules never applied: skip clean."""
501+
with patch(f"{MODULE}.clean_tc_vmi_chaos") as mock_clean:
502+
self.module._rollback("ns", "chaos-pod", "101", "tap0", None, None)
503+
504+
mock_clean.assert_not_called()
505+
self.mock_kubernetes.delete_pod.assert_called_once_with("chaos-pod", "ns")
506+
497507
# ------------------------------------------------------------------ rollback from run: error before tc
498508

499509
@patch(f"{MODULE}.clean_tc_vmi_chaos")
@@ -528,7 +538,7 @@ def test_run_rollback_does_not_clean_when_no_netns_pid(
528538
# ------------------------------------------------------------------ rollback from run: error after tc
529539

530540
@patch(f"{MODULE}.clean_tc_vmi_chaos")
531-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
541+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=(["in_rule"], ["out_rule"]))
532542
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
533543
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
534544
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
@@ -547,24 +557,103 @@ def test_run_rollback_cleans_tc_and_deletes_pod_on_error_after_tc(
547557
self.mock_kubernetes.delete_pod.assert_called_once()
548558

549559
@patch(f"{MODULE}.clean_tc_vmi_chaos")
550-
@patch(f"{MODULE}.apply_tc_vmi_chaos")
560+
@patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=(["in_rule"], ["out_rule"]))
551561
@patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0")
552562
@patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101")
553563
@patch(f"{MODULE}.deploy_network_chaos_ng_pod")
554564
@patch(f"{MODULE}.time.sleep")
555565
@patch(f"{MODULE}.log_info")
556-
def test_run_rollback_passes_correct_pid_and_iface_to_clean(
566+
def test_run_rollback_passes_correct_pid_iface_and_rules_to_clean(
557567
self, mock_log, mock_sleep, mock_deploy, mock_find, mock_tap, mock_apply, mock_clean
558568
):
559-
"""Clean must receive the resolved netns_pid and iface, not defaults."""
569+
"""Clean must receive the resolved netns_pid, iface, and the rules returned by apply."""
560570
mock_sleep.side_effect = RuntimeError("interrupted")
561571

562572
with self.assertRaises(RuntimeError):
563573
self.module.run("virt-density-udn-3/virt-server-3")
564574

565575
clean_args = mock_clean.call_args[0]
566-
self.assertEqual(clean_args[3], "101") # netns_pid
567-
self.assertEqual(clean_args[4], "tap0") # iface
576+
self.assertEqual(clean_args[3], "101") # netns_pid
577+
self.assertEqual(clean_args[4], "tap0") # iface
578+
self.assertEqual(clean_args[5], ["in_rule"]) # input_rules from apply
579+
self.assertEqual(clean_args[6], ["out_rule"]) # output_rules from apply
580+
581+
582+
class TestVmiNetworkFilterPortsProtocols(unittest.TestCase):
583+
584+
def setUp(self):
585+
self.mock_kubecli = MagicMock()
586+
self.mock_kubernetes = MagicMock()
587+
self.mock_kubecli.get_lib_kubernetes.return_value = self.mock_kubernetes
588+
589+
self.mock_kubernetes.get_vmi.return_value = {"status": {"nodeName": "worker-1"}}
590+
self.mock_kubernetes.list_pods.return_value = ["virt-launcher-virt-server-3-abc12"]
591+
compute = _make_container("compute", ready=True, container_id="containerd://deadbeef")
592+
mock_pod_info = MagicMock()
593+
mock_pod_info.containers = [compute]
594+
self.mock_kubernetes.get_pod_info.return_value = mock_pod_info
595+
self.mock_kubernetes.get_pod_pids.return_value = ["100", "101", "102"]
596+
597+
def _run_and_capture_apply(self, **config_overrides):
598+
config = _make_config(**config_overrides)
599+
module = VmiNetworkFilterModule(config, self.mock_kubecli)
600+
with patch(f"{MODULE}.deploy_network_chaos_ng_pod"), \
601+
patch(f"{MODULE}.find_virt_launcher_netns_pid", return_value="101"), \
602+
patch(f"{MODULE}.get_vmi_tap_interface", return_value="tap0"), \
603+
patch(f"{MODULE}.apply_tc_vmi_chaos", return_value=([], [])) as mock_apply, \
604+
patch(f"{MODULE}.clean_tc_vmi_chaos"), \
605+
patch(f"{MODULE}.time.sleep"), \
606+
patch(f"{MODULE}.log_info"):
607+
module.run("virt-density-udn-3/virt-server-3")
608+
return mock_apply
609+
610+
def test_apply_receives_specific_ports(self):
611+
mock_apply = self._run_and_capture_apply(ports=[53, 80, 443])
612+
config_arg = mock_apply.call_args[0][5]
613+
self.assertEqual(config_arg.ports, [53, 80, 443])
614+
615+
def test_apply_receives_empty_ports_for_all_traffic(self):
616+
mock_apply = self._run_and_capture_apply(ports=[])
617+
config_arg = mock_apply.call_args[0][5]
618+
self.assertEqual(config_arg.ports, [])
619+
620+
def test_apply_receives_tcp_only_protocol(self):
621+
mock_apply = self._run_and_capture_apply(protocols=["tcp"])
622+
config_arg = mock_apply.call_args[0][5]
623+
self.assertEqual(config_arg.protocols, ["tcp"])
624+
625+
def test_apply_receives_udp_only_protocol(self):
626+
mock_apply = self._run_and_capture_apply(protocols=["udp"])
627+
config_arg = mock_apply.call_args[0][5]
628+
self.assertEqual(config_arg.protocols, ["udp"])
629+
630+
def test_apply_receives_both_protocols(self):
631+
mock_apply = self._run_and_capture_apply(protocols=["tcp", "udp"])
632+
config_arg = mock_apply.call_args[0][5]
633+
self.assertIn("tcp", config_arg.protocols)
634+
self.assertIn("udp", config_arg.protocols)
635+
636+
def test_apply_receives_dns_ports_with_both_protocols(self):
637+
"""DNS blackout: port 53 on tcp and udp."""
638+
mock_apply = self._run_and_capture_apply(ports=[53], protocols=["tcp", "udp"])
639+
config_arg = mock_apply.call_args[0][5]
640+
self.assertEqual(config_arg.ports, [53])
641+
self.assertIn("tcp", config_arg.protocols)
642+
self.assertIn("udp", config_arg.protocols)
643+
644+
def test_apply_receives_management_ports(self):
645+
"""Management plane loss: SSH + HTTPS + k8s API."""
646+
mock_apply = self._run_and_capture_apply(ports=[22, 443, 6443], protocols=["tcp"])
647+
config_arg = mock_apply.call_args[0][5]
648+
self.assertEqual(config_arg.ports, [22, 443, 6443])
649+
self.assertEqual(config_arg.protocols, ["tcp"])
650+
651+
def test_apply_config_has_resolved_namespace_not_regex(self):
652+
"""The config passed to apply must use the real namespace from the target string."""
653+
mock_apply = self._run_and_capture_apply(namespace="virt-density-udn-.*")
654+
config_arg = mock_apply.call_args[0][5]
655+
self.assertEqual(config_arg.namespace, "virt-density-udn-3")
656+
self.assertNotEqual(config_arg.namespace, "virt-density-udn-.*")
568657

569658

570659
if __name__ == "__main__":

0 commit comments

Comments
 (0)