testprotocols 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- testprotocols/__init__.py +217 -0
- testprotocols/aftr_gateway.py +22 -0
- testprotocols/appliance_nat.py +52 -0
- testprotocols/appliance_uplinks.py +32 -0
- testprotocols/appliance_vlans.py +50 -0
- testprotocols/arp_client.py +26 -0
- testprotocols/bgp.py +55 -0
- testprotocols/conntrack.py +147 -0
- testprotocols/content_filtering.py +47 -0
- testprotocols/device_lifecycle.py +49 -0
- testprotocols/device_management.py +50 -0
- testprotocols/devices/__init__.py +46 -0
- testprotocols/devices/base.py +40 -0
- testprotocols/devices/client.py +133 -0
- testprotocols/devices/cpe.py +66 -0
- testprotocols/devices/infra.py +62 -0
- testprotocols/devices/sdwan.py +97 -0
- testprotocols/devices/switch.py +115 -0
- testprotocols/devices/traffic.py +53 -0
- testprotocols/devices/voice.py +69 -0
- testprotocols/devices/wan.py +60 -0
- testprotocols/dhcp_client.py +30 -0
- testprotocols/dhcp_server.py +23 -0
- testprotocols/discovery.py +20 -0
- testprotocols/dns_client.py +23 -0
- testprotocols/file_transfer.py +22 -0
- testprotocols/firewall.py +121 -0
- testprotocols/firewall_zones.py +133 -0
- testprotocols/first_hop_security.py +52 -0
- testprotocols/gateway_redundancy.py +29 -0
- testprotocols/http_client.py +36 -0
- testprotocols/http_server.py +22 -0
- testprotocols/hw_console.py +48 -0
- testprotocols/infra_controller.py +28 -0
- testprotocols/interface_dhcp.py +30 -0
- testprotocols/ip_interface.py +62 -0
- testprotocols/ip_routing.py +57 -0
- testprotocols/iperf_client.py +47 -0
- testprotocols/iperf_generator.py +42 -0
- testprotocols/iperf_server.py +41 -0
- testprotocols/l3_firewall.py +74 -0
- testprotocols/l7_firewall.py +32 -0
- testprotocols/link_aggregation.py +24 -0
- testprotocols/mac_table.py +20 -0
- testprotocols/models/__init__.py +304 -0
- testprotocols/models/dhcp.py +28 -0
- testprotocols/models/firewall.py +197 -0
- testprotocols/models/impairment.py +18 -0
- testprotocols/models/l2_common.py +53 -0
- testprotocols/models/multicast.py +22 -0
- testprotocols/models/networking.py +50 -0
- testprotocols/models/packets.py +21 -0
- testprotocols/models/qoe.py +31 -0
- testprotocols/models/radius.py +63 -0
- testprotocols/models/sdwan_appliance.py +637 -0
- testprotocols/models/switch.py +297 -0
- testprotocols/models/switch_routing.py +122 -0
- testprotocols/models/tr069.py +35 -0
- testprotocols/models/traffic.py +29 -0
- testprotocols/models/wan_edge.py +116 -0
- testprotocols/models/wifi.py +183 -0
- testprotocols/multicast_client.py +20 -0
- testprotocols/nat.py +87 -0
- testprotocols/netem_controller.py +42 -0
- testprotocols/network_endpoint.py +32 -0
- testprotocols/network_probe.py +27 -0
- testprotocols/nmap_scanner.py +27 -0
- testprotocols/ntp_client.py +26 -0
- testprotocols/ntp_config.py +25 -0
- testprotocols/ospf.py +24 -0
- testprotocols/packet_filter.py +144 -0
- testprotocols/pcap_capture.py +39 -0
- testprotocols/pdu_controller.py +26 -0
- testprotocols/port_poe.py +25 -0
- testprotocols/port_security.py +25 -0
- testprotocols/port_status.py +23 -0
- testprotocols/py.typed +0 -0
- testprotocols/qoe_browser.py +62 -0
- testprotocols/radius_client.py +78 -0
- testprotocols/radius_server.py +130 -0
- testprotocols/routed_interfaces.py +29 -0
- testprotocols/router.py +53 -0
- testprotocols/routing_read.py +22 -0
- testprotocols/sdwan_policy_manager.py +64 -0
- testprotocols/sip_phone.py +230 -0
- testprotocols/sip_server.py +205 -0
- testprotocols/site_to_site_vpn.py +61 -0
- testprotocols/snmp_client.py +17 -0
- testprotocols/spanning_tree.py +37 -0
- testprotocols/static_routes.py +47 -0
- testprotocols/storm_control.py +24 -0
- testprotocols/streaming_server.py +32 -0
- testprotocols/switch_acl.py +29 -0
- testprotocols/switch_ports.py +28 -0
- testprotocols/switch_qos.py +33 -0
- testprotocols/switch_vlans.py +34 -0
- testprotocols/syslog_config.py +31 -0
- testprotocols/tftp_server.py +22 -0
- testprotocols/threat_prevention.py +60 -0
- testprotocols/tr069_client.py +47 -0
- testprotocols/tr069_server.py +151 -0
- testprotocols/traffic_shaping.py +54 -0
- testprotocols/upnp_client.py +37 -0
- testprotocols/vlan_client.py +22 -0
- testprotocols/wan_link_admin.py +34 -0
- testprotocols/wifi_bss.py +197 -0
- testprotocols/wifi_client.py +72 -0
- testprotocols/wifi_mesh.py +259 -0
- testprotocols/wifi_onboarding.py +105 -0
- testprotocols/wifi_radio.py +153 -0
- testprotocols/wifi_rf.py +78 -0
- testprotocols/wifi_stations.py +59 -0
- testprotocols/wifi_transitions.py +112 -0
- testprotocols-0.1.0.dist-info/METADATA +29 -0
- testprotocols-0.1.0.dist-info/RECORD +119 -0
- testprotocols-0.1.0.dist-info/WHEEL +5 -0
- testprotocols-0.1.0.dist-info/licenses/LICENSE +201 -0
- testprotocols-0.1.0.dist-info/licenses/NOTICE +11 -0
- testprotocols-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""WiFi-domain data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class WifiDfsState:
|
|
10
|
+
"""DFS state of a radio.
|
|
11
|
+
|
|
12
|
+
A radio is in CAC for ~60s after tuning to a DFS channel; during CAC it
|
|
13
|
+
cannot transmit. Channels on which radar was recently detected enter the
|
|
14
|
+
Non-Occupancy List for ~30 minutes and are unavailable until the NOL ages out.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
is_in_cac: bool
|
|
18
|
+
cac_remaining_seconds: int | None # None when not in CAC
|
|
19
|
+
nol_channels: list[int] = field(default_factory=list[int])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class WifiCaptiveConfig:
|
|
24
|
+
"""Per-BSS captive-portal state."""
|
|
25
|
+
|
|
26
|
+
enabled: bool
|
|
27
|
+
redirect_url: str | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class WifiBssConfig:
|
|
32
|
+
"""Configuration of a single BSS, as returned by WifiBss read methods.
|
|
33
|
+
|
|
34
|
+
*passphrase* is intentionally absent — write-only across the contract.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str # stable logical handle
|
|
38
|
+
band: str
|
|
39
|
+
ssid: str # broadcast SSID string
|
|
40
|
+
bssid: str # MAC address assigned to this BSS
|
|
41
|
+
enabled: bool
|
|
42
|
+
broadcast_enabled: bool
|
|
43
|
+
security_mode: str
|
|
44
|
+
radius_server_name: str | None # references RadiusClient registry; None when not Enterprise
|
|
45
|
+
mfp: str # "off" | "optional" | "required"
|
|
46
|
+
vlan_id: int | None
|
|
47
|
+
max_clients: int | None
|
|
48
|
+
dtim_period: int
|
|
49
|
+
captive_portal: WifiCaptiveConfig
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class WifiStation:
|
|
54
|
+
"""An associated station's identity, capabilities, and current stats.
|
|
55
|
+
|
|
56
|
+
Stats are point-in-time snapshots; cumulative counters (bytes, packets,
|
|
57
|
+
retries) are since the start of the current association.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
mac: str # canonical: lowercase colon-separated
|
|
61
|
+
bss_name: str # logical BSS handle the station is associated to
|
|
62
|
+
band: str # "2.4GHz" / "5GHz" / "6GHz"
|
|
63
|
+
ip_address: str | None # station's IP if known to the AP (e.g. via DHCP snooping)
|
|
64
|
+
associated_since: float # Unix timestamp
|
|
65
|
+
rssi_dbm: int
|
|
66
|
+
snr_db: int | None # None if the driver doesn't report SNR
|
|
67
|
+
tx_rate_mbps: float # last known PHY rate AP -> station
|
|
68
|
+
rx_rate_mbps: float # last known PHY rate station -> AP
|
|
69
|
+
tx_bytes: int
|
|
70
|
+
rx_bytes: int
|
|
71
|
+
tx_packets: int
|
|
72
|
+
rx_packets: int
|
|
73
|
+
tx_retries: int
|
|
74
|
+
capability_flags: list[str] = field(default_factory=list[str])
|
|
75
|
+
# capability_flags examples: ["HT", "VHT", "HE"], or ["EHT", "MLO"] for Wi-Fi 7
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class WifiAcl:
|
|
80
|
+
"""Per-BSS MAC access-control list state."""
|
|
81
|
+
|
|
82
|
+
bss_name: str
|
|
83
|
+
mode: str # "disabled" | "allow" | "deny"
|
|
84
|
+
# MACs, canonical lowercase colon-separated
|
|
85
|
+
entries: list[str] = field(default_factory=list[str])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class WifiNeighbor:
|
|
90
|
+
"""A neighbour BSS observed by an off-channel scan."""
|
|
91
|
+
|
|
92
|
+
bssid: str # MAC, canonical lowercase colon-separated
|
|
93
|
+
ssid: str # may be empty for hidden SSIDs
|
|
94
|
+
band: str # the band the scanner observed it on
|
|
95
|
+
channel: int # operating channel of the neighbour
|
|
96
|
+
rssi_dbm: int
|
|
97
|
+
security_mode: str # best-effort identification
|
|
98
|
+
last_seen: float # Unix timestamp of the last beacon/probe-response
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class WifiChannelUtilization:
|
|
103
|
+
"""Per-radio channel-utilization breakdown.
|
|
104
|
+
|
|
105
|
+
All fields are 0-100. ``busy_pct`` is always populated; the
|
|
106
|
+
component splits (tx/rx/interference) are populated only on drivers
|
|
107
|
+
that report them separately.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
band: str
|
|
111
|
+
busy_pct: int # total channel occupancy
|
|
112
|
+
tx_pct: int | None # own transmissions
|
|
113
|
+
rx_pct: int | None # all reception (own BSS + neighbours)
|
|
114
|
+
interference_pct: int | None # non-WiFi interference, where the driver can distinguish
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class WifiRadioStats:
|
|
119
|
+
"""Cumulative per-radio TX/RX/retry counters."""
|
|
120
|
+
|
|
121
|
+
band: str
|
|
122
|
+
tx_bytes: int
|
|
123
|
+
rx_bytes: int
|
|
124
|
+
tx_packets: int
|
|
125
|
+
rx_packets: int
|
|
126
|
+
tx_retries: int # retransmitted frames
|
|
127
|
+
tx_failed: int # frames the driver gave up on (max retries exceeded)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class WifiTransitionConfig:
|
|
132
|
+
"""Per-BSS k/v/r configuration snapshot."""
|
|
133
|
+
|
|
134
|
+
bss_name: str
|
|
135
|
+
rrm_enabled: bool # 802.11k
|
|
136
|
+
btm_enabled: bool # 802.11v
|
|
137
|
+
ft_enabled: bool # 802.11r
|
|
138
|
+
# 802.11r over-the-DS (True) vs over-the-air (False); meaningful only when ft_enabled
|
|
139
|
+
ft_over_ds: bool
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class WifiMeshLink:
|
|
144
|
+
"""A wireless backhaul link between mesh agents."""
|
|
145
|
+
|
|
146
|
+
band: str # "2.4GHz" / "5GHz" / "6GHz"
|
|
147
|
+
channel: int
|
|
148
|
+
rssi_dbm: int # signal strength on the link
|
|
149
|
+
capacity_mbps: float # estimated PHY-rate capacity in Mbps
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class WifiMeshStatus:
|
|
154
|
+
"""A mesh participant's local status snapshot."""
|
|
155
|
+
|
|
156
|
+
# "controller" | "agent" | "controller-and-agent" | "uncommissioned"
|
|
157
|
+
role: str
|
|
158
|
+
enabled: bool
|
|
159
|
+
parent_mac: str | None # MAC of this agent's parent; None for the controller / root
|
|
160
|
+
hop_count: int # 0 for the controller; N for an agent N hops away
|
|
161
|
+
backhaul_link: WifiMeshLink | None # None when no uplink (root) or mesh disabled
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class WifiMeshNode:
|
|
166
|
+
"""Identity and position of a mesh agent in the topology."""
|
|
167
|
+
|
|
168
|
+
mac: str # canonical lowercase colon-separated
|
|
169
|
+
role: str # "controller" | "agent" | "controller-and-agent"
|
|
170
|
+
parent_mac: str | None # None for the controller / root
|
|
171
|
+
hop_count: int
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class WifiMeshTopology:
|
|
176
|
+
"""The mesh as known to the querying participant.
|
|
177
|
+
|
|
178
|
+
Controllers populate the full agent list; pure agents populate
|
|
179
|
+
only the parent + any peers they directly observe.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
controller_mac: str
|
|
183
|
+
agents: list[WifiMeshNode] = field(default_factory=list[WifiMeshNode])
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Multicast client template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for sending multicast group membership reports
|
|
4
|
+
from a test client device.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from testprotocols.models.multicast import MulticastGroupRecord
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class MulticastClient(Protocol):
|
|
16
|
+
"""Abstract contract for multicast client operations."""
|
|
17
|
+
|
|
18
|
+
def send_mldv2_report(self, mcast_group_record: MulticastGroupRecord, count: int) -> None:
|
|
19
|
+
"""Send *count* MLDv2 membership report packets for the given group records."""
|
|
20
|
+
...
|
testprotocols/nat.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Firewall / NAT template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for low-level NAT primitives — SNAT,
|
|
4
|
+
DNAT, and 1:1 / static NAT. Rules are identified by a stable logical
|
|
5
|
+
*name* and expressed as transport-agnostic ``NatRule`` records.
|
|
6
|
+
|
|
7
|
+
In scope: rule lifecycle (add / remove / list / get / flush), enable
|
|
8
|
+
toggles, and per-rule packet/byte counters.
|
|
9
|
+
|
|
10
|
+
Out of scope: high-level / named port-forwarding entries (see the
|
|
11
|
+
port-mapping surface on ``firewall.Firewall``), packet-filter rules
|
|
12
|
+
(see ``packet_filter`` or the rule-administration surface inherited
|
|
13
|
+
by ``firewall.Firewall``), and zone-level masquerade (a per-zone flag
|
|
14
|
+
on ``firewall_zones``).
|
|
15
|
+
|
|
16
|
+
NAT and packet-filter rules are kept in separate templates because a
|
|
17
|
+
device may legitimately compose one without the other (e.g. a transit
|
|
18
|
+
router does NAT only; a host firewall does packet-filter only).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Protocol, runtime_checkable
|
|
24
|
+
|
|
25
|
+
from testprotocols.models.firewall import NatRule
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class Nat(Protocol):
|
|
30
|
+
"""Abstract contract for low-level NAT primitives."""
|
|
31
|
+
|
|
32
|
+
# --- Rule lifecycle ---
|
|
33
|
+
|
|
34
|
+
def add_nat_rule(self, rule: NatRule) -> None:
|
|
35
|
+
"""Install a NAT rule.
|
|
36
|
+
|
|
37
|
+
Validates that *rule.mode* is one of ``"snat"``, ``"dnat"``,
|
|
38
|
+
``"1to1"`` and that the per-mode field invariants hold (see
|
|
39
|
+
``NatRule`` docstring). Raises ValueError on a duplicate
|
|
40
|
+
``rule.name``, on an unknown mode, or on mode/field
|
|
41
|
+
inconsistency (e.g. *translated_src* set with ``mode="dnat"``).
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def remove_nat_rule(self, name: str) -> None:
|
|
46
|
+
"""Remove the NAT rule identified by *name*.
|
|
47
|
+
|
|
48
|
+
Raises KeyError if no rule with that name exists.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def list_nat_rules(self, mode: str | None = None) -> list[NatRule]:
|
|
53
|
+
"""Return installed NAT rules, optionally filtered by *mode*.
|
|
54
|
+
|
|
55
|
+
*mode* is one of ``None`` (all), ``"snat"``, ``"dnat"``,
|
|
56
|
+
``"1to1"``. Raises ValueError if *mode* is set but not one of
|
|
57
|
+
the recognized values.
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def get_nat_rule(self, name: str) -> NatRule:
|
|
62
|
+
"""Return the NAT rule identified by *name*.
|
|
63
|
+
|
|
64
|
+
Raises KeyError if no rule with that name exists.
|
|
65
|
+
"""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def set_nat_rule_enabled(self, name: str, enabled: bool) -> None:
|
|
69
|
+
"""Enable or disable an existing NAT rule without removing it.
|
|
70
|
+
|
|
71
|
+
Raises KeyError if no rule with that name exists.
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def flush_nat_rules(self) -> None:
|
|
76
|
+
"""Remove every NAT rule on this device."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
# --- Counters ---
|
|
80
|
+
|
|
81
|
+
def get_nat_rule_counters(self, name: str) -> tuple[int, int]:
|
|
82
|
+
"""Return ``(packets, bytes)`` matched by the rule since it was added.
|
|
83
|
+
|
|
84
|
+
Raises KeyError if no rule with that name exists.
|
|
85
|
+
Drivers without per-rule counter support raise NotImplementedError.
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Traffic / NetemController template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for network emulation (netem) impairment
|
|
4
|
+
control on device interfaces.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from testprotocols.models.impairment import ImpairmentProfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class NetemController(Protocol):
|
|
16
|
+
"""Abstract contract for netem-based network impairment control."""
|
|
17
|
+
|
|
18
|
+
def set_impairment_profile(self, profile: ImpairmentProfile | dict[str, Any]) -> None:
|
|
19
|
+
"""Apply *profile* as the default impairment on all managed interfaces."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def set_interface_profile(
|
|
23
|
+
self, interface: str, profile: ImpairmentProfile | dict[str, Any]
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Apply *profile* as the impairment on a specific *interface*."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def get_interface_profile(self, interface: str) -> ImpairmentProfile:
|
|
29
|
+
"""Return the current impairment profile for *interface*."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def get_interface_profiles(self) -> dict[str, ImpairmentProfile]:
|
|
33
|
+
"""Return a mapping of interface names to their current impairment profiles."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def clear(self) -> None:
|
|
37
|
+
"""Remove all active impairments from all managed interfaces."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def inject_transient(self, event: str, duration_ms: int, **kwargs: float | int) -> None:
|
|
41
|
+
"""Inject a transient impairment *event* lasting *duration_ms* milliseconds."""
|
|
42
|
+
...
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Network endpoint template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for a *logical* network endpoint's identity
|
|
4
|
+
— the address a peer would use to reach a device's named role (the CPE's
|
|
5
|
+
WAN side, a server's data-plane interface, etc.). Distinct from
|
|
6
|
+
``IpInterface``, which is the per-physical-interface query surface:
|
|
7
|
+
``NetworkEndpoint`` is what the test wants ("give me the WAN address"),
|
|
8
|
+
``IpInterface`` is one of the things a driver might use under the hood
|
|
9
|
+
to satisfy it.
|
|
10
|
+
|
|
11
|
+
Drivers attach one ``NetworkEndpoint`` per logical role they expose, so
|
|
12
|
+
the device shape remains a clean aggregation of capability namespaces
|
|
13
|
+
rather than accreting role-specific accessor methods.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@runtime_checkable
|
|
22
|
+
class NetworkEndpoint(Protocol):
|
|
23
|
+
"""Abstract contract for a logical network endpoint's identity."""
|
|
24
|
+
|
|
25
|
+
def get_ipv4_addr(self) -> str:
|
|
26
|
+
"""Return the IPv4 address for this logical endpoint.
|
|
27
|
+
|
|
28
|
+
The resolution strategy is driver-internal: it may query a live
|
|
29
|
+
interface, read inventory metadata, lift a value from a TR-069
|
|
30
|
+
data model, etc. Callers see only the address.
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Network reachability probe template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for L3/L4 reachability checks initiated from
|
|
4
|
+
a host toward an external target. Distinct from ``HttpClient`` (which is
|
|
5
|
+
app-layer): a ``NetworkProbe`` verifies whether the SYN/ACK handshake
|
|
6
|
+
completes, not whether an HTTP responder is configured at the far end.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class NetworkProbe(Protocol):
|
|
16
|
+
"""Abstract contract for L3/L4 reachability probes."""
|
|
17
|
+
|
|
18
|
+
def tcp_can_connect(self, host: str, port: int, timeout: int = 5) -> bool:
|
|
19
|
+
"""Attempt a TCP connection to *host*:*port* and return reachability.
|
|
20
|
+
|
|
21
|
+
Returns ``True`` if the three-way handshake completes within
|
|
22
|
+
*timeout* seconds, ``False`` otherwise (connection refused, no
|
|
23
|
+
route, filtered, timed out). The probe is read-only — no data is
|
|
24
|
+
sent on the connection — so it is safe to call against arbitrary
|
|
25
|
+
targets without side effects on the destination.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Nmap / Scanner template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for network scanning operations using nmap.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class NmapScanner(Protocol):
|
|
13
|
+
"""Abstract contract for nmap network scanning operations."""
|
|
14
|
+
|
|
15
|
+
def nmap(
|
|
16
|
+
self,
|
|
17
|
+
ipaddr: str,
|
|
18
|
+
ip_type: str,
|
|
19
|
+
port: str | int | None = None,
|
|
20
|
+
protocol: str | None = None,
|
|
21
|
+
max_retries: int | None = None,
|
|
22
|
+
min_rate: int | None = None,
|
|
23
|
+
opts: str | None = None,
|
|
24
|
+
timeout: int = 30,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
"""Run an nmap scan against *ipaddr* and return the parsed results."""
|
|
27
|
+
...
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""NTP / Client template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for NTP client operations including
|
|
4
|
+
date retrieval, date setting, and time synchronisation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class NtpClient(Protocol):
|
|
14
|
+
"""Abstract contract for NTP client operations."""
|
|
15
|
+
|
|
16
|
+
def get_date(self) -> str | None:
|
|
17
|
+
"""Return the current date/time string from the device."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def set_date(self, opt: str, date_string: str) -> bool:
|
|
21
|
+
"""Set the device date/time using *opt* and *date_string*."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def execute_time_sync(self, time_server: str) -> str:
|
|
25
|
+
"""Synchronise device time against *time_server* and return status."""
|
|
26
|
+
...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""NTP-server configuration — the time-sync sibling of syslog_config.
|
|
2
|
+
|
|
3
|
+
Small and generic. Distinct from the operational ``ntp_client`` (get/set/sync
|
|
4
|
+
time): this is server-list config. A cloud-managed product that sets time
|
|
5
|
+
itself (timezone-only) raises unsupported-capability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
from testprotocols.models.switch import NtpServer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class NtpConfig(Protocol):
|
|
17
|
+
"""Abstract contract for NTP-server configuration."""
|
|
18
|
+
|
|
19
|
+
def set_ntp_servers(self, servers: list[NtpServer]) -> None:
|
|
20
|
+
"""Replace the NTP-server list."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def get_ntp_servers(self) -> list[NtpServer]:
|
|
24
|
+
"""Return the configured NTP servers."""
|
|
25
|
+
...
|
testprotocols/ospf.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""OSPF dynamic-routing configuration (whole-config replace)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from testprotocols.models.switch_routing import OspfConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class Ospf(Protocol):
|
|
12
|
+
"""Abstract contract for OSPF configuration.
|
|
13
|
+
|
|
14
|
+
A product that runs OSPF only on a gateway (not the L3 switch) raises
|
|
15
|
+
unsupported-capability.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def set_ospf_config(self, config: OspfConfig) -> None:
|
|
19
|
+
"""Replace the OSPF configuration."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def get_ospf_config(self) -> OspfConfig:
|
|
23
|
+
"""Return the OSPF configuration."""
|
|
24
|
+
...
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Firewall / PacketFilter template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for stateless and stateful packet-filtering
|
|
4
|
+
rules on a device's INPUT, OUTPUT, and FORWARD paths. Rules are addressed
|
|
5
|
+
by a stable logical *name* and expressed as transport-agnostic
|
|
6
|
+
``FirewallRule`` records — drivers translate into iptables, nftables,
|
|
7
|
+
pf, or vendor CLI as appropriate.
|
|
8
|
+
|
|
9
|
+
In scope: per-chain rule administration (add / remove / list / get /
|
|
10
|
+
flush), chain default-policy control, and per-rule packet/byte counters.
|
|
11
|
+
|
|
12
|
+
Out of scope: NAT (see ``nat``), high-level port forwarding (see the
|
|
13
|
+
``Firewall`` Protocol in ``firewall``, which extends ``PacketFilter``
|
|
14
|
+
with port-mapping methods), conntrack inspection (see ``conntrack``),
|
|
15
|
+
zone-based policy (see ``firewall_zones``), and L7 application-aware
|
|
16
|
+
classification (see ``l7_firewall`` for managed appliances).
|
|
17
|
+
|
|
18
|
+
The IPv4 / IPv6 split is not a contract dimension — each rule's address
|
|
19
|
+
family is inferred from its CIDR fields. Drivers that maintain separate
|
|
20
|
+
v4/v6 tables internally do so transparently.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Protocol, runtime_checkable
|
|
26
|
+
|
|
27
|
+
from testprotocols.models.firewall import FirewallRule
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class PacketFilter(Protocol):
|
|
32
|
+
"""Abstract contract for stateless / stateful packet filtering."""
|
|
33
|
+
|
|
34
|
+
# --- Rule lifecycle ---
|
|
35
|
+
|
|
36
|
+
def add_rule(
|
|
37
|
+
self,
|
|
38
|
+
chain: str,
|
|
39
|
+
rule: FirewallRule,
|
|
40
|
+
position: int | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Insert *rule* into *chain* at *position*.
|
|
43
|
+
|
|
44
|
+
*chain* is one of ``"INPUT"``, ``"OUTPUT"``, ``"FORWARD"``.
|
|
45
|
+
*position* is 1-based: ``1`` inserts at the top, ``None`` appends
|
|
46
|
+
at the end.
|
|
47
|
+
|
|
48
|
+
Raises ValueError if *chain* is unknown, if a rule named
|
|
49
|
+
``rule.name`` already exists in *chain*, or if *position* is
|
|
50
|
+
less than 1.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def remove_rule(self, chain: str, name: str) -> None:
|
|
55
|
+
"""Remove the rule identified by *name* from *chain*.
|
|
56
|
+
|
|
57
|
+
Raises ValueError if *chain* is unknown.
|
|
58
|
+
Raises KeyError if no rule with that name exists in *chain*.
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def list_rules(self, chain: str) -> list[FirewallRule]:
|
|
63
|
+
"""Return all rules currently installed in *chain*, in evaluation order.
|
|
64
|
+
|
|
65
|
+
Raises ValueError if *chain* is unknown.
|
|
66
|
+
"""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def get_rule(self, chain: str, name: str) -> FirewallRule:
|
|
70
|
+
"""Return the rule identified by *name* in *chain*.
|
|
71
|
+
|
|
72
|
+
Raises ValueError if *chain* is unknown.
|
|
73
|
+
Raises KeyError if no rule with that name exists in *chain*.
|
|
74
|
+
"""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def flush_chain(self, chain: str) -> None:
|
|
78
|
+
"""Remove every rule from *chain*. Default policy is unchanged.
|
|
79
|
+
|
|
80
|
+
Raises ValueError if *chain* is unknown.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
# --- Default policy ---
|
|
85
|
+
|
|
86
|
+
def set_default_policy(self, chain: str, policy: str) -> None:
|
|
87
|
+
"""Set the default action for traffic on *chain* that matches no rule.
|
|
88
|
+
|
|
89
|
+
*policy* is one of ``"accept"``, ``"drop"``, ``"reject"``.
|
|
90
|
+
Raises ValueError if *chain* is unknown or *policy* is not
|
|
91
|
+
a valid value.
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def get_default_policy(self, chain: str) -> str:
|
|
96
|
+
"""Return the current default policy of *chain*.
|
|
97
|
+
|
|
98
|
+
Raises ValueError if *chain* is unknown.
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
# --- Counters ---
|
|
103
|
+
|
|
104
|
+
def get_rule_counters(self, chain: str, name: str) -> tuple[int, int]:
|
|
105
|
+
"""Return ``(packets, bytes)`` matched by the rule since it was added.
|
|
106
|
+
|
|
107
|
+
Raises ValueError if *chain* is unknown.
|
|
108
|
+
Raises KeyError if no rule with that name exists in *chain*.
|
|
109
|
+
Drivers without per-rule counter support raise NotImplementedError.
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@runtime_checkable
|
|
115
|
+
class PacketFilterWhiteBox(PacketFilter, Protocol):
|
|
116
|
+
"""White-box extension of PacketFilter for raw kernel-level introspection.
|
|
117
|
+
|
|
118
|
+
Linux drivers that can shell into the box satisfy this extension by
|
|
119
|
+
capturing the underlying iptables / nftables ruleset. Vendor-RTOS or
|
|
120
|
+
locked-down devices typically satisfy only the base ``PacketFilter``
|
|
121
|
+
Protocol; tests requiring kernel-level rule verification should pin
|
|
122
|
+
against ``PacketFilterWhiteBox`` and accept the collection-skip on
|
|
123
|
+
drivers that don't satisfy it (per the ``@white_box`` scenario tag rule).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def get_kernel_iptables_dump(self) -> str:
|
|
127
|
+
"""Return the raw ``iptables-save`` output (legacy iptables backend).
|
|
128
|
+
|
|
129
|
+
For drivers running on a Linux kernel with the legacy iptables
|
|
130
|
+
backend. Format is the standard iptables-save serialisation. Tests
|
|
131
|
+
parse this to verify rules landed at the kernel level rather than
|
|
132
|
+
only in the driver's intermediate state.
|
|
133
|
+
"""
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
def get_nftables_ruleset(self) -> str:
|
|
137
|
+
"""Return the raw ``nft list ruleset`` output (nftables backend).
|
|
138
|
+
|
|
139
|
+
For drivers running on a Linux kernel with the nftables backend.
|
|
140
|
+
Format is the standard nftables ruleset serialisation. Returns
|
|
141
|
+
the entire ruleset across all tables and families; tests grep
|
|
142
|
+
for the rules they care about.
|
|
143
|
+
"""
|
|
144
|
+
...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Pcap / Capture template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for packet capture operations using
|
|
4
|
+
tcpdump and tshark.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class PcapCapture(Protocol):
|
|
14
|
+
"""Abstract contract for packet capture operations."""
|
|
15
|
+
|
|
16
|
+
def start_tcpdump(
|
|
17
|
+
self,
|
|
18
|
+
interface: str,
|
|
19
|
+
port: str | None,
|
|
20
|
+
output_file: str = "pkt_capture.pcap",
|
|
21
|
+
filters: dict[str, Any] | None = None,
|
|
22
|
+
additional_filters: str | None = "",
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Start a tcpdump capture on *interface* and return the process identifier."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def stop_tcpdump(self, process_id: str) -> None:
|
|
28
|
+
"""Stop the tcpdump process identified by *process_id*."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def tshark_read_pcap(
|
|
32
|
+
self,
|
|
33
|
+
fname: str,
|
|
34
|
+
additional_args: str | None = None,
|
|
35
|
+
timeout: int = 30,
|
|
36
|
+
rm_pcap: bool = False,
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Read and parse pcap file *fname* using tshark and return the output."""
|
|
39
|
+
...
|