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.
Files changed (119) hide show
  1. testprotocols/__init__.py +217 -0
  2. testprotocols/aftr_gateway.py +22 -0
  3. testprotocols/appliance_nat.py +52 -0
  4. testprotocols/appliance_uplinks.py +32 -0
  5. testprotocols/appliance_vlans.py +50 -0
  6. testprotocols/arp_client.py +26 -0
  7. testprotocols/bgp.py +55 -0
  8. testprotocols/conntrack.py +147 -0
  9. testprotocols/content_filtering.py +47 -0
  10. testprotocols/device_lifecycle.py +49 -0
  11. testprotocols/device_management.py +50 -0
  12. testprotocols/devices/__init__.py +46 -0
  13. testprotocols/devices/base.py +40 -0
  14. testprotocols/devices/client.py +133 -0
  15. testprotocols/devices/cpe.py +66 -0
  16. testprotocols/devices/infra.py +62 -0
  17. testprotocols/devices/sdwan.py +97 -0
  18. testprotocols/devices/switch.py +115 -0
  19. testprotocols/devices/traffic.py +53 -0
  20. testprotocols/devices/voice.py +69 -0
  21. testprotocols/devices/wan.py +60 -0
  22. testprotocols/dhcp_client.py +30 -0
  23. testprotocols/dhcp_server.py +23 -0
  24. testprotocols/discovery.py +20 -0
  25. testprotocols/dns_client.py +23 -0
  26. testprotocols/file_transfer.py +22 -0
  27. testprotocols/firewall.py +121 -0
  28. testprotocols/firewall_zones.py +133 -0
  29. testprotocols/first_hop_security.py +52 -0
  30. testprotocols/gateway_redundancy.py +29 -0
  31. testprotocols/http_client.py +36 -0
  32. testprotocols/http_server.py +22 -0
  33. testprotocols/hw_console.py +48 -0
  34. testprotocols/infra_controller.py +28 -0
  35. testprotocols/interface_dhcp.py +30 -0
  36. testprotocols/ip_interface.py +62 -0
  37. testprotocols/ip_routing.py +57 -0
  38. testprotocols/iperf_client.py +47 -0
  39. testprotocols/iperf_generator.py +42 -0
  40. testprotocols/iperf_server.py +41 -0
  41. testprotocols/l3_firewall.py +74 -0
  42. testprotocols/l7_firewall.py +32 -0
  43. testprotocols/link_aggregation.py +24 -0
  44. testprotocols/mac_table.py +20 -0
  45. testprotocols/models/__init__.py +304 -0
  46. testprotocols/models/dhcp.py +28 -0
  47. testprotocols/models/firewall.py +197 -0
  48. testprotocols/models/impairment.py +18 -0
  49. testprotocols/models/l2_common.py +53 -0
  50. testprotocols/models/multicast.py +22 -0
  51. testprotocols/models/networking.py +50 -0
  52. testprotocols/models/packets.py +21 -0
  53. testprotocols/models/qoe.py +31 -0
  54. testprotocols/models/radius.py +63 -0
  55. testprotocols/models/sdwan_appliance.py +637 -0
  56. testprotocols/models/switch.py +297 -0
  57. testprotocols/models/switch_routing.py +122 -0
  58. testprotocols/models/tr069.py +35 -0
  59. testprotocols/models/traffic.py +29 -0
  60. testprotocols/models/wan_edge.py +116 -0
  61. testprotocols/models/wifi.py +183 -0
  62. testprotocols/multicast_client.py +20 -0
  63. testprotocols/nat.py +87 -0
  64. testprotocols/netem_controller.py +42 -0
  65. testprotocols/network_endpoint.py +32 -0
  66. testprotocols/network_probe.py +27 -0
  67. testprotocols/nmap_scanner.py +27 -0
  68. testprotocols/ntp_client.py +26 -0
  69. testprotocols/ntp_config.py +25 -0
  70. testprotocols/ospf.py +24 -0
  71. testprotocols/packet_filter.py +144 -0
  72. testprotocols/pcap_capture.py +39 -0
  73. testprotocols/pdu_controller.py +26 -0
  74. testprotocols/port_poe.py +25 -0
  75. testprotocols/port_security.py +25 -0
  76. testprotocols/port_status.py +23 -0
  77. testprotocols/py.typed +0 -0
  78. testprotocols/qoe_browser.py +62 -0
  79. testprotocols/radius_client.py +78 -0
  80. testprotocols/radius_server.py +130 -0
  81. testprotocols/routed_interfaces.py +29 -0
  82. testprotocols/router.py +53 -0
  83. testprotocols/routing_read.py +22 -0
  84. testprotocols/sdwan_policy_manager.py +64 -0
  85. testprotocols/sip_phone.py +230 -0
  86. testprotocols/sip_server.py +205 -0
  87. testprotocols/site_to_site_vpn.py +61 -0
  88. testprotocols/snmp_client.py +17 -0
  89. testprotocols/spanning_tree.py +37 -0
  90. testprotocols/static_routes.py +47 -0
  91. testprotocols/storm_control.py +24 -0
  92. testprotocols/streaming_server.py +32 -0
  93. testprotocols/switch_acl.py +29 -0
  94. testprotocols/switch_ports.py +28 -0
  95. testprotocols/switch_qos.py +33 -0
  96. testprotocols/switch_vlans.py +34 -0
  97. testprotocols/syslog_config.py +31 -0
  98. testprotocols/tftp_server.py +22 -0
  99. testprotocols/threat_prevention.py +60 -0
  100. testprotocols/tr069_client.py +47 -0
  101. testprotocols/tr069_server.py +151 -0
  102. testprotocols/traffic_shaping.py +54 -0
  103. testprotocols/upnp_client.py +37 -0
  104. testprotocols/vlan_client.py +22 -0
  105. testprotocols/wan_link_admin.py +34 -0
  106. testprotocols/wifi_bss.py +197 -0
  107. testprotocols/wifi_client.py +72 -0
  108. testprotocols/wifi_mesh.py +259 -0
  109. testprotocols/wifi_onboarding.py +105 -0
  110. testprotocols/wifi_radio.py +153 -0
  111. testprotocols/wifi_rf.py +78 -0
  112. testprotocols/wifi_stations.py +59 -0
  113. testprotocols/wifi_transitions.py +112 -0
  114. testprotocols-0.1.0.dist-info/METADATA +29 -0
  115. testprotocols-0.1.0.dist-info/RECORD +119 -0
  116. testprotocols-0.1.0.dist-info/WHEEL +5 -0
  117. testprotocols-0.1.0.dist-info/licenses/LICENSE +201 -0
  118. testprotocols-0.1.0.dist-info/licenses/NOTICE +11 -0
  119. testprotocols-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,637 @@
1
+ """Data models for the vendor-neutral SD-WAN **appliance** capabilities.
2
+
3
+ These back the managed-appliance capability protocols composed by
4
+ ``devices.sdwan.SdwanApplianceDevice`` (distinct from the Linux digital twin's
5
+ ``SdwanRouterDevice``).
6
+
7
+ **Vendor neutrality is part of the contract.** Field value-vocabularies are
8
+ *normalized* and owned here as ``StrEnum`` types: members are plain strings (so
9
+ serialization to a vendor's REST/JSON API is trivial), constructing from a value
10
+ validates it (``RuleAction("x")`` raises ``ValueError``), and the types give
11
+ static checking at every driver/test call site. A testbed plugin maps its
12
+ product's representation to/from these neutral values; no vendor identifier, raw
13
+ payload, or vendor-specific vocabulary ever appears in this module.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from enum import StrEnum
20
+
21
+
22
+ class RuleAction(StrEnum):
23
+ """Action a firewall rule takes on a match."""
24
+
25
+ ALLOW = "allow"
26
+ DENY = "deny"
27
+
28
+
29
+ class RuleProtocol(StrEnum):
30
+ """Transport a rule matches. ``ANY`` leaves the protocol unconstrained."""
31
+
32
+ TCP = "tcp"
33
+ UDP = "udp"
34
+ ICMP = "icmp"
35
+ ICMP6 = "icmp6"
36
+ ANY = "any"
37
+
38
+
39
+ @dataclass
40
+ class L3Rule:
41
+ """A single ordered L3 firewall rule — 5-tuple match plus an action.
42
+
43
+ A managed appliance evaluates its L3 policy as a flat, ordered list of these
44
+ (not as netfilter INPUT/OUTPUT/FORWARD chains). The CIDR and port fields take
45
+ ``"any"`` when unconstrained; ports may be a single port, a range
46
+ (``"8000-8100"``), or a comma list — always a string so the contract stays
47
+ transport- and vendor-agnostic.
48
+
49
+ ``syslog_enabled`` is per-rule intent. Products whose firewall logging is
50
+ only list- or segment-scoped approximate it in the driver (enable scoped
51
+ logging when any rule requests it) — an accepted, documented approximation,
52
+ not a contract violation.
53
+ """
54
+
55
+ action: RuleAction
56
+ protocol: RuleProtocol = RuleProtocol.ANY
57
+ src_cidr: str = "any"
58
+ src_port: str = "any"
59
+ dst_cidr: str = "any"
60
+ dst_port: str = "any"
61
+ comment: str = ""
62
+ syslog_enabled: bool = False
63
+
64
+
65
+ class L7MatchType(StrEnum):
66
+ """How an L7 (application-aware) firewall rule selects traffic."""
67
+
68
+ APPLICATION = "application"
69
+ APPLICATION_CATEGORY = "application_category"
70
+ HOST = "host"
71
+ PORT = "port"
72
+ IP_RANGE = "ip_range"
73
+
74
+
75
+ @dataclass
76
+ class L7Rule:
77
+ """A single application-aware (L7) firewall rule.
78
+
79
+ ``match_type`` selects the dimension; ``value`` carries the matched item.
80
+ For ``HOST`` / ``PORT`` / ``IP_RANGE`` the value is a free string. For
81
+ ``APPLICATION_CATEGORY`` the value is an ``ApplicationCategory`` member. For
82
+ ``APPLICATION`` (an individual app) the value is a vendor-mapped string — a
83
+ normalized ``Application`` registry is not seeded yet (grow on evidence). The
84
+ driver maps the value to its product's identifier in all cases.
85
+ """
86
+
87
+ action: RuleAction
88
+ match_type: L7MatchType
89
+ value: str
90
+ comment: str = ""
91
+
92
+
93
+ class ContentCategory(StrEnum):
94
+ """Normalized URL / content-filtering categories owned by commons.
95
+
96
+ A balanced **standard** set drawn from the common-denominator categories
97
+ across managed SD-WAN appliances' URL-filter taxonomies — broad enough to
98
+ cover the likely need without mirroring any one vendor's full list. The
99
+ plugin maps each to its product's category id; add members on evidence.
100
+ """
101
+
102
+ ADULT = "adult"
103
+ ADVERTISING = "advertising"
104
+ ALCOHOL_AND_TOBACCO = "alcohol_and_tobacco"
105
+ BUSINESS = "business"
106
+ DATING = "dating"
107
+ DRUGS = "drugs"
108
+ EDUCATION = "education"
109
+ FILE_SHARING = "file_sharing"
110
+ FINANCE = "finance"
111
+ GAMBLING = "gambling"
112
+ GAMES = "games"
113
+ GOVERNMENT = "government"
114
+ HACKING = "hacking"
115
+ HEALTH = "health"
116
+ ILLEGAL_CONTENT = "illegal_content"
117
+ JOB_SEARCH = "job_search"
118
+ MALWARE_SITES = "malware_sites"
119
+ NEWS = "news"
120
+ PEER_TO_PEER = "peer_to_peer"
121
+ PHISHING = "phishing"
122
+ RELIGION = "religion"
123
+ SEARCH_ENGINES = "search_engines"
124
+ SHOPPING = "shopping"
125
+ SOCIAL_NETWORKING = "social_networking"
126
+ SPORTS = "sports"
127
+ STREAMING_MEDIA = "streaming_media"
128
+ TRAVEL = "travel"
129
+ VIOLENCE = "violence"
130
+ WEAPONS = "weapons"
131
+ WEB_BASED_EMAIL = "web_based_email"
132
+
133
+
134
+ class ApplicationCategory(StrEnum):
135
+ """Normalized application categories for L7 (application-aware) policy.
136
+
137
+ A balanced **standard** set drawn from the common-denominator app-control
138
+ categories across managed SD-WAN appliances — the dimensions a test is
139
+ likely to steer or block on. The plugin maps each to its product's
140
+ application-category id; add members on evidence. (Individual application
141
+ identifiers — a far larger, more divergent catalog — are deliberately not
142
+ seeded here; add an ``Application`` registry if/when a test needs one.)
143
+ """
144
+
145
+ ADVERTISING = "advertising"
146
+ BUSINESS_AND_PRODUCTIVITY = "business_and_productivity"
147
+ CLOUD_SERVICES = "cloud_services"
148
+ COLLABORATION = "collaboration"
149
+ DATABASE = "database"
150
+ EMAIL = "email"
151
+ FILE_SHARING = "file_sharing"
152
+ GAMING = "gaming"
153
+ INSTANT_MESSAGING = "instant_messaging"
154
+ MUSIC_STREAMING = "music_streaming"
155
+ NETWORK_SERVICES = "network_services"
156
+ NEWS = "news"
157
+ PEER_TO_PEER = "peer_to_peer"
158
+ REMOTE_ACCESS = "remote_access"
159
+ SOCIAL_NETWORKING = "social_networking"
160
+ SOFTWARE_UPDATES = "software_updates"
161
+ SPORTS = "sports"
162
+ VIDEO_STREAMING = "video_streaming"
163
+ VOIP_AND_VIDEO_CONFERENCING = "voip_and_video_conferencing"
164
+ VPN_AND_PROXY = "vpn_and_proxy"
165
+ WEB_FILE_TRANSFER = "web_file_transfer"
166
+
167
+
168
+ # --- Traffic shaping ---
169
+
170
+
171
+ class ShapingPriority(StrEnum):
172
+ """Relative scheduling priority a shaping rule assigns to matched traffic."""
173
+
174
+ LOW = "low"
175
+ NORMAL = "normal"
176
+ HIGH = "high"
177
+
178
+
179
+ @dataclass
180
+ class ShapingRule:
181
+ """A traffic-shaping rule — match a traffic class, then limit / mark / prioritize.
182
+
183
+ ``match_type`` / ``value`` reuse the L7 match vocabulary (by application,
184
+ application category, host, port, or IP range). ``bandwidth_limit_kbps``
185
+ caps the class (``None`` = uncapped); ``dscp_tag`` applies a DSCP marking
186
+ (``None`` = leave unmarked); ``priority`` sets relative scheduling.
187
+ """
188
+
189
+ name: str
190
+ match_type: L7MatchType
191
+ value: str
192
+ bandwidth_limit_kbps: int | None = None
193
+ dscp_tag: int | None = None
194
+ priority: ShapingPriority = ShapingPriority.NORMAL
195
+
196
+
197
+ # --- NAT (1:1 / 1:Many / port-forwarding) ---
198
+
199
+
200
+ @dataclass
201
+ class NatInboundAllow:
202
+ """An inbound allowance attached to a 1:1 NAT mapping."""
203
+
204
+ protocol: RuleProtocol = RuleProtocol.ANY
205
+ ports: str = "any"
206
+ allowed_remote_cidrs: list[str] = field(default_factory=lambda: ["any"])
207
+
208
+
209
+ @dataclass
210
+ class PortForwardRule:
211
+ """A port-forwarding (DNAT) rule — public port → internal host:port."""
212
+
213
+ name: str
214
+ protocol: RuleProtocol
215
+ public_port: str
216
+ lan_ip: str
217
+ local_port: str
218
+ uplink: str = "any"
219
+ allowed_remote_cidrs: list[str] = field(default_factory=lambda: ["any"])
220
+
221
+
222
+ @dataclass
223
+ class OneToOneNatRule:
224
+ """A 1:1 NAT mapping between a public IP and an internal IP."""
225
+
226
+ name: str
227
+ public_ip: str
228
+ lan_ip: str
229
+ uplink: str = "any"
230
+ allowed_inbound: list[NatInboundAllow] = field(default_factory=list)
231
+
232
+
233
+ @dataclass
234
+ class OneToManyNatRule:
235
+ """A 1:many (PAT) mapping — one public IP, many port-based forwards."""
236
+
237
+ public_ip: str
238
+ uplink: str = "any"
239
+ port_forwards: list[PortForwardRule] = field(default_factory=list)
240
+
241
+
242
+ # --- WAN uplinks ---
243
+
244
+
245
+ class UplinkState(StrEnum):
246
+ """Operational state of a WAN uplink.
247
+
248
+ ``DEGRADED`` covers vendor states reporting a link that is forwarding but
249
+ impaired (unstable / lossy / connecting) — normalized here so drivers do
250
+ not collapse such states into ``UP``.
251
+ """
252
+
253
+ UP = "up"
254
+ DEGRADED = "degraded"
255
+ DOWN = "down"
256
+ STANDBY = "standby"
257
+ NOT_CONNECTED = "not_connected"
258
+
259
+
260
+ @dataclass
261
+ class UplinkStatus:
262
+ """Current status of a single WAN uplink (read-only observation)."""
263
+
264
+ name: str
265
+ state: UplinkState
266
+ ip: str = ""
267
+ gateway: str = ""
268
+ public_ip: str = ""
269
+ primary_dns: str = ""
270
+
271
+
272
+ # --- Syslog destinations ---
273
+
274
+
275
+ class SyslogRole(StrEnum):
276
+ """Category of log a syslog destination receives."""
277
+
278
+ EVENT_LOG = "event_log"
279
+ FLOWS = "flows"
280
+ SECURITY = "security"
281
+ URLS = "urls"
282
+
283
+
284
+ @dataclass
285
+ class SyslogServer:
286
+ """A syslog destination and the log roles it receives."""
287
+
288
+ host: str
289
+ port: int = 514
290
+ roles: list[SyslogRole] = field(default_factory=list)
291
+
292
+
293
+ # --- Threat prevention (IDS / IPS + malware) ---
294
+
295
+
296
+ class IntrusionMode(StrEnum):
297
+ """IDS/IPS operating mode."""
298
+
299
+ DISABLED = "disabled"
300
+ DETECTION = "detection"
301
+ PREVENTION = "prevention"
302
+
303
+
304
+ class IntrusionSensitivity(StrEnum):
305
+ """Normalized IPS ruleset sensitivity (vendor ruleset names map onto this)."""
306
+
307
+ LOW = "low"
308
+ MEDIUM = "medium"
309
+ HIGH = "high"
310
+
311
+
312
+ class MalwareMode(StrEnum):
313
+ """Anti-malware operating mode."""
314
+
315
+ DISABLED = "disabled"
316
+ ENABLED = "enabled"
317
+
318
+
319
+ class SecurityAction(StrEnum):
320
+ """What the appliance did about a security event."""
321
+
322
+ ALLOWED = "allowed"
323
+ BLOCKED = "blocked"
324
+ DETECTED = "detected"
325
+
326
+
327
+ class ThreatCategory(StrEnum):
328
+ """Normalized class of a security event."""
329
+
330
+ MALWARE = "malware"
331
+ INTRUSION = "intrusion"
332
+ EXPLOIT = "exploit"
333
+ SCAN = "scan"
334
+ BOTNET = "botnet"
335
+ PHISHING = "phishing"
336
+ POLICY_VIOLATION = "policy_violation"
337
+
338
+
339
+ @dataclass
340
+ class IntrusionConfig:
341
+ """IDS/IPS configuration state."""
342
+
343
+ mode: IntrusionMode
344
+ sensitivity: IntrusionSensitivity | None = None
345
+
346
+
347
+ @dataclass
348
+ class MalwareConfig:
349
+ """Anti-malware configuration state."""
350
+
351
+ mode: MalwareMode
352
+
353
+
354
+ @dataclass
355
+ class SecurityEvent:
356
+ """A normalized security event (the deferred-API-augmentation surface).
357
+
358
+ Carries only normalized fields for portable assertions — vendor signature
359
+ ids and raw payloads are deliberately not modelled. ``ts`` is an ISO-8601
360
+ UTC timestamp string.
361
+ """
362
+
363
+ ts: str
364
+ src_ip: str
365
+ dst_ip: str
366
+ protocol: RuleProtocol
367
+ action: SecurityAction
368
+ category: ThreatCategory
369
+ description: str = ""
370
+
371
+
372
+ # --- LAN VLANs + DHCP ---
373
+
374
+
375
+ class DhcpMode(StrEnum):
376
+ """How the appliance handles DHCP on a VLAN."""
377
+
378
+ SERVER = "server"
379
+ RELAY = "relay"
380
+ DISABLED = "disabled"
381
+
382
+
383
+ class DhcpOptionType(StrEnum):
384
+ """Value type of a DHCP option."""
385
+
386
+ TEXT = "text"
387
+ IP = "ip"
388
+ INTEGER = "integer"
389
+ HEX = "hex"
390
+
391
+
392
+ @dataclass
393
+ class DhcpOption:
394
+ """A custom DHCP option served on a VLAN."""
395
+
396
+ code: int
397
+ type: DhcpOptionType
398
+ value: str
399
+
400
+
401
+ @dataclass
402
+ class DhcpReservation:
403
+ """A fixed IP assignment for a known MAC."""
404
+
405
+ mac: str
406
+ ip: str
407
+ name: str = ""
408
+
409
+
410
+ @dataclass
411
+ class VlanConfig:
412
+ """A LAN VLAN and its DHCP configuration.
413
+
414
+ ``dhcp_lease_seconds`` normalizes lease time to seconds (vendors express it
415
+ variously). ``dns_servers`` empty means "use the appliance / upstream
416
+ default". Reserved ranges are ``(start_ip, end_ip)`` pairs excluded from
417
+ the dynamic pool.
418
+ """
419
+
420
+ vlan_id: int
421
+ name: str
422
+ subnet: str
423
+ appliance_ip: str
424
+ dhcp_mode: DhcpMode = DhcpMode.SERVER
425
+ dhcp_lease_seconds: int = 86400
426
+ dns_servers: list[str] = field(default_factory=list)
427
+ dhcp_options: list[DhcpOption] = field(default_factory=list)
428
+ reservations: list[DhcpReservation] = field(default_factory=list)
429
+ reserved_ranges: list[tuple[str, str]] = field(default_factory=list)
430
+
431
+
432
+ @dataclass
433
+ class DhcpLease:
434
+ """An observed DHCP lease (read-only)."""
435
+
436
+ mac: str
437
+ ip: str
438
+ hostname: str = ""
439
+ vlan_id: int = 0
440
+
441
+
442
+ # --- Site-to-site VPN overlay ---
443
+
444
+
445
+ class VpnRole(StrEnum):
446
+ """Role a device plays in the site-to-site VPN overlay."""
447
+
448
+ DISABLED = "disabled"
449
+ HUB = "hub"
450
+ SPOKE = "spoke"
451
+
452
+
453
+ class VpnPeerState(StrEnum):
454
+ """Reachability of a site-to-site VPN peer."""
455
+
456
+ REACHABLE = "reachable"
457
+ UNREACHABLE = "unreachable"
458
+ UNKNOWN = "unknown"
459
+
460
+
461
+ @dataclass
462
+ class VpnHub:
463
+ """A hub a spoke connects to.
464
+
465
+ ``name`` is the testbed-level hub identifier; the plugin maps it to the
466
+ vendor's id. ``use_default_route`` points the spoke's default route into
467
+ the overlay via this hub.
468
+ """
469
+
470
+ name: str
471
+ use_default_route: bool = False
472
+
473
+
474
+ @dataclass
475
+ class VpnSubnet:
476
+ """A local subnet and whether it participates in the overlay."""
477
+
478
+ subnet: str
479
+ advertise: bool = True
480
+
481
+
482
+ @dataclass
483
+ class SiteToSiteVpnConfig:
484
+ """Complete overlay-participation config — read and replaced whole.
485
+
486
+ ``hubs`` is only meaningful for ``VpnRole.SPOKE`` and is ordered by
487
+ priority. ``subnets`` lists the local subnets and whether each is
488
+ advertised into the overlay.
489
+ """
490
+
491
+ role: VpnRole
492
+ hubs: list[VpnHub] = field(default_factory=list)
493
+ subnets: list[VpnSubnet] = field(default_factory=list)
494
+
495
+
496
+ @dataclass
497
+ class VpnPeerStatus:
498
+ """Observed status of one site-to-site VPN peer (read-only).
499
+
500
+ ``name`` is the peer's testbed-level site name (normalized; the plugin
501
+ maps the vendor's peer identifier). ``uplink`` names the local uplink
502
+ carrying the tunnel when the product reports it, else ``""``.
503
+ """
504
+
505
+ name: str
506
+ state: VpnPeerState
507
+ uplink: str = ""
508
+
509
+
510
+ # --- Path steering (uplink selection) ---
511
+
512
+
513
+ class SteeringScope(StrEnum):
514
+ """Traffic domain an uplink-selection rule steers.
515
+
516
+ Deliberately no ``ANY`` member — the test author states the intent, and
517
+ products with split steering surfaces need it to route the write.
518
+ """
519
+
520
+ INTERNET = "internet"
521
+ OVERLAY = "overlay"
522
+
523
+
524
+ @dataclass
525
+ class FlowMatch:
526
+ """5-tuple traffic match for steering rules (match only — no action).
527
+
528
+ Field semantics mirror ``L3Rule``'s match half: ``"any"`` when
529
+ unconstrained; ports may be a single port, a range (``"8000-8100"``),
530
+ or a comma list.
531
+ """
532
+
533
+ protocol: RuleProtocol = RuleProtocol.ANY
534
+ src_cidr: str = "any"
535
+ src_port: str = "any"
536
+ dst_cidr: str = "any"
537
+ dst_port: str = "any"
538
+
539
+
540
+ @dataclass
541
+ class UplinkSelectionRule:
542
+ """One ordered uplink-steering rule.
543
+
544
+ With ``performance_class`` set (the *name* of an ``SLAPolicy`` configured
545
+ via ``configure_sla_policy``), traffic matching ``match`` is steered to
546
+ ``preferred_uplink`` while the class is met and fails over when it is
547
+ breached. With ``performance_class=None`` the preference is static —
548
+ failover occurs only on uplink loss.
549
+ """
550
+
551
+ name: str
552
+ scope: SteeringScope
553
+ match: FlowMatch
554
+ preferred_uplink: str
555
+ performance_class: str | None = None
556
+
557
+
558
+ # --- Static routes ---
559
+
560
+
561
+ @dataclass
562
+ class StaticRoute:
563
+ """A testbed-owned static route.
564
+
565
+ ``name`` is the per-entry CRUD handle (``remove_static_route(name)``).
566
+ Products whose API keys routes by sequence number or opaque id carry the
567
+ name in their description/comment field or a driver-side mapping — a
568
+ driver concern, not a contract one. ``next_hop`` is a next-hop IP
569
+ address; interface-bound next hops, metrics/administrative distance, and
570
+ per-route advertise flags grow on evidence.
571
+ """
572
+
573
+ name: str
574
+ destination_cidr: str
575
+ next_hop: str
576
+
577
+
578
+ # --- BGP ---
579
+
580
+
581
+ class BgpSessionState(StrEnum):
582
+ """BGP FSM state of a neighbor session.
583
+
584
+ The RFC 4271 state vocabulary — protocol-standard, not vendor-specific,
585
+ so the full set is seeded (the grow-on-evidence rule applies to vendor
586
+ taxonomies, not to standardized protocol states). ``UNKNOWN`` absorbs
587
+ vendor representations that do not map to an FSM state.
588
+ """
589
+
590
+ IDLE = "idle"
591
+ CONNECT = "connect"
592
+ ACTIVE = "active"
593
+ OPEN_SENT = "open_sent"
594
+ OPEN_CONFIRM = "open_confirm"
595
+ ESTABLISHED = "established"
596
+ UNKNOWN = "unknown"
597
+
598
+
599
+ @dataclass
600
+ class BgpNeighbor:
601
+ """A configured BGP neighbor (minimal — timers/auth/multihop grow on
602
+ evidence)."""
603
+
604
+ peer_ip: str
605
+ remote_as: int
606
+
607
+
608
+ @dataclass
609
+ class BgpConfig:
610
+ """Complete BGP configuration — read and replaced whole.
611
+
612
+ ``enabled`` / ``as_number`` / ``neighbors`` are semantically coupled,
613
+ and most reviewed management planes expose BGP as one object, so the
614
+ surface is whole-config replace (idempotent), not per-neighbor CRUD.
615
+ ``advertised_networks`` lists CIDRs announced to peers; products that
616
+ auto-advertise their overlay subnets and offer no per-network control
617
+ raise unsupported-capability when it is non-empty.
618
+ """
619
+
620
+ enabled: bool
621
+ as_number: int
622
+ neighbors: list[BgpNeighbor] = field(default_factory=list)
623
+ advertised_networks: list[str] = field(default_factory=list)
624
+
625
+
626
+ @dataclass
627
+ class BgpPeerStatus:
628
+ """Observed status of one BGP neighbor session (read-only).
629
+
630
+ ``prefixes_received`` is ``None`` when the product does not report a
631
+ count.
632
+ """
633
+
634
+ peer_ip: str
635
+ remote_as: int
636
+ state: BgpSessionState
637
+ prefixes_received: int | None = None