@push.rocks/smartnftables 2.2.0 → 2.4.0

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.
@@ -94,6 +94,15 @@ pub struct Generation {
94
94
  pub conntrack_label: String,
95
95
  pub grants: Vec<Grant>,
96
96
  }
97
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
98
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
99
+ pub struct RouterPublishedPort {
100
+ pub protocol: String,
101
+ pub transit_port: u16,
102
+ pub endpoint_port: u16,
103
+ pub endpoint_address: String,
104
+ pub transit_source_address: String,
105
+ }
97
106
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
98
107
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
99
108
  pub struct RouterScope {
@@ -103,6 +112,10 @@ pub struct RouterScope {
103
112
  pub handoff: LocalLink,
104
113
  pub protection: Protection,
105
114
  pub generations: Vec<Generation>,
115
+ /// Absent and empty are the same canonical policy, so an unpublished scope
116
+ /// keeps its exact previous digest and compiled graph.
117
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
118
+ pub published_ports: Vec<RouterPublishedPort>,
106
119
  }
107
120
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
108
121
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -110,6 +123,15 @@ pub struct HostHandoff {
110
123
  pub link: LocalLink,
111
124
  pub allocations: Vec<Allocation>,
112
125
  }
126
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
127
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
128
+ pub struct PublishedPort {
129
+ pub protocol: String,
130
+ pub host_port: u16,
131
+ pub target_port: u16,
132
+ pub target_address: String,
133
+ pub host_ip: String,
134
+ }
113
135
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
114
136
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
115
137
  pub struct HostScope {
@@ -117,6 +139,10 @@ pub struct HostScope {
117
139
  pub handoffs: Vec<HostHandoff>,
118
140
  pub uplink: LocalLink,
119
141
  pub snat_address: String,
142
+ /// Absent and empty are the same canonical policy, so an unpublished scope
143
+ /// keeps its exact previous digest and compiled graph.
144
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
145
+ pub published_ports: Vec<PublishedPort>,
120
146
  }
121
147
  #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
122
148
  #[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -367,6 +393,86 @@ pub fn destination(grant: &Grant, protection: &Protection) -> Result<String> {
367
393
  }
368
394
  }
369
395
  }
396
+ /// Inbound publication is bounded by the authority this scope already proves: an
397
+ /// exact leased transit source address present on the handoff, an exact current
398
+ /// workload endpoint address behind one of this scope's veth links, and no overlap
399
+ /// with the leased outbound source ports translated to that same transit address.
400
+ fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
401
+ let mut ports = std::mem::take(&mut value.published_ports);
402
+ require(ports.len() <= 64)?;
403
+ ports.sort();
404
+ for (index, port) in ports.iter().enumerate() {
405
+ require(
406
+ protocol(&port.protocol)
407
+ && port.transit_port > 0
408
+ && port.endpoint_port > 0
409
+ && (index == 0
410
+ || ports[index - 1].protocol != port.protocol
411
+ || ports[index - 1].transit_port != port.transit_port),
412
+ )?;
413
+ // rtnetlink verifies this exact handoff address at apply, recovery and
414
+ // inspection, and only a current leased transit source address receives a
415
+ // publication; that binding is what the host publication already targets.
416
+ require(
417
+ value
418
+ .handoff
419
+ .required_ipv4_addresses
420
+ .contains(&port.transit_source_address)
421
+ && value.generations.iter().any(|generation| {
422
+ generation.transit_source_address == port.transit_source_address
423
+ }),
424
+ )?;
425
+ unicast(&port.endpoint_address)?;
426
+ // Endpoint source prefixes are canonical and pairwise disjoint, so at most
427
+ // one endpoint can hold this address. It must be an actual workload veth
428
+ // behind this scope, never a router-local address, a gateway, the transit
429
+ // address or a protected platform endpoint.
430
+ let target = format!("{}/32", port.endpoint_address);
431
+ let endpoint = value
432
+ .endpoints
433
+ .iter()
434
+ .find(|endpoint| {
435
+ endpoint
436
+ .source_prefixes
437
+ .iter()
438
+ .any(|source| covers(source, &target) == Ok(true))
439
+ })
440
+ .ok_or(Error::Invalid)?;
441
+ require(
442
+ endpoint.interface_kind == "veth"
443
+ && value
444
+ .links
445
+ .iter()
446
+ .chain(std::iter::once(&value.handoff))
447
+ .all(|link| {
448
+ !link
449
+ .required_ipv4_addresses
450
+ .contains(&port.endpoint_address)
451
+ })
452
+ && value
453
+ .protection
454
+ .platform_endpoints
455
+ .iter()
456
+ .all(|item| item.address != port.endpoint_address),
457
+ )?;
458
+ for generation in &value.generations {
459
+ if generation.transit_source_address != port.transit_source_address {
460
+ continue;
461
+ }
462
+ for range in &generation.source_port_ranges {
463
+ // Source NAT translates to this same transit address, so a published
464
+ // port inside a leased outbound range is a translation conflict.
465
+ require(
466
+ range.protocol != port.protocol
467
+ || port.transit_port < range.first
468
+ || port.transit_port > range.last,
469
+ )?;
470
+ }
471
+ }
472
+ }
473
+ value.published_ports = ports;
474
+ Ok(())
475
+ }
370
476
  fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
371
477
  require(value.links.len() <= 32 && value.generations.len() <= 32)?;
372
478
  let private = policy::Policy {
@@ -537,7 +643,8 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
537
643
  item.transit_source_address.as_str(),
538
644
  item.source_port_ranges.as_slice(),
539
645
  )
540
- }))
646
+ }))?;
647
+ normalize_router_published(value)
541
648
  }
542
649
  fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
543
650
  require(
@@ -556,6 +663,53 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
556
663
  }
557
664
  Ok(())
558
665
  }
666
+ /// Inbound publication is bounded by the same authority the scope already proves:
667
+ /// an exact current uplink address, an exact leased transit source address, and no
668
+ /// overlap with the leased outbound source ports translated to that same address.
669
+ fn normalize_published(value: &mut HostScope) -> Result<()> {
670
+ let mut ports = std::mem::take(&mut value.published_ports);
671
+ require(ports.len() <= 64)?;
672
+ ports.sort();
673
+ for (index, port) in ports.iter().enumerate() {
674
+ require(
675
+ protocol(&port.protocol)
676
+ && port.host_port > 0
677
+ && port.target_port > 0
678
+ && (index == 0
679
+ || ports[index - 1].protocol != port.protocol
680
+ || ports[index - 1].host_port != port.host_port),
681
+ )?;
682
+ // rtnetlink verifies this exact uplink address at apply, recovery and
683
+ // inspection. No wildcard, secondary-link or default-route inference.
684
+ require(value.uplink.required_ipv4_addresses.contains(&port.host_ip))?;
685
+ // Only a current leased transit source address can be published; that
686
+ // binding is what proves the exact handoff link of the forward admission.
687
+ require(
688
+ value
689
+ .handoffs
690
+ .iter()
691
+ .flat_map(|handoff| handoff.allocations.iter())
692
+ .any(|allocation| allocation.transit_source_address == port.target_address),
693
+ )?;
694
+ if port.host_ip == value.snat_address {
695
+ for handoff in &value.handoffs {
696
+ for allocation in &handoff.allocations {
697
+ for range in &allocation.source_port_ranges {
698
+ // Outer SNAT translates to this same address, so a published
699
+ // port inside a leased outbound range is a translation conflict.
700
+ require(
701
+ range.protocol != port.protocol
702
+ || port.host_port < range.first
703
+ || port.host_port > range.last,
704
+ )?;
705
+ }
706
+ }
707
+ }
708
+ }
709
+ }
710
+ value.published_ports = ports;
711
+ Ok(())
712
+ }
559
713
  fn normalize_host(value: &mut HostScope) -> Result<()> {
560
714
  normalize_protection(&mut value.protection)?;
561
715
  normalize_link(&mut value.uplink)?;
@@ -622,6 +776,7 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
622
776
  }
623
777
  }
624
778
  }
779
+ normalize_published(value)?;
625
780
  unique_links(
626
781
  value
627
782
  .handoffs