Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions _Log.md
Original file line number Diff line number Diff line change
Expand Up @@ -742,3 +742,6 @@
- **Timestamp**: 2026-05-17T09:34:59-07:00
- **Action**: #1373 Phase 1 documentation migration: mark Rust AF_XDP userspace as the primary/default dataplane development and validation target, demote eBPF wording to legacy compatibility/regression context, and preserve explicit retirement blockers for #1374-#1381 without claiming unresolved gaps closed.
- **File(s)**: `README.md`, `CLAUDE.md`, `docs/testing.md`, `docs/development-workflow.md`, `docs/test_env.md`, `docs/userspace-dataplane-gaps.md`, `docs/feature-gaps.md`, `docs/userspace-dataplane-architecture.md`, `docs/afxdp-packet-processing.md`, `docs/ha-cluster-test-plan.md`, `testing-docs/README.md`, `bpf/README.md`, `userspace-dp/README.md`, `pkg/dataplane/README.md`, `userspace-dp/src/afxdp/README.md`, `_Log.md`
- **Timestamp**: 2026-05-17T18:57:46Z
- **Action**: Adversarial PR #1374 follow-up: optimize SYN-cookie validated-client cache with map+queue state so insert/take are O(1), while expiry/eviction drain stale queue tokens from the head without whole-cache scans.
- **File(s)**: `userspace-dp/src/screen.rs`, `userspace-dp/src/screen_tests.rs`, `_Log.md`
37 changes: 35 additions & 2 deletions docs/pr/1373-retire-ebpf-dataplane/plan-1374-syn-cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ SYN cookie behavior in `userspace-dp`.
profile the actual TX completion cost instead of assuming in-place RX-to-TX
bounce is required.

## Current Slice Status (2026-05-17)

- #1393 landed the deterministic userspace cookie codec/layout and codec tests.
- This runtime slice carries `syn_cookie` through Go and Rust screen snapshots,
adds a fail-closed screen challenge verdict when no HA-safe secret is
published, uses a fixed-size keyed validated-client table for
attacker-controlled tuples, and validates returning ACKs only after normal
session lookup misses.
- The AF_XDP hook currently consumes valid cookie ACKs and drops invalid cookie
ACKs while cookie mode is active. `SynCookieChallenge` is still accounted as a
screen drop instead of transmitting a SYN-ACK.
- This PR is therefore a SYN-cookie validation/admission runtime slice, not the
full SYN-ACK/RST TX implementation.
- The userspace capability gate remains in place until bounded SYN-ACK TX, ACK
RST emission, HA-safe secret publication, counters/status, and integration
validation land.

## Design

Use SipHash, not HMAC-SHA1/SHA256. Linux SYN cookies and the current kernel
Expand Down Expand Up @@ -63,10 +80,13 @@ On returning ACK:

- No heap allocation while deciding SYN cookie mint or ACK validation.
- SipHash key lookup is per-zone and read-only on the published snapshot.
- Validated-client state is a fixed-size keyed table; attacker-controlled
tuples do not enter `FxHashMap` or an unbounded queue.
- Per-zone SYN-cookie active/counter state is config-bound and prepopulated on
profile updates, so packet processing does not allocate zone strings.
- Cookie reply frame allocation is bounded; normal forwarding frame ownership
takes priority over diagnostic/flood replies.
- Random ACKs never install sessions.
- Validated-client state uses a bounded LRU or equivalent capped table.

## State and HA Behavior

Expand Down Expand Up @@ -105,7 +125,20 @@ On returning ACK:
- Cargo: `screen::syn_cookie_epoch_low_bits_wrap_rejects_32_epoch_old_cookie`.
- Cargo: `screen::syn_cookie_validation_tries_current_and_previous_full_epoch`.
- Cargo: `screen::syn_cookie_chosen_when_threshold_exceeded`.
- Cargo: `screen::syn_cookie_budget_drop_does_not_starve_tx`.
- Cargo: `screen::syn_cookie_without_published_secret_fails_closed`.
- Cargo: `screen::syn_cookie_ack_validation_marks_next_syn_bypass_without_session_creation`.
- Cargo: `screen::syn_cookie_validated_syn_still_runs_later_screen_checks`.
- Cargo: `screen::syn_cookie_invalid_ack_does_not_validate_client`.
- Cargo: `screen::syn_cookie_ack_fin_is_invalid_while_cookie_mode_is_active`.
- Cargo: `screen::syn_cookie_validated_cache_is_bounded`.
- Cargo: `screen::syn_cookie_validated_cache_index_is_keyed`.
- Cargo: `screen::syn_cookie_invalid_ack_flood_does_not_grow_validated_cache`.
- Cargo: `screen::syn_cookie_master_key_rotation_clears_validated_cache`.
- Cargo: `screen::update_profiles_prepopulates_syn_cookie_active_state`.
- Cargo: `screen::syn_cookie_validated_cache_refresh_extends_ttl`.
- Go: while the gate remains, keep the `SynFloodProtectionMode == "syn-cookie"`
capability rejection pinned and verify screen snapshots carry `syn_cookie` for
the runtime path.
- Go: remove/update the `SynFloodProtectionMode == "syn-cookie"` capability
rejection and the manager test that pins it.
- Integration: hping3 SYN flood against the userspace HA cluster with
Expand Down
6 changes: 3 additions & 3 deletions docs/userspace-dataplane-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ These are the remaining explicit configuration gates in
| Feature/config shape | Gate status | Retirement blocker |
|----------------------|-------------|--------------------|
| Unsupported policy shapes | Gated | Address/application expansion must succeed for userspace |
| Screen behavior requiring SYN cookies | Gated | #1374 |
| Screen behavior requiring SYN cookies | Gated; userspace screen runtime has fail-closed cookie challenge/ACK-validation/cache scaffolding, but no HA key publication or SYN-ACK/RST TX yet | #1374 |
| Three-color policers | Gated | #1375 |
| Port mirroring | Gated | #1376 |

Expand All @@ -61,7 +61,7 @@ These are not "missing", but they are not pure userspace forwarding either:

| Area | Current boundary |
|------|------------------|
| SYN cookie flood protection | Legacy eBPF fallback |
| SYN cookie flood protection | Legacy eBPF fallback until #1374 wires HA-safe secrets, bounded SYN-ACK/RST TX, counters/status, and removes the userspace capability gate |
| Kernel-owned traffic (ARP, local delivery, management, some non-IP) | cpumap or kernel pass-through from XDP |
| GRE / ESP / explicit early filters | Tail-call back into the legacy XDP pipeline |
| IPsec / XFRM handling | Userspace detects and punts to kernel/slow-path as needed |
Expand All @@ -79,7 +79,7 @@ The current #1373 audit produced these tracked blockers:
| #1377 | Preserve address-persistent SNAT pool selection with an approved cross-backend contract. #1385 landed userspace-v1 deterministic selection and fail-closed pool admission, but does not close cross-backend parity by itself. | Phase 4 BPF source removal |
| #1378 | Finish the policy-scheduler retirement contract after #1396 userspace propagation: hit-counter survival across scheduler snapshot rebuilds, strict missing-scheduler commit behavior, and integration/failover validation | Phase 4 BPF source removal |
| #1379 | Emit policy-deny, screen-drop, and filter-log dataplane events from userspace | Phase 4 BPF source removal |
| #1374 | Implement userspace SYN-cookie flood protection or an approved equivalent | Phase 4 BPF source removal |
| #1374 | Implement userspace SYN-cookie flood protection or an approved equivalent. #1393 and the 2026-05-17 runtime slice cover deterministic cookie codec/layout, snapshot propagation, fail-closed screen challenge selection, session-miss ACK validation, and a bounded validated-client cache. Lower-layer coverage in `userspace-dp/src/screen_tests.rs` pins 4-way validated-client cache replacement; poll-stage tests only pin the operational invalid-ACK drop/bypass semantics. Remaining: validated-client cache expiration semantics, secret-epoch rotation, bounded SYN-ACK TX, ACK RST emission, HA-safe secret publication/cache survivability, counters/status, integration/failover validation, and userspace capability gate removal. | Phase 4 BPF source removal |
| #1375 | Implement userspace RFC 2697/2698 three-color policers | Phase 4 BPF source removal |
| #1376 | Implement userspace port mirroring or explicitly retire the feature | Phase 4 BPF source removal |
| #1380 | Retire the remaining BPF-map-oriented `show system buffers` operator surface now that #1386 provides userspace helper-status reporting. | Phase 5 CLI / observability cleanup |
Expand Down
22 changes: 22 additions & 0 deletions pkg/dataplane/userspace/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3169,6 +3169,28 @@ func TestBuildScreenSnapshotsMatchesZoneToProfile(t *testing.T) {
}
}

func TestBuildScreenSnapshotsMarksSynCookieMode(t *testing.T) {
cfg := &config.Config{}
cfg.Security.Flow.SynFloodProtectionMode = "syn-cookie"
cfg.Security.Zones = map[string]*config.ZoneConfig{
"trust": {Name: "trust", ScreenProfile: "flood"},
}
cfg.Security.Screen = map[string]*config.ScreenProfile{
"flood": {
Name: "flood",
TCP: config.TCPScreen{SynFlood: &config.SynFloodConfig{AttackThreshold: 100}},
},
}

snaps := buildScreenSnapshots(cfg)
if len(snaps) != 1 {
t.Fatalf("len(snaps) = %d, want 1", len(snaps))
}
if !snaps[0].SYNCookie {
t.Fatalf("SYNCookie = false, want true: %+v", snaps[0])
}
}

func TestDeriveUserspaceCapabilitiesAllowsSessionTimeouts(t *testing.T) {
cfg := &config.Config{}
cfg.Security.Flow.TCPSession = &config.TCPSessionConfig{
Expand Down
1 change: 1 addition & 0 deletions pkg/dataplane/userspace/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ type ScreenProfileSnapshot struct {
ICMPFloodThreshold uint32 `json:"icmp_flood_threshold,omitempty"`
UDPFloodThreshold uint32 `json:"udp_flood_threshold,omitempty"`
SYNFloodThreshold uint32 `json:"syn_flood_threshold,omitempty"`
SYNCookie bool `json:"syn_cookie,omitempty"`
// Advanced screen features for userspace dataplane
SessionLimitSrc uint32 `json:"session_limit_src,omitempty"`
SessionLimitDst uint32 `json:"session_limit_dst,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions pkg/dataplane/userspace/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,7 @@ func buildScreenSnapshots(cfg *config.Config) []ScreenProfileSnapshot {
}
if sp.TCP.SynFlood != nil && sp.TCP.SynFlood.AttackThreshold > 0 {
snap.SYNFloodThreshold = uint32(sp.TCP.SynFlood.AttackThreshold)
snap.SYNCookie = cfg.Security.Flow.SynFloodProtectionMode == "syn-cookie"
}
if sp.LimitSession.SourceIPBased > 0 {
snap.SessionLimitSrc = uint32(sp.LimitSession.SourceIPBased)
Expand Down
2 changes: 1 addition & 1 deletion userspace-dp/src/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ordering.

| File | Stage | What it does |
|------|-------|--------------|
| `screen.rs` | screen | Pre-session attack-protection checks (land, TCP SYN+FIN, no-flag, FIN-without-ACK, ICMP frag, plus rate-limits). Mirrors `bpf/xdp/xdp_screen.c`. Also contains the #1374 userspace SYN-cookie mint/validate core; the Go `syn-cookie` capability gate must stay closed until SYN-ACK TX replies, returning-ACK handling, HA-secret publication, bounded validated-client state, and status counters are wired into the worker path. |
| `screen.rs` | screen | Pre-session attack-protection checks (land, TCP SYN+FIN, no-flag, FIN-without-ACK, ICMP frag, plus rate-limits). Mirrors `bpf/xdp/xdp_screen.c`. Also contains the #1374 userspace SYN-cookie mint/validate core, fixed-size validated-client admission table, and session-miss ACK validation hook. The lower screen tests cover bounded 4-way validated-client cache replacement; validated-client cache expiration, secret-epoch rotation, and HA-safe secret/cache survivability are still deferred #1374 work. The Go `syn-cookie` capability gate must stay closed until SYN-ACK TX replies, ACK RST emission, HA-secret publication, status counters, and integration validation are wired into the worker path. |
| `policy.rs` | policy | Zone-pair → permit/deny + forwarding-class + DSCP-rewrite + filter chaining. `ZonePairKey` is a `u32` (`from_id << 16 \| to_id`); `JUNOS_GLOBAL_ZONE_ID = u16::MAX` is the sentinel for the global zone. |
| `nat.rs` | NAT44 | Source / destination / static NAT decisions. `NatDecision` carries `rewrite_src` and `rewrite_dst` Options the TX path consumes. |
| `nat64.rs` | NAT64 | RFC 6052 IPv4↔IPv6 translation. `Nat64Prefix` is the 96-bit + IPv4-pool config; `Nat64ReverseInfo` carries the original IPv6 tuple for reverse translation. |
Expand Down
1 change: 1 addition & 0 deletions userspace-dp/src/afxdp/forwarding_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub(super) fn build_screen_profiles(snapshot: &ConfigSnapshot) -> FxHashMap<Stri
icmp_flood_threshold: sp.icmp_flood_threshold,
udp_flood_threshold: sp.udp_flood_threshold,
syn_flood_threshold: sp.syn_flood_threshold,
syn_cookie: sp.syn_cookie,
session_limit_src: sp.session_limit_src,
session_limit_dst: sp.session_limit_dst,
port_scan_threshold: sp.port_scan_threshold,
Expand Down
19 changes: 17 additions & 2 deletions userspace-dp/src/afxdp/poll_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
use super::*;
use super::poll_stages::{
stage_classify_fabric_ingress, stage_ipsec_passthrough_check, stage_link_layer_classify,
stage_native_gre_decap, stage_parse_flow_and_learn, stage_screen_check, FabricIngressOutcome,
StageOutcome,
stage_native_gre_decap, stage_parse_flow_and_learn, stage_screen_check,
stage_screen_syn_cookie_ack_on_session_miss, FabricIngressOutcome, StageOutcome,
};

// Per-batch packet processing lifted from `poll_binding` (#678).
Expand Down Expand Up @@ -459,6 +459,21 @@ pub(super) fn poll_binding_process_descriptor(
} else {
telemetry.counters.session_misses += 1;
telemetry.dbg.session_miss += 1;
if let StageOutcome::RecycleAndContinue =
stage_screen_syn_cookie_ack_on_session_miss(
Some(flow),
packet_frame,
meta,
ingress_zone_override,
now_secs,
screen,
telemetry.counters,
worker_ctx,
)
{
binding.scratch.scratch_recycle.push(desc.addr);
continue;
}
let resolution_target =
parse_packet_destination_from_frame(packet_frame, meta)
.unwrap_or(flow.dst_ip);
Expand Down
Loading