Skip to content

Commit 46e330d

Browse files
authored
Merge pull request #89 from BrennanPaciorek/gh87
Add/remove interfaces to zone using PCI device ID
2 parents 0feae11 + 2b0bb1b commit 46e330d

File tree

4 files changed

+291
-11
lines changed

4 files changed

+291
-11
lines changed

README.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,63 @@ source: 192.0.2.0/24
366366
367367
String or list of interface name strings.
368368
369-
```
369+
```yaml
370370
interface: eth2
371371
```
372372

373+
This role handles interface arguments similar to
374+
how firewalld's cli, `firewall-cmd` does, i.e.
375+
manages the interface through NetworkManger if possible,
376+
and handles the interface binding purely through firewalld
377+
otherwise.
378+
379+
```
380+
WARNING: Neither firewalld nor this role throw any
381+
errors if the interface name specified is not
382+
tied to any existing network interface. This can cause confusion
383+
when attempting to add an interface via PCI device ID,
384+
for which you should use the parameter `interface_pci_id`
385+
instead of the `interface` parameter.
386+
387+
Allow interface named '8086:15d7' in dmz zone
388+
389+
firewall:
390+
- zone: dmz
391+
interface: 8086:15d7
392+
state: enabled
393+
394+
The above will successfully add a nftables/iptables rule
395+
for an interface named `8086:15d7`, but no traffic should/will
396+
ever match to an interface with this name.
397+
398+
TLDR - When using this parameter, please stick only to using
399+
logical interface names that you know exist on the device to
400+
avoid confusing behavior.
401+
```
402+
403+
### interface_pci_id
404+
405+
String or list of interface PCI device IDs.
406+
Accepts PCI IDs if the wildcard `XXXX:YYYY` applies
407+
where:
408+
- XXXX: Hexadecimal, corresponds to Vendor ID
409+
- YYYY: Hexadecimal, corresponds to Device ID
410+
411+
```yaml
412+
# PCI id for Intel Corporation Ethernet Connection
413+
interface_pci_id: 8086:15d7
414+
```
415+
416+
Only accepts PCI devices IDs that correspond to a named network interface,
417+
and converts all PCI device IDs to their respective logical interface names.
418+
419+
If a PCI id corresponds to more than one logical interface name,
420+
all interfaces with the PCI id specified will have the play applied.
421+
422+
A list of PCI devices with their IDs can be retrieved using `lcpci -nn`.
423+
For more information on PCI device IDs, see the linux man page at:
424+
https://man7.org/linux/man-pages/man5/pci.ids.5.html
425+
373426
### icmp_block
374427

375428
String or list of ICMP type strings to block. The ICMP type names needs to be
@@ -586,6 +639,14 @@ firewall:
586639
state: disabled
587640
```
588641
642+
Allow interface with PCI device ID '8086:15d7' in dmz zone
643+
```yaml
644+
firewall:
645+
- zone: dmz
646+
interface_pci_id: 8086:15d7
647+
state: enabled
648+
```
649+
589650
Example Playbooks
590651
-----------------
591652

library/firewall_lib.py

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@
9898
required: false
9999
type: list
100100
elements: str
101+
interface_pci_id:
102+
description:
103+
List of inteface PCI device ID strings.
104+
PCI device ID needs to correspond to a named network interface.
105+
required: false
106+
type: list
107+
elements: str
101108
icmp_block:
102109
description:
103110
List of ICMP type strings to block.
@@ -205,6 +212,8 @@
205212

206213
from distutils.version import LooseVersion
207214
from ansible.module_utils.basic import AnsibleModule
215+
import re
216+
import os
208217

209218
try:
210219
import firewall.config
@@ -222,6 +231,52 @@
222231
except ImportError:
223232
HAS_FIREWALLD = False
224233

234+
try:
235+
from firewall.core.fw_nm import (
236+
nm_is_imported,
237+
nm_get_connection_of_interface,
238+
nm_get_zone_of_connection,
239+
nm_set_zone_of_connection,
240+
nm_get_interfaces,
241+
nm_get_client,
242+
)
243+
244+
NM_IMPORTED = nm_is_imported()
245+
except ImportError:
246+
NM_IMPORTED = False
247+
248+
249+
PCI_REGEX = re.compile("[0-9a-fA-F]{4}:[0-9a-fA-F]{4}")
250+
251+
252+
def try_get_connection_of_interface(interface):
253+
try:
254+
return nm_get_connection_of_interface(interface)
255+
except Exception:
256+
return None
257+
258+
259+
def try_set_zone_of_interface(module, _zone, interface):
260+
if NM_IMPORTED:
261+
connection = try_get_connection_of_interface(interface)
262+
if connection is not None:
263+
if _zone == "":
264+
zone_string = "the default zone"
265+
else:
266+
zone_string = _zone
267+
if _zone == nm_get_zone_of_connection(connection):
268+
module.log(
269+
msg="The interface is under control of NetworkManager and already bound to '%s'"
270+
% zone_string
271+
)
272+
elif not module.check_mode:
273+
nm_set_zone_of_connection(_zone, connection)
274+
return True
275+
return False
276+
277+
278+
# Above: adapted from firewall-cmd source code
279+
225280

226281
def create_service(module, fw, service):
227282
if not module.check_mode:
@@ -269,6 +324,46 @@ def handle_interface_permanent(
269324
fw_settings.addInterface(item)
270325

271326

327+
pci_ids = None
328+
329+
330+
def get_interface_pci():
331+
pci_dict = {}
332+
for interface in nm_get_interfaces():
333+
# udi/device/[vendor, device]
334+
interface_ids = []
335+
device_udi = nm_get_client().get_device_by_iface(interface).get_udi()
336+
device_path = os.path.join(device_udi, "device")
337+
for field in ["vendor", "device"]:
338+
with open(os.path.join(device_path, field)) as _file:
339+
interface_ids.append(_file.readline().strip(" \n")[2:])
340+
interface_ids = ":".join(interface_ids)
341+
if interface_ids not in pci_dict:
342+
pci_dict[interface_ids] = [interface]
343+
else:
344+
pci_dict[interface_ids].append(interface)
345+
return pci_dict
346+
347+
348+
def parse_pci_id(module, item):
349+
if PCI_REGEX.search(item):
350+
global pci_ids
351+
if not pci_ids:
352+
pci_ids = get_interface_pci()
353+
354+
interface_name = pci_ids.get(item)
355+
if interface_name:
356+
return interface_name
357+
358+
module.warn(msg="No network interfaces found with PCI device ID %s" % item)
359+
else:
360+
module.fail_json(
361+
msg="PCI id %s does not match format: XXXX:XXXX (X = hexadecimal number)"
362+
% item
363+
)
364+
return []
365+
366+
272367
def parse_port(module, item):
273368
_port, _protocol = item.split("/")
274369
if _protocol is None:
@@ -447,6 +542,9 @@ def main():
447542
rich_rule=dict(required=False, type="list", elements="str", default=[]),
448543
source=dict(required=False, type="list", elements="str", default=[]),
449544
interface=dict(required=False, type="list", elements="str", default=[]),
545+
interface_pci_id=dict(
546+
required=False, type="list", elements="str", default=[]
547+
),
450548
icmp_block=dict(required=False, type="list", elements="str", default=[]),
451549
icmp_block_inversion=dict(required=False, type="bool", default=None),
452550
timeout=dict(required=False, type="int", default=0),
@@ -532,6 +630,10 @@ def main():
532630
elif destination_ipv6 and ip_type == "ipv6":
533631
module.fail_json(msg="cannot have more than one destination ipv6")
534632
interface = module.params["interface"]
633+
for _interface in module.params["interface_pci_id"]:
634+
for interface_name in parse_pci_id(module, _interface):
635+
if interface_name not in interface:
636+
interface.append(interface_name)
535637
icmp_block = module.params["icmp_block"]
536638
icmp_block_inversion = module.params["icmp_block_inversion"]
537639
timeout = module.params["timeout"]
@@ -1091,21 +1193,27 @@ def exception_handler(exception_message):
10911193
if not module.check_mode:
10921194
fw.changeZoneOfInterface(zone, item)
10931195
changed = True
1094-
if permanent and not fw_settings.queryInterface(item):
1095-
if not module.check_mode:
1096-
handle_interface_permanent(
1097-
zone, item, fw_zone, fw_settings, fw, fw_offline, module
1098-
)
1099-
changed = True
1196+
if permanent:
1197+
if try_set_zone_of_interface(module, zone, item):
1198+
changed = True
1199+
elif not fw_settings.queryInterface(item):
1200+
if not module.check_mode:
1201+
handle_interface_permanent(
1202+
zone, item, fw_zone, fw_settings, fw, fw_offline, module
1203+
)
1204+
changed = True
11001205
elif state == "disabled":
11011206
if runtime and fw.queryInterface(zone, item):
11021207
if not module.check_mode:
11031208
fw.removeInterface(zone, item)
11041209
changed = True
1105-
if permanent and fw_settings.queryInterface(item):
1106-
if not module.check_mode:
1107-
fw_settings.removeInterface(item)
1108-
changed = True
1210+
if permanent:
1211+
if try_set_zone_of_interface(module, "", item):
1212+
changed = True
1213+
elif fw_settings.queryInterface(item):
1214+
if not module.check_mode:
1215+
fw_settings.removeInterface(item)
1216+
changed = True
11091217

11101218
# icmp_block
11111219
for item in icmp_block:

tests/tests_interface_pci.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
# Tests interface_pci field, test must be used in VM
3+
- name: Test interfaces with PCI ids
4+
hosts: all
5+
become: true
6+
roles:
7+
- linux-system-roles.firewall
8+
9+
tasks:
10+
- name: get backend from dbus
11+
command: >-
12+
dbus-send --system --print-reply --type=method_call
13+
--dest=org.fedoraproject.FirewallD1
14+
/org/fedoraproject/FirewallD1/config
15+
org.freedesktop.DBus.Properties.Get
16+
string:"org.fedoraproject.FirewallD1.config"
17+
string:"FirewallBackend"
18+
ignore_errors: yes
19+
register: result
20+
21+
- name: get backend from result
22+
set_fact:
23+
nftables_backend:
24+
"{{ result is not failed and 'nftables' in result.stdout }}"
25+
26+
- name: test interfaces with PCI ids
27+
block:
28+
- name: add pci device ethernet controller
29+
include_role:
30+
name: linux-system-roles.firewall
31+
vars:
32+
firewall:
33+
zone: internal
34+
interface_pci_id: 1af4:0001
35+
state: enabled
36+
permanent: yes
37+
38+
- name: add pci device again
39+
include_role:
40+
name: linux-system-roles.firewall
41+
vars:
42+
firewall:
43+
zone: internal
44+
interface_pci_id: 1af4:0001
45+
state: enabled
46+
permanent: yes
47+
48+
- name: assert pcid not in nftable ruleset
49+
command: nft list ruleset
50+
register: result
51+
failed_when: result is failed or '1af4:0001' in result.stdout
52+
when: nftables_backend | bool
53+
54+
- name: assert pcid not in iptables rules
55+
command: iptables -S
56+
register: result
57+
failed_when: result is failed or '1af4:0001' in result.stdout
58+
when: not nftables_backend | bool
59+
60+
- name: remove interface from internal
61+
include_role:
62+
name: linux-system-roles.firewall
63+
vars:
64+
firewall:
65+
zone: internal
66+
interface_pci_id: 1af4:0001
67+
state: disabled
68+
permament: yes
69+
always:
70+
- name: Cleanup
71+
tags:
72+
- tests::cleanup
73+
include_role:
74+
name: linux-system-roles.firewall
75+
vars:
76+
firewall:
77+
- previous: replaced
78+
79+
...

tests/unit/test_firewall_lib.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,20 @@
188188
}
189189
},
190190
},
191+
"InterfacePciId": {
192+
"input": {"interface_pci_id": ["600D:7C1D"]},
193+
"enabled": {
194+
"expected": {
195+
"runtime": [call("default", "600D:7C1D")],
196+
},
197+
},
198+
"disabled": {
199+
"expected": {
200+
"runtime": [call("default", "600D:7C1D")],
201+
"permanent": [call("600D:7C1D")],
202+
},
203+
},
204+
},
191205
"IcmpBlock": {
192206
"input": {
193207
"icmp_block": ["echo-request"],
@@ -360,6 +374,24 @@ def test_parse_forward_port(self):
360374
rc = firewall_lib.parse_forward_port(module, item)
361375
self.assertEqual(("a", "b", None, None), rc)
362376

377+
@patch("firewall_lib.AnsibleModule", new_callable=MockAnsibleModule)
378+
@patch("firewall_lib.HAS_FIREWALLD", True)
379+
@patch("firewall_lib.pci_ids", {"600D:7C1D": ["eth0"]})
380+
def test_parse_pci_id(self, am_class):
381+
am = am_class.return_value
382+
383+
am.params = {"interface_pci_id": ["123G:1111"]}
384+
with self.assertRaises(MockException):
385+
firewall_lib.main()
386+
am.fail_json.assert_called_with(
387+
msg="PCI id 123G:1111 does not match format: XXXX:XXXX (X = hexadecimal number)"
388+
)
389+
390+
am.params = {"interface_pci_id": ["600D:7C1D"]}
391+
with self.assertRaises(MockException):
392+
firewall_lib.main()
393+
am.fail_json.assert_called_with(msg="Options invalid without state option set")
394+
363395

364396
@patch("firewall_lib.AnsibleModule", new_callable=MockAnsibleModule)
365397
class FirewallLibMain(unittest.TestCase):

0 commit comments

Comments
 (0)