Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d2c6079
tests/util: Make native_module_helper_t safely movable
Jun 15, 2026
cd1b65c
netebpfext: Restore sock_addr context between multi-attach programs
Jun 15, 2026
3793f4a
netebpfext: Check FWPS_RIGHT_ACTION_WRITE in sock_addr classify funct…
Jun 15, 2026
627a706
tests/util: Add optional compartment_id to socket_helper constructors
Jun 15, 2026
dd25e29
tests/socket: Extend execute_connection_test for multi-program scenarios
Jun 19, 2026
023c28e
tests/socket: Add bind multi-program test suite and scenario helpers
Jun 19, 2026
fc8c798
docs: Document bind hook multi-attach verdict combination semantics
Jun 19, 2026
feb487c
tests/socket: Address adversarial review findings in multi-attach tests
Jun 19, 2026
8f1802a
netebpfext: Fix auth-connect rights check skipping connection context…
Jun 19, 2026
85a1999
tests/socket: Fix bpf_prog_detach2 failure for BPF_ATTACH_TYPE_BIND p…
Jun 23, 2026
3e0f198
tests/socket: Remove compartment_id infrastructure from socket helpers
Jun 23, 2026
d09ba99
Merge branch 'main' into pr1-bind-multi-attach
Alan-Jowett Jul 7, 2026
4560ea3
libs/api: reject non-wildcard attachable_fd for the legacy bind hook
Jul 16, 2026
39d75d9
tests/socket: use ASCII characters in multi-attach test comments
Jul 16, 2026
e2b9bea
docs: clarify bind hook multi-attach verdict and WFP semantics
Jul 16, 2026
6096a40
netebpfext: Enable multi-attach for the sock_addr listen hook
Jun 19, 2026
aa359db
tests/socket: Add listen multi-attach test suite
Jun 19, 2026
a18d320
docs: Document listen hook multi-attach verdict combination semantics
Jun 19, 2026
fb8710a
tests/socket: Fix listen test family detection for program_policies path
Jun 19, 2026
4275644
docs: state listen verdict as permit/deny and link WFP arbitration docs
Aug 4, 2026
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
42 changes: 40 additions & 2 deletions docs/BindHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,46 @@ typedef enum _ebpf_sock_addr_verdict
} ebpf_sock_addr_verdict_t;
```

When multiple bind programs are attached, the verdicts are combined: if any program
rejects, the bind is blocked.
When multiple bind programs are attached, the verdicts are combined using a
most-restrictive accumulation rule:

- **Priority**: `REJECT` (2) > `PROCEED_HARD` (1) > `PROCEED_SOFT` (0)
- The accumulated verdict is the highest-priority value seen across all
attached programs.
- **Short-circuit on REJECT**: If any program returns `REJECT`, the provider
loop stops immediately — subsequent programs are not invoked.
- If no programs are attached (or all are detached), the default verdict is
`PROCEED_SOFT` (permit).
- An unknown/invalid return value from a program is treated as `REJECT`.

The accumulated verdict then interacts with WFP:

- `PROCEED_SOFT`: bind is allowed unless a WFP filter with higher weight blocks it.
- `PROCEED_HARD`: bind is allowed unconditionally — clears `FWPS_RIGHT_ACTION_WRITE`
so no subsequent WFP filter can override.
- `REJECT`: bind is denied (returns `WSAEACCES` / `EACCES`).

The "no subsequent WFP filter can override" guarantee for `PROCEED_HARD` applies
only to WFP filters. Among eBPF programs the most-restrictive verdict wins, so a
later program returning `REJECT` still denies a bind that another program
permitted.

### Multi-Attach Test Coverage

The following scenarios are exercised in `tests/socket/socket_tests.cpp`
(tagged `[bind_tests][multi_attach]`), across TCP/UDP and IPv4/IPv6:

| Scenario | Programs | Expected Result |
|---|---|---|
| All soft permits | 2× `PROCEED_SOFT` | Bind allowed |
| Second program rejects | `PROCEED_SOFT` + `REJECT` | Bind denied |
| First program rejects (short-circuit) | `REJECT` + `PROCEED_SOFT` | Bind denied |
| Soft + hard mix | `PROCEED_SOFT` + `PROCEED_HARD` | Bind allowed (hard priority) |
| Soft permits blocked by WFP | 2× `PROCEED_SOFT` + WFP block | Bind denied |
| Hard overrides WFP | 2 programs (one returns `PROCEED_HARD`) + WFP block | Bind allowed |
| Detach middle program | 3 programs → detach REJECT middle | Bind recovers |
| Detach and reattach | Detach + reattach with new verdict | Verdict updates |
| Three soft permits | 3× `PROCEED_SOFT` | Bind allowed |

## Architecture

Expand Down
36 changes: 36 additions & 0 deletions docs/ListenHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,42 @@ typedef enum _ebpf_sock_addr_verdict
} ebpf_sock_addr_verdict_t;
```

When multiple listen programs are attached, the verdicts are combined using a
most-restrictive accumulation rule:

- **Priority (highest wins)**: `REJECT` > `PROCEED_HARD` > `PROCEED_SOFT`
- The accumulated verdict is the highest-priority value returned by any
attached program.
- **Short-circuit on REJECT**: If any program returns `REJECT`, the provider
loop stops immediately — subsequent programs are not invoked.
- If no programs are attached (or all are detached), the default verdict is
`PROCEED_SOFT` (permit).
- An unknown/invalid return value from a program is treated as `REJECT`.

The accumulated eBPF verdict decides whether the listen is permitted
(`PROCEED_SOFT` or `PROCEED_HARD`) or denied (`REJECT`, surfaced to the caller as
`WSAEACCES` / `EACCES`). Among eBPF programs the most-restrictive verdict wins, so
a later program returning `REJECT` still denies a listen that a prior program
permitted.

Refer to [WFP Filter Arbitration](https://learn.microsoft.com/en-us/windows/win32/fwp/filter-arbitration)
for the effect of WFP filters in other sublayers on a connection that was
permitted or rejected by the eBPF programs.

### Multi-Attach Test Coverage

The following scenarios are exercised in `tests/socket/socket_tests.cpp`
(tagged `[sock_addr_tests][multi_attach]`), for TCP IPv4/IPv6:

| Scenario | Programs | Expected Result |
|---|---|---|
| All soft permits | 2× `PROCEED_SOFT` | Listen allowed |
| Second program rejects | `PROCEED_SOFT` + `REJECT` | Listen denied |
| First program rejects (short-circuit) | `REJECT` + `PROCEED_SOFT` | Listen denied |
| Hard overrides WFP | `PROCEED_SOFT` + `PROCEED_HARD` + WFP block | Listen allowed |
| REJECT beats HARD | `REJECT` + `PROCEED_HARD` | Listen denied |
| Detach middle program | 3 programs → detach REJECT middle | Listen recovers |

## Architecture

### Hook Integration and Flow
Expand Down
28 changes: 28 additions & 0 deletions libs/api/libbpf_program.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ _does_attach_type_support_attachable_fd(enum bpf_attach_type type)
case BPF_CGROUP_INET4_LISTEN:
case BPF_CGROUP_INET6_LISTEN:
case BPF_CGROUP_SOCK_OPS:
case BPF_ATTACH_TYPE_BIND:
supported = TRUE;
break;
default:
Expand All @@ -260,11 +261,31 @@ _does_attach_type_support_attachable_fd(enum bpf_attach_type type)
return supported;
}

// The legacy bind hook (BPF_ATTACH_TYPE_BIND) has no per-target attach
// parameter: its provider installs wildcard filters and ignores any supplied
// client data. Only the wildcard form is meaningful, so the libbpf-compat
// wrappers reject a non-zero attachable_fd for this attach type rather than
// silently dropping it (which would falsely imply the program was scoped to a
// specific target). The wildcard value is still encoded as a 4-byte zero
// payload, which bpf_prog_detach2 relies on to locate the matching link to
// detach.
static bool
_attach_type_is_wildcard_only(enum bpf_attach_type type)
{
return type == BPF_ATTACH_TYPE_BIND;
}

int
bpf_prog_attach(int prog_fd, int attachable_fd, enum bpf_attach_type type, unsigned int flags)
{
ebpf_result_t result = EBPF_SUCCESS;

// Bind accepts only the wildcard attachable_fd (0); a non-zero value is a
// caller error rather than a silently-ignored scope.
if (_attach_type_is_wildcard_only(type) && (attachable_fd != 0)) {
return libbpf_result_err(EBPF_INVALID_ARGUMENT);
}

if (_does_attach_type_support_attachable_fd(type) && (flags == 0)) {
result = ebpf_program_attach_by_fd(
prog_fd, get_ebpf_attach_type(type), &attachable_fd, sizeof(attachable_fd), nullptr);
Expand All @@ -288,6 +309,13 @@ bpf_prog_detach2(int prog_fd, int attachable_fd, enum bpf_attach_type type)
result = EBPF_INVALID_ARGUMENT;
return libbpf_result_err(result);
}

// Bind accepts only the wildcard attachable_fd (0); a non-zero value is a
// caller error rather than a silently-ignored scope.
if (_attach_type_is_wildcard_only(type) && (attachable_fd != 0)) {
return libbpf_result_err(EBPF_INVALID_ARGUMENT);
}

if (_does_attach_type_support_attachable_fd(type)) {
result = ebpf_program_detach(prog_fd, attach_type, &attachable_fd, sizeof(attachable_fd));
} else {
Expand Down
Loading
Loading