diff --git a/docs/BindHook.md b/docs/BindHook.md index a62619f4e6..f6d6df4a79 100644 --- a/docs/BindHook.md +++ b/docs/BindHook.md @@ -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 diff --git a/docs/ListenHook.md b/docs/ListenHook.md index 52f985b29f..a21c3f60c8 100644 --- a/docs/ListenHook.md +++ b/docs/ListenHook.md @@ -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 diff --git a/libs/api/libbpf_program.cpp b/libs/api/libbpf_program.cpp index cc8567620d..cbf58c729d 100644 --- a/libs/api/libbpf_program.cpp +++ b/libs/api/libbpf_program.cpp @@ -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: @@ -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); @@ -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 { diff --git a/netebpfext/net_ebpf_ext_sock_addr.c b/netebpfext/net_ebpf_ext_sock_addr.c index a903adfc14..7927223526 100644 --- a/netebpfext/net_ebpf_ext_sock_addr.c +++ b/netebpfext/net_ebpf_ext_sock_addr.c @@ -302,7 +302,7 @@ static bool _net_ebpf_extension_sock_addr_process_verdict(_Inout_ void* program_context, int program_verdict); static bool -_net_ebpf_extension_sock_addr_bind_process_verdict(_Inout_ void* program_context, int program_verdict); +_net_ebpf_extension_sock_addr_authorize_process_verdict(_Inout_ void* program_context, int program_verdict); // // sock_addr helper functions. @@ -1402,13 +1402,14 @@ net_ebpf_ext_sock_addr_register_providers() .create_filter_context = _net_ebpf_extension_sock_addr_create_filter_context, .delete_filter_context = _net_ebpf_extension_sock_addr_delete_filter_context, .validate_client_data = _net_ebpf_extension_sock_addr_validate_client_data, - .process_verdict = _net_ebpf_extension_sock_addr_bind_process_verdict, + .process_verdict = _net_ebpf_extension_sock_addr_authorize_process_verdict, }; const net_ebpf_extension_hook_provider_dispatch_table_t listen_dispatch_table = { .create_filter_context = _net_ebpf_extension_sock_addr_create_filter_context, .delete_filter_context = _net_ebpf_extension_sock_addr_delete_filter_context, .validate_client_data = _net_ebpf_extension_sock_addr_validate_client_data, + .process_verdict = _net_ebpf_extension_sock_addr_authorize_process_verdict, }; status = _net_ebpf_sock_addr_create_security_descriptor(); @@ -1480,7 +1481,7 @@ net_ebpf_ext_sock_addr_register_providers() attach_capability = ATTACH_CAPABILITY_MULTI_ATTACH_WITH_WILDCARD; } else if (is_cgroup_listen_attach_type) { dispatch_table = &listen_dispatch_table; - attach_capability = ATTACH_CAPABILITY_SINGLE_ATTACH_PER_HOOK; + attach_capability = ATTACH_CAPABILITY_MULTI_ATTACH_WITH_WILDCARD; } else { dispatch_table = &recv_accept_dispatch_table; attach_capability = ATTACH_CAPABILITY_SINGLE_ATTACH_PER_HOOK; @@ -1894,6 +1895,11 @@ _net_ebpf_extension_sock_addr_copy_wfp_listen_fields( { net_ebpf_extension_hook_id_t hook_id = net_ebpf_extension_get_hook_id_from_wfp_layer_id(incoming_fixed_values->layerId); + + // Listen is registered only on the ALE_AUTH_LISTEN_V4/V6 layers, so hook_id is always in range for + // wfp_connection_fields (which shares the EBPF_HOOK_ALE_AUTH_CONNECT_V4 base). Assert the invariant in + // debug builds, matching the bind and connect field-copy helpers. + ASSERT(hook_id == EBPF_HOOK_ALE_AUTH_LISTEN_V4 || hook_id == EBPF_HOOK_ALE_AUTH_LISTEN_V6); const wfp_ale_layer_fields_t* fields = &wfp_connection_fields[hook_id - EBPF_HOOK_ALE_AUTH_CONNECT_V4]; FWPS_INCOMING_VALUE0* incoming_values = incoming_fixed_values->incomingValue; @@ -2062,23 +2068,42 @@ _net_ebpf_extension_sock_addr_process_verdict(_Inout_ void* program_context, int return TRUE; } -// Multi-attach verdict accumulator for the sock_addr bind hook. Tracks the -// most-restrictive verdict across attached programs in net_ebpf_sock_addr_t::verdict -// using _get_verdict_priority(), and returns FALSE on REJECT so the hook provider -// loop stops invoking subsequent programs. Address/port writes to the context are -// ignored at bind (the WFP ALE_RESOURCE_ASSIGNMENT layer does not support address -// rewrite), so no redirect handling is performed here. +// Multi-attach verdict accumulator for sock_addr authorization gates that do +// not support context rewrite, such as the sock_addr bind hook +// (ALE_RESOURCE_ASSIGNMENT) and the sock_addr listen hook (ALE_AUTH_LISTEN). +// Tracks the most-restrictive normalized verdict across attached programs in +// net_ebpf_sock_addr_t::verdict using _get_verdict_priority(), and returns +// FALSE on REJECT so the hook provider loop stops invoking subsequent +// programs. +// +// Any address/port writes a program makes to the context are silently ignored +// for WFP purposes (the underlying ALE layer does not support address +// rewrite). To prevent one program's writes from being observed by subsequent +// programs in the same multi-attach invocation, the context is restored from +// net_ebpf_sock_addr_t::original_context after each program. The caller must +// set original_context to point at a pristine snapshot of bpf_sock_addr_t +// before invoking any program. static bool -_net_ebpf_extension_sock_addr_bind_process_verdict(_Inout_ void* program_context, int program_verdict) +_net_ebpf_extension_sock_addr_authorize_process_verdict(_Inout_ void* program_context, int program_verdict) { bpf_sock_addr_t* sock_addr_ctx = (bpf_sock_addr_t*)program_context; net_ebpf_sock_addr_t* context = CONTAINING_RECORD(sock_addr_ctx, net_ebpf_sock_addr_t, base); + bpf_sock_addr_t* original_context = context->original_context; int normalized_verdict = _normalize_sock_addr_verdict(program_verdict); + // original_context must be set by the caller before invoking programs. + // It points to a caller's stack variable and is only valid during synchronous program invocation. + ASSERT(original_context != NULL); + if (_get_verdict_priority(normalized_verdict) > _get_verdict_priority(context->verdict)) { context->verdict = normalized_verdict; } + // Restore the context so the next attached program sees the original + // WFP-provided values, not whatever the previous program may have written + // to user_ip / user_port / msg_src_*. + *sock_addr_ctx = *original_context; + return normalized_verdict != BPF_SOCK_ADDR_VERDICT_REJECT; } @@ -2112,7 +2137,8 @@ net_ebpf_extension_sock_addr_authorize_listen_classify( _Inout_ FWPS_CLASSIFY_OUT* classify_output) { EBPF_EXT_LOG_ENTRY(); - uint32_t result; + uint32_t ignored_result; + uint32_t verdict; net_ebpf_extension_sock_addr_wfp_filter_context_t* filter_context = NULL; net_ebpf_sock_addr_t net_ebpf_sock_addr_ctx = {0}; bpf_sock_addr_t* sock_addr_ctx = &net_ebpf_sock_addr_ctx.base; @@ -2124,6 +2150,14 @@ net_ebpf_extension_sock_addr_authorize_listen_classify( UNREFERENCED_PARAMETER(classify_context); UNREFERENCED_PARAMETER(flow_context); + if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) { + // A callout with higher weight has revoked the write permission. Bail out + // without touching classify_output->actionType. + EBPF_EXT_LOG_MESSAGE( + EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting."); + goto Exit; + } + classify_output->actionType = FWP_ACTION_PERMIT; filter_context = (net_ebpf_extension_sock_addr_wfp_filter_context_t*)filter->context; @@ -2162,20 +2196,34 @@ net_ebpf_extension_sock_addr_authorize_listen_classify( goto Exit; } + // Initialize the accumulated verdict to PROCEED_SOFT so that if no program updates it + // (e.g. all clients are filtered out), the listen defaults to permit. + // The authorize_process_verdict callback updates net_ebpf_sock_addr_ctx.verdict with the + // most-restrictive verdict across multi-attach programs and short-circuits on REJECT. + net_ebpf_sock_addr_ctx.verdict = BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT; + + // Snapshot the context so the shared authorize_process_verdict callback can restore it + // between programs. The snapshot is stack-local and only valid for the synchronous + // program invocation below. + bpf_sock_addr_t sock_addr_ctx_original; + memcpy(&sock_addr_ctx_original, sock_addr_ctx, sizeof(sock_addr_ctx_original)); + net_ebpf_sock_addr_ctx.original_context = &sock_addr_ctx_original; + program_result = - net_ebpf_extension_hook_expand_stack_and_invoke_programs(sock_addr_ctx, &filter_context->base, &result); + net_ebpf_extension_hook_expand_stack_and_invoke_programs(sock_addr_ctx, &filter_context->base, &ignored_result); if (program_result == EBPF_OBJECT_NOT_FOUND) { // No eBPF program is attached to this filter. goto Exit; } else if (program_result != EBPF_SUCCESS) { - // We failed to invoke at least one program in the chain, block the request. + // Failed to invoke at least one program in the chain — block the listen. classify_output->actionType = FWP_ACTION_BLOCK; + classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE; goto Exit; } - // Set action type based on verdict. - // Clear FWPS_RIGHT_ACTION_WRITE for block and hard permit. - switch (result) { + // Use the accumulated verdict from the authorize_process_verdict callback. + verdict = net_ebpf_sock_addr_ctx.verdict; + switch (verdict) { case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT: classify_output->actionType = FWP_ACTION_PERMIT; break; @@ -2194,7 +2242,7 @@ net_ebpf_extension_sock_addr_authorize_listen_classify( 0, // No transport endpoint handle for listen. sock_addr_ctx, NULL, - result, + verdict, compartment_id); Exit: @@ -2239,6 +2287,14 @@ net_ebpf_extension_sock_addr_authorize_recv_accept_classify( UNREFERENCED_PARAMETER(classify_context); UNREFERENCED_PARAMETER(flow_context); + if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) { + // A callout with higher weight has revoked the write permission. Bail out + // without touching classify_output->actionType. + EBPF_EXT_LOG_MESSAGE( + EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting."); + goto Exit; + } + classify_output->actionType = FWP_ACTION_PERMIT; filter_context = (net_ebpf_extension_sock_addr_wfp_filter_context_t*)filter->context; @@ -2359,6 +2415,14 @@ net_ebpf_extension_sock_addr_bind_classify( UNREFERENCED_PARAMETER(classify_context); UNREFERENCED_PARAMETER(flow_context); + if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) { + // A callout with higher weight has revoked the write permission. Bail out + // without touching classify_output->actionType. + EBPF_EXT_LOG_MESSAGE( + EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting."); + goto Exit; + } + classify_output->actionType = FWP_ACTION_PERMIT; filter_context = (net_ebpf_extension_sock_addr_wfp_filter_context_t*)filter->context; @@ -2387,10 +2451,17 @@ net_ebpf_extension_sock_addr_bind_classify( // Initialize the accumulated verdict to PROCEED_SOFT so that if no program updates it // (e.g. all clients are filtered out), the bind defaults to permit. - // The bind process_verdict callback updates net_ebpf_sock_addr_ctx.verdict with the + // The authorize_process_verdict callback updates net_ebpf_sock_addr_ctx.verdict with the // most-restrictive verdict across multi-attach programs and short-circuits on REJECT. net_ebpf_sock_addr_ctx.verdict = BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT; + // Snapshot the context so the shared authorize_process_verdict callback can restore it + // between programs. The snapshot is stack-local and only valid for the synchronous + // program invocation below. + bpf_sock_addr_t sock_addr_ctx_original; + memcpy(&sock_addr_ctx_original, sock_addr_ctx, sizeof(sock_addr_ctx_original)); + net_ebpf_sock_addr_ctx.original_context = &sock_addr_ctx_original; + program_result = net_ebpf_extension_hook_expand_stack_and_invoke_programs(sock_addr_ctx, &filter_context->base, &ignored_result); if (program_result == EBPF_OBJECT_NOT_FOUND) { @@ -2403,9 +2474,9 @@ net_ebpf_extension_sock_addr_bind_classify( goto Exit; } - // Use the accumulated verdict from the bind process_verdict callback. Bind hooks do not + // Use the accumulated verdict from the authorize_process_verdict callback. Bind hooks do not // support address modification: any changes the program made to user_ip/user_port are - // silently ignored. + // silently ignored (and restored between programs by the shared accumulator). verdict = net_ebpf_sock_addr_ctx.verdict; switch (verdict) { case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT: @@ -2469,6 +2540,7 @@ net_ebpf_extension_sock_addr_authorize_connection_classify( bpf_sock_addr_t* sock_addr_ctx = &net_ebpf_sock_addr_ctx.base; uint32_t compartment_id = UNSPECIFIED_COMPARTMENT_ID; ebpf_result_t program_result; + bool rights_revoked = FALSE; UNREFERENCED_PARAMETER(incoming_metadata_values); UNREFERENCED_PARAMETER(layer_data); @@ -2510,9 +2582,21 @@ net_ebpf_extension_sock_addr_authorize_connection_classify( } // First, try to find and use existing connection context from redirect layer. + // This must happen before the rights check so the cached entry is always cleaned up, + // even when a higher-weight callout has revoked our write permission. verdict = _net_ebpf_ext_find_and_remove_connection_context( incoming_metadata_values->transportEndpointHandle, sock_addr_ctx); + if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) { + // A callout with higher weight has revoked the write permission. Bail out + // without touching classify_output->actionType (the Exit-block switch is + // also skipped via rights_revoked). The cache cleanup above has already run. + EBPF_EXT_LOG_MESSAGE( + EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting."); + rights_revoked = TRUE; + goto Exit; + } + // CONNECT_AUTHORIZATION programs run for all non-REJECT verdicts from the redirect layer. // REJECT is already final. PROCEED_HARD and PROCEED_SOFT both allow authorization programs // to run so they can make decisions based on route-dependent metadata. @@ -2556,18 +2640,20 @@ net_ebpf_extension_sock_addr_authorize_connection_classify( Exit: // Set action type based on verdict. // Clear FWPS_RIGHT_ACTION_WRITE for block and hard permit. - switch (verdict) { - case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT: - classify_output->actionType = FWP_ACTION_PERMIT; - break; - case BPF_SOCK_ADDR_VERDICT_PROCEED_HARD: - classify_output->actionType = FWP_ACTION_PERMIT; - classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE; - break; - default: - classify_output->actionType = FWP_ACTION_BLOCK; - classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE; - break; + if (!rights_revoked) { + switch (verdict) { + case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT: + classify_output->actionType = FWP_ACTION_PERMIT; + break; + case BPF_SOCK_ADDR_VERDICT_PROCEED_HARD: + classify_output->actionType = FWP_ACTION_PERMIT; + classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE; + break; + default: + classify_output->actionType = FWP_ACTION_BLOCK; + classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE; + break; + } } _net_ebpf_ext_log_sock_addr_classify( diff --git a/tests/libs/util/native_helper.cpp b/tests/libs/util/native_helper.cpp index 5bf9978061..6f55ee73bb 100644 --- a/tests/libs/util/native_helper.cpp +++ b/tests/libs/util/native_helper.cpp @@ -68,3 +68,25 @@ _native_module_helper::~_native_module_helper() DeleteFileA(_file_name.c_str()); } } + +_native_module_helper::_native_module_helper(_native_module_helper&& other) noexcept + : _file_name(std::move(other._file_name)), _delete_file_on_destruction(other._delete_file_on_destruction), + _is_main_thread(other._is_main_thread) +{ + other._delete_file_on_destruction = false; +} + +_native_module_helper& +_native_module_helper::operator=(_native_module_helper&& other) noexcept +{ + if (this != &other) { + if (_delete_file_on_destruction && !_file_name.empty()) { + DeleteFileA(_file_name.c_str()); + } + _file_name = std::move(other._file_name); + _delete_file_on_destruction = other._delete_file_on_destruction; + _is_main_thread = other._is_main_thread; + other._delete_file_on_destruction = false; + } + return *this; +} diff --git a/tests/libs/util/native_helper.hpp b/tests/libs/util/native_helper.hpp index 0efc64e815..21d9b11fab 100644 --- a/tests/libs/util/native_helper.hpp +++ b/tests/libs/util/native_helper.hpp @@ -21,6 +21,21 @@ typedef class _native_module_helper { public: + _native_module_helper() = default; + + // Non-copyable: the destructor deletes the underlying .sys file, so two + // instances with the same _file_name and _delete_file_on_destruction = true + // would race / double-delete. + _native_module_helper(const _native_module_helper&) = delete; + _native_module_helper& + operator=(const _native_module_helper&) = delete; + + // Movable: transfer the file ownership flag and clear it on the source so + // only the destination's destructor deletes the file. + _native_module_helper(_native_module_helper&& other) noexcept; + _native_module_helper& + operator=(_native_module_helper&& other) noexcept; + void initialize(_In_z_ const char* file_name_prefix) { diff --git a/tests/socket/socket_tests.cpp b/tests/socket/socket_tests.cpp index 4082c6cdd7..f97a0c1485 100644 --- a/tests/socket/socket_tests.cpp +++ b/tests/socket/socket_tests.cpp @@ -90,32 +90,6 @@ enum class connection_test_result block, }; -/** - * @brief Test parameters for individual connection test. - */ -struct connection_test_params -{ - std::string_view description; - - // Expected bind error for server socket (0 = expect success). - std::optional expected_server_bind_error{}; - - // Expected listen error for server socket (0 = expect success). - std::optional expected_listen_error{}; - - // Expected outcome. - connection_test_result expected_result{connection_test_result::block}; - - std::optional egress_verdict{}; ///< Egress verdict for connect hook. - std::optional ingress_verdict{}; ///< Ingress verdict for recv_accept hook. - std::optional listen_verdict{}; ///< Listen verdict for listen enforcement (sock_addr). - std::optional bind_policy{}; ///< Bind policy to apply for this test. - std::optional - bind_verdict{}; ///< Simplified bind policy (uses current process, test port, test protocol). - std::optional sock_addr_bind_verdict{}; ///< Verdict for the sock_addr-aligned bind hook - ///< (uses test port, test protocol). -}; - /** * @brief Program attach method for the connection test framework. * @@ -129,6 +103,18 @@ enum class attach_method_t bpf_prog_attach, ///< libbpf-compat API with 4-byte attach parameter containing compartment_id=0. }; +/** + * @brief Reference to a specific program within a loaded module set. + * + * Used by multi-program test infrastructure to target policy updates, + * attachment steps, and counter expectations at specific programs. + */ +struct program_ref +{ + size_t module_index{0}; ///< Index in connection_test_case::modules / loaded_modules. + size_t program_index{0}; ///< Index in module_spec::programs. +}; + /** * @brief Program specification for attaching. */ @@ -148,6 +134,82 @@ struct module_spec std::vector programs{}; ///< Programs to load from the object file. }; +/** + * @brief Per-module policy specification for multi-program tests. + * + * Each entry targets a specific module (by index) and carries the same verdict + * fields as the legacy single-program shorthand. The framework applies these + * to the target module's own maps, enabling per-program verdict control. + */ +struct program_policy_spec +{ + program_ref target{}; + + std::optional egress_verdict{}; + std::optional ingress_verdict{}; + std::optional listen_verdict{}; + std::optional bind_policy{}; + std::optional bind_verdict{}; + std::optional sock_addr_bind_verdict{}; +}; + +/** + * @brief Attachment action for dynamic attach/detach steps. + */ +enum class attachment_action +{ + do_attach, + do_detach, +}; + +/** + * @brief A single attachment state change applied before a test step. + * + * Used to express detach-first/middle/last and reattach scenarios. + */ +struct attachment_step +{ + attachment_action action{}; + program_ref target{}; +}; + +/** + * @brief Test parameters for individual connection test. + */ +struct connection_test_params +{ + std::string_view description; + + // Expected bind error for server socket (0 = expect success). + std::optional expected_server_bind_error{}; + + // Expected listen error for server socket (0 = expect success). + std::optional expected_listen_error{}; + + // Expected outcome. + connection_test_result expected_result{connection_test_result::block}; + + // Legacy single-program shorthand (unchanged for existing callers). + std::optional egress_verdict{}; ///< Egress verdict for connect hook. + std::optional ingress_verdict{}; ///< Ingress verdict for recv_accept hook. + std::optional listen_verdict{}; ///< Listen verdict for listen enforcement (sock_addr). + std::optional bind_policy{}; ///< Bind policy to apply for this test. + std::optional + bind_verdict{}; ///< Simplified bind policy (uses current process, test port, test protocol). + std::optional sock_addr_bind_verdict{}; ///< Verdict for the sock_addr-aligned bind hook + ///< (uses test port, test protocol). + + // --- Multi-program controls --- + + /// Attachment state changes to apply before this test step executes. + /// Processed in order: detach/attach programs dynamically. + std::vector before{}; + + /// Per-module policy updates. Each entry targets a specific module's maps. + /// When non-empty, legacy shorthand fields above are ignored. + std::vector program_policies{}; +}; + /** * @brief Connection test case specification. * @@ -321,8 +383,10 @@ execute_connection_attempt( * This function orchestrates a complete connection test scenario including: * - Loading eBPF modules and programs from object files * - Creating WFP filters if specified - * - Retrieving policy maps (sock_addr and bind) + * - Retrieving policy maps (sock_addr and bind) -- both per-module and legacy global * - Attaching eBPF programs to their respective attach points + * - Processing dynamic attach/detach steps per test + * - Applying per-module policy updates for multi-program scenarios * - Creating and managing client/server socket pairs * - Executing individual test steps with configured policies * - Validating connection behavior against expected results @@ -336,18 +400,31 @@ execute_connection_attempt( static void execute_connection_test(_In_ const connection_test_case& test_case) { + // Per-module map set for multi-program tests. + struct module_maps + { + bpf_map* ingress_connection_policy_map{}; + bpf_map* egress_connection_policy_map{}; + bpf_map* bind_policy_map{}; + bpf_map* connection_map{}; + bpf_map* listen_connection_policy_map{}; + bpf_map* bind_verdict_map{}; + }; + // Load modules (object files + programs). struct loaded_program { bpf_program* program; program_spec spec; bpf_link* link; + bool attached; }; struct loaded_module { native_module_helper_t helper; bpf_object_ptr object; std::vector programs; + module_maps maps; }; std::vector loaded_modules; @@ -363,12 +440,34 @@ execute_connection_test(_In_ const connection_test_case& test_case) for (const auto& prog_spec : module.programs) { auto* prog = bpf_object__find_program_by_name(obj, prog_spec.program_name.data()); SAFE_REQUIRE(prog != nullptr); - mod.programs.push_back({prog, prog_spec, nullptr}); + mod.programs.push_back({prog, prog_spec, nullptr, false}); } + // Per-module map discovery. + mod.maps.ingress_connection_policy_map = bpf_object__find_map_by_name(obj, "ingress_connection_policy_map"); + mod.maps.egress_connection_policy_map = bpf_object__find_map_by_name(obj, "egress_connection_policy_map"); + mod.maps.bind_policy_map = bpf_object__find_map_by_name(obj, "bind_policy_map"); + mod.maps.connection_map = bpf_object__find_map_by_name(obj, "connection_map"); + mod.maps.listen_connection_policy_map = bpf_object__find_map_by_name(obj, "listen_connection_policy_map"); + mod.maps.bind_verdict_map = bpf_object__find_map_by_name(obj, "bind_verdict_map"); + loaded_modules.push_back(std::move(mod)); } + // Bounds-checked program reference resolver. + auto resolve_program_ref = [&](const program_ref& ref) -> loaded_program& { + SAFE_REQUIRE(ref.module_index < loaded_modules.size()); + auto& mod = loaded_modules[ref.module_index]; + SAFE_REQUIRE(ref.program_index < mod.programs.size()); + return mod.programs[ref.program_index]; + }; + + // Resolve module maps for a program reference. + auto resolve_module_maps = [&](const program_ref& ref) -> module_maps& { + SAFE_REQUIRE(ref.module_index < loaded_modules.size()); + return loaded_modules[ref.module_index].maps; + }; + // Create WFP filters if specified. std::unique_ptr filter; if (!test_case.wfp_filters.empty()) { @@ -379,7 +478,7 @@ execute_connection_test(_In_ const connection_test_case& test_case) } } - // Get policy maps (sock_addr, bind, or sockops). + // Legacy global maps: first-match across all modules (for existing single-program callers). bpf_map* ingress_map = nullptr; bpf_map* egress_map = nullptr; bpf_map* bind_policy_map = nullptr; @@ -389,22 +488,22 @@ execute_connection_test(_In_ const connection_test_case& test_case) for (const auto& mod : loaded_modules) { if (!ingress_map) { - ingress_map = bpf_object__find_map_by_name(mod.object.get(), "ingress_connection_policy_map"); + ingress_map = mod.maps.ingress_connection_policy_map; } if (!egress_map) { - egress_map = bpf_object__find_map_by_name(mod.object.get(), "egress_connection_policy_map"); + egress_map = mod.maps.egress_connection_policy_map; } if (!bind_policy_map) { - bind_policy_map = bpf_object__find_map_by_name(mod.object.get(), "bind_policy_map"); + bind_policy_map = mod.maps.bind_policy_map; } if (!connection_map) { - connection_map = bpf_object__find_map_by_name(mod.object.get(), "connection_map"); + connection_map = mod.maps.connection_map; } if (!listen_map) { - listen_map = bpf_object__find_map_by_name(mod.object.get(), "listen_connection_policy_map"); + listen_map = mod.maps.listen_connection_policy_map; } if (!bind_verdict_map) { - bind_verdict_map = bpf_object__find_map_by_name(mod.object.get(), "bind_verdict_map"); + bind_verdict_map = mod.maps.bind_verdict_map; } } @@ -433,76 +532,97 @@ execute_connection_test(_In_ const connection_test_case& test_case) // server is binding a specific address and shouldn't accept the other family). For all other // tests (connect, recv_accept, bind), dual-stack is correct because the server needs to // accept connections from both V4 and V6 clients. - bool is_listen_test = std::any_of( - test_case.tests.begin(), test_case.tests.end(), [](const auto& t) { return t.listen_verdict.has_value(); }); + bool is_listen_test = std::any_of(test_case.tests.begin(), test_case.tests.end(), [](const auto& t) { + if (t.listen_verdict.has_value()) { + return true; + } + return std::any_of(t.program_policies.begin(), t.program_policies.end(), [](const auto& p) { + return p.listen_verdict.has_value(); + }); + }); bool use_specific_family = is_listen_test || test_case.server_bind_address.has_value(); socket_family_t server_family = use_specific_family ? (test_case.address_family == AF_INET ? IPv4 : IPv6) : Dual; + // Helper: attach a single program using its spec's attach_method. + auto attach_program = [](loaded_program& lp) { + bpf_program* program = lp.program; + if (lp.spec.attach_method == attach_method_t::bpf_prog_attach) { + // libbpf-compat path: second argument is compartment_id (0 = wildcard). + int rc = ::bpf_prog_attach(bpf_program__fd(program), 0, lp.spec.attach_type, 0); + SAFE_REQUIRE(rc == 0); + } else { + // Native API path: NULL attach parameter (wildcard / unspecified compartment). + ebpf_attach_type_t attach_type_guid{}; + SAFE_REQUIRE(ebpf_get_ebpf_attach_type(lp.spec.attach_type, &attach_type_guid) == EBPF_SUCCESS); + SAFE_REQUIRE(ebpf_program_attach(program, &attach_type_guid, nullptr, 0, nullptr) == EBPF_SUCCESS); + } + lp.attached = true; + }; + + // Helper: detach a single program. + auto detach_program = [](loaded_program& lp) { + if (!lp.attached) { + return; + } + // Use bpf_prog_detach2 uniformly -- it works for both attach paths. + // For ebpf_program_attach with NULL (wildcard), compartment 0 matches. + int rc = ::bpf_prog_detach2(bpf_program__fd(lp.program), 0, lp.spec.attach_type); + SAFE_REQUIRE(rc == 0); + lp.attached = false; + }; + // Attach all programs before executing tests. for (auto& mod : loaded_modules) { CAPTURE(mod.helper.get_file_name()); - for (auto& loaded_program : mod.programs) { - bpf_program* program = loaded_program.program; + for (auto& loaded_prog : mod.programs) { CAPTURE( - std::string(loaded_program.spec.program_name), - loaded_program.spec.attach_type, - bpf_program__fd(loaded_program.program)); - if (loaded_program.spec.attach_method == attach_method_t::bpf_prog_attach) { - // libbpf-compat path: passes a 4-byte attach parameter containing compartment_id=0. - int rc = ::bpf_prog_attach(bpf_program__fd(program), 0, loaded_program.spec.attach_type, 0); - SAFE_REQUIRE(rc == 0); - } else { - // Native API path: passes NULL attach parameter (wildcard / unspecified compartment). - ebpf_attach_type_t attach_type_guid{}; - SAFE_REQUIRE( - ebpf_get_ebpf_attach_type(loaded_program.spec.attach_type, &attach_type_guid) == EBPF_SUCCESS); - SAFE_REQUIRE(ebpf_program_attach(program, &attach_type_guid, nullptr, 0, nullptr) == EBPF_SUCCESS); - } + std::string(loaded_prog.spec.program_name), + loaded_prog.spec.attach_type, + bpf_program__fd(loaded_prog.program)); + attach_program(loaded_prog); } } - // Execute tests. - std::unique_ptr client; - std::unique_ptr server; - int test_index = 0; + // Helper: apply per-module policy for a single program_policy_spec entry. + auto apply_program_policy = [&](const program_policy_spec& policy, const connection_tuple_t& conn_tuple) { + auto& maps = resolve_module_maps(policy.target); - for (const auto& test : test_case.tests) { - INFO("test " << test_index << ": " << test.description); - CAPTURE(test.expected_result); - - // Update policy maps based on test policy. - if (test.egress_verdict) { - SAFE_REQUIRE(egress_map != nullptr); - SAFE_REQUIRE(bpf_map_update_elem(bpf_map__fd(egress_map), &tuple, &(*test.egress_verdict), EBPF_ANY) == 0); + if (policy.egress_verdict) { + SAFE_REQUIRE(maps.egress_connection_policy_map != nullptr); + SAFE_REQUIRE( + bpf_map_update_elem( + bpf_map__fd(maps.egress_connection_policy_map), &conn_tuple, &(*policy.egress_verdict), EBPF_ANY) == + 0); } - if (test.ingress_verdict) { - SAFE_REQUIRE(ingress_map != nullptr); + if (policy.ingress_verdict) { + SAFE_REQUIRE(maps.ingress_connection_policy_map != nullptr); SAFE_REQUIRE( - bpf_map_update_elem(bpf_map__fd(ingress_map), &tuple, &(*test.ingress_verdict), EBPF_ANY) == 0); + bpf_map_update_elem( + bpf_map__fd(maps.ingress_connection_policy_map), + &conn_tuple, + &(*policy.ingress_verdict), + EBPF_ANY) == 0); } - if (test.bind_policy) { - SAFE_REQUIRE(bind_policy_map != nullptr); + if (policy.bind_policy) { + SAFE_REQUIRE(maps.bind_policy_map != nullptr); _update_bind_policy_map_entry( - bpf_map__fd(bind_policy_map), - test.bind_policy->process_id, - test.bind_policy->port, - test.bind_policy->protocol, - test.bind_policy->action); + bpf_map__fd(maps.bind_policy_map), + policy.bind_policy->process_id, + policy.bind_policy->port, + policy.bind_policy->protocol, + policy.bind_policy->action); } - if (test.bind_verdict) { - SAFE_REQUIRE(bind_policy_map != nullptr); + if (policy.bind_verdict) { + SAFE_REQUIRE(maps.bind_policy_map != nullptr); _update_bind_policy_map_entry( - bpf_map__fd(bind_policy_map), - 0, // process_id = 0 (wildcard). + bpf_map__fd(maps.bind_policy_map), + 0, static_cast(SOCKET_TEST_PORT), static_cast(test_case.protocol), - *test.bind_verdict); + *policy.bind_verdict); } - if (test.listen_verdict) { - SAFE_REQUIRE(listen_map != nullptr); - // Setup tuple for listen operation — key uses local address/port. - // When server_bind_address is provided, populate local_ip from it; otherwise the - // server binds to INADDR_ANY and WFP reports local_ip as zero. + if (policy.listen_verdict) { + SAFE_REQUIRE(maps.listen_connection_policy_map != nullptr); connection_tuple_t listen_tuple = {0}; if (test_case.server_bind_address) { if (test_case.address_family == AF_INET) { @@ -515,17 +635,106 @@ execute_connection_test(_In_ const connection_test_case& test_case) } } listen_tuple.local_port = htons(SOCKET_TEST_PORT); - listen_tuple.protocol = tuple.protocol; + listen_tuple.protocol = conn_tuple.protocol; SAFE_REQUIRE( - bpf_map_update_elem(bpf_map__fd(listen_map), &listen_tuple, &(*test.listen_verdict), EBPF_ANY) == 0); + bpf_map_update_elem( + bpf_map__fd(maps.listen_connection_policy_map), + &listen_tuple, + &(*policy.listen_verdict), + EBPF_ANY) == 0); } - if (test.sock_addr_bind_verdict) { - SAFE_REQUIRE(bind_verdict_map != nullptr); + if (policy.sock_addr_bind_verdict) { + SAFE_REQUIRE(maps.bind_verdict_map != nullptr); _update_sock_addr_bind_verdict_map_entry( - bpf_map__fd(bind_verdict_map), + bpf_map__fd(maps.bind_verdict_map), htons(static_cast(SOCKET_TEST_PORT)), static_cast(test_case.protocol), - *test.sock_addr_bind_verdict); + *policy.sock_addr_bind_verdict); + } + }; + + // Execute tests. + std::unique_ptr client; + std::unique_ptr server; + int test_index = 0; + + for (const auto& test : test_case.tests) { + INFO("test " << test_index << ": " << test.description); + CAPTURE(test.expected_result); + + // Process dynamic attachment steps (detach/attach) before the test step. + for (const auto& step : test.before) { + auto& lp = resolve_program_ref(step.target); + if (step.action == attachment_action::do_detach) { + detach_program(lp); + } else { + attach_program(lp); + } + } + + // Apply per-module policies if specified; otherwise fall through to legacy shorthand. + if (!test.program_policies.empty()) { + for (const auto& policy : test.program_policies) { + apply_program_policy(policy, tuple); + } + } else { + // Legacy single-program map updates (backwards-compatible path). + if (test.egress_verdict) { + SAFE_REQUIRE(egress_map != nullptr); + SAFE_REQUIRE( + bpf_map_update_elem(bpf_map__fd(egress_map), &tuple, &(*test.egress_verdict), EBPF_ANY) == 0); + } + if (test.ingress_verdict) { + SAFE_REQUIRE(ingress_map != nullptr); + SAFE_REQUIRE( + bpf_map_update_elem(bpf_map__fd(ingress_map), &tuple, &(*test.ingress_verdict), EBPF_ANY) == 0); + } + if (test.bind_policy) { + SAFE_REQUIRE(bind_policy_map != nullptr); + _update_bind_policy_map_entry( + bpf_map__fd(bind_policy_map), + test.bind_policy->process_id, + test.bind_policy->port, + test.bind_policy->protocol, + test.bind_policy->action); + } + if (test.bind_verdict) { + SAFE_REQUIRE(bind_policy_map != nullptr); + _update_bind_policy_map_entry( + bpf_map__fd(bind_policy_map), + 0, + static_cast(SOCKET_TEST_PORT), + static_cast(test_case.protocol), + *test.bind_verdict); + } + if (test.listen_verdict) { + SAFE_REQUIRE(listen_map != nullptr); + connection_tuple_t listen_tuple = {0}; + if (test_case.server_bind_address) { + if (test_case.address_family == AF_INET) { + const sockaddr_in* addr4 = + reinterpret_cast(&(*test_case.server_bind_address)); + listen_tuple.local_ip.ipv4 = addr4->sin_addr.s_addr; + } else { + const sockaddr_in6* addr6 = + reinterpret_cast(&(*test_case.server_bind_address)); + memcpy(listen_tuple.local_ip.ipv6, &addr6->sin6_addr, sizeof(listen_tuple.local_ip.ipv6)); + } + } + listen_tuple.local_port = htons(SOCKET_TEST_PORT); + listen_tuple.protocol = tuple.protocol; + SAFE_REQUIRE( + bpf_map_update_elem(bpf_map__fd(listen_map), &listen_tuple, &(*test.listen_verdict), EBPF_ANY) == + 0); + } + if (test.sock_addr_bind_verdict) { + SAFE_REQUIRE(bind_verdict_map != nullptr); + _update_sock_addr_bind_verdict_map_entry( + bpf_map__fd(bind_verdict_map), + htons(static_cast(SOCKET_TEST_PORT)), + static_cast(test_case.protocol), + *test.sock_addr_bind_verdict); + } } // Create sockets on init or after reset. @@ -559,6 +768,7 @@ execute_connection_test(_In_ const connection_test_case& test_case) if (server_bind_error != 0 || server_listen_error != 0) { server.reset(); client.reset(); + ++test_index; continue; } SAFE_REQUIRE(server != nullptr); @@ -589,6 +799,13 @@ execute_connection_test(_In_ const connection_test_case& test_case) ++test_index; } + + // Cleanup: detach all programs that are still attached. + for (auto& mod : loaded_modules) { + for (auto& lp : mod.programs) { + detach_program(lp); + } + } } // Type tuples for TEMPLATE_TEST_CASE: (address_family, protocol). @@ -603,6 +820,50 @@ using udp_v6_params = #define ALL_CONNECTION_TEST_PARAMS tcp_v4_params, tcp_v6_params, udp_v4_params, udp_v6_params +// --------------------------------------------------------------------------- +// Hook descriptor helpers for multi-program bind test scenarios. +// +// These reduce boilerplate in multi-program test cases without hiding the +// important parts (expected verdicts, expected results). Each test case still +// explicitly declares its connection_test_case -- these helpers just produce +// the repetitive hook-specific pieces. +// --------------------------------------------------------------------------- + +/** + * @brief Create a module_spec for the sock_addr-aligned bind hook. + * + * Each call produces an independent module that, when loaded by the framework, + * creates a uniquely-named .sys copy with its own bind_verdict_map. + */ +static module_spec +sock_addr_bind_module(ADDRESS_FAMILY family) +{ + return { + .object_file = "cgroup_sock_addr_bind", + .programs{ + {.program_name = (family == AF_INET) ? "authorize_bind4" : "authorize_bind6", + .attach_type = (family == AF_INET) ? BPF_CGROUP_INET4_BIND : BPF_CGROUP_INET6_BIND}}, + }; +} + +/** + * @brief Create a program_policy_spec that sets a bind verdict for a specific module. + */ +static program_policy_spec +sock_addr_bind_verdict(size_t module_index, ebpf_sock_addr_verdict_t verdict) +{ + return {.target = {.module_index = module_index}, .sock_addr_bind_verdict = verdict}; +} + +/** + * @brief Return the WFP layer GUID for bind (ALE_RESOURCE_ASSIGNMENT) given address family. + */ +static GUID +sock_addr_bind_wfp_layer(ADDRESS_FAMILY family) +{ + return (family == AF_INET) ? FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4 : FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V6; +} + TEST_CASE("connection_test_attach_all", "[attach]") { execute_connection_test({ @@ -1240,6 +1501,451 @@ TEST_CASE("bind_helper_functions_validation_tcp_v6", "[bind_tests][helper_valida bind_helper_functions_validation_test(AF_INET6); } +// =========================================================================== +// Multi-program bind tests (sock_addr-aligned cgroup/bind4 / cgroup/bind6). +// +// These tests exercise verdict accumulation across multiple independently-loaded +// instances of cgroup_sock_addr_bind, each with its own bind_verdict_map. +// =========================================================================== + +// Two programs both return PROCEED_SOFT -> accumulated verdict is PROCEED_SOFT -> bind allowed. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_all_soft", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_all_soft", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{{ + .description = "Two soft permits allow bind", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// First program PROCEED_SOFT, second program REJECT -> accumulated verdict is REJECT -> bind denied. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_second_rejects", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_second_rejects", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{{ + .description = "Second program rejects after first soft permit", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_REJECT), + }, + }}, + }); +} + +// First program REJECT -> short-circuit, second program never contributes -> bind denied. +// Verifies that a leading REJECT terminates the accumulation loop. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_first_rejects", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_first_rejects", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{{ + .description = "First program rejects, short-circuits accumulation", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// Mix of PROCEED_SOFT + PROCEED_HARD -> accumulated verdict is PROCEED_HARD (higher priority) -> bind allowed. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_soft_hard_mix", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_soft_hard_mix", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{{ + .description = "Soft + hard mix yields hard permit", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + }, + }}, + }); +} + +// Multi-program with WFP block: both programs return PROCEED_SOFT -> WFP block overrides -> bind denied. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_soft_blocked_by_wfp", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_soft_blocked_by_wfp", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .wfp_filters{{ + .layer = sock_addr_bind_wfp_layer(family), + .local_port = static_cast(SOCKET_TEST_PORT), + }}, + .tests{{ + .description = "Two soft permits cannot override WFP block", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// Multi-program with WFP block: one PROCEED_HARD -> hard permit overrides WFP -> bind allowed. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_hard_overrides_wfp", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_hard_overrides_wfp", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .wfp_filters{{ + .layer = sock_addr_bind_wfp_layer(family), + .local_port = static_cast(SOCKET_TEST_PORT), + }}, + .tests{{ + .description = "Hard permit from one program overrides WFP block", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + }, + }}, + }); +} + +// Detach middle program: start with 3 programs (PROCEED_SOFT, REJECT, PROCEED_SOFT). The middle program's REJECT +// should deny bind. After detaching the middle program, only the two PROCEED_SOFT programs remain and bind +// should succeed. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_detach_middle", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_detach_middle", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_bind_module(family), + sock_addr_bind_module(family), + sock_addr_bind_module(family), + }, + .tests{ + { + .description = "With middle program rejecting, bind is denied", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + { + .description = "After detaching middle program, bind succeeds", + .expected_result = connection_test_result::allow, + .before{{.action = attachment_action::do_detach, .target = {.module_index = 1}}}, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + }, + }); +} + +// Detach and reattach: start with 2 programs (both PROCEED_SOFT -> allow). Detach program 1, change its +// verdict to REJECT, reattach. Bind should now be denied. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_detach_reattach", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_detach_reattach", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{ + { + .description = "Both programs soft permit, bind allowed", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + { + .description = "After detach+reattach with reject, bind denied", + .expected_server_bind_error = WSAEACCES, + .before{ + {.action = attachment_action::do_detach, .target = {.module_index = 1}}, + {.action = attachment_action::do_attach, .target = {.module_index = 1}}, + }, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_REJECT), + }, + }, + }, + }); +} + +// Three programs all PROCEED_SOFT -> bind allowed. Tests accumulation across more than 2 programs. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_three_soft", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_three_soft", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_bind_module(family), + sock_addr_bind_module(family), + sock_addr_bind_module(family), + }, + .tests{{ + .description = "Three soft permits allow bind", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// PROCEED_HARD first, PROCEED_SOFT second, with WFP block -> PROCEED_HARD wins, bind allowed. +// Validates that HARD is accumulated regardless of program ordering (not just last-wins). +TEMPLATE_TEST_CASE( + "sock_addr_bind_multi_hard_first_overrides_wfp", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_hard_first_overrides_wfp", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .wfp_filters{{ + .layer = sock_addr_bind_wfp_layer(family), + .local_port = static_cast(SOCKET_TEST_PORT), + }}, + .tests{{ + .description = "Hard permit first, soft second, WFP block -> bind allowed", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// REJECT + PROCEED_HARD -> REJECT wins (higher priority). Validates that PROCEED_HARD cannot override REJECT. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_reject_beats_hard", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_reject_beats_hard", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{{ + .description = "Reject from first program overrides hard permit from second", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + }, + }}, + }); +} + +// PROCEED_HARD + REJECT (reversed) -> REJECT wins. Tests that REJECT short-circuits even after PROCEED_HARD. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_hard_then_reject", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_hard_then_reject", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_bind_module(family), sock_addr_bind_module(family)}, + .tests{{ + .description = "Hard permit then reject -> reject wins", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_REJECT), + }, + }}, + }); +} + +// Third program is decisive: two PROCEED_SOFT then one REJECT -> bind denied. +// Validates that the accumulator processes all N programs, not just the first two. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_third_rejects", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_third_rejects", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_bind_module(family), + sock_addr_bind_module(family), + sock_addr_bind_module(family), + }, + .tests{{ + .description = "Third program rejects after two soft permits", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_REJECT), + }, + }}, + }); +} + +// Third program provides decisive PROCEED_HARD permit with WFP block -> bind allowed. +// Validates accumulator reaches the third program and PROCEED_HARD overrides WFP. +TEMPLATE_TEST_CASE( + "sock_addr_bind_multi_third_hard_overrides_wfp", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_third_hard_overrides_wfp", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_bind_module(family), + sock_addr_bind_module(family), + sock_addr_bind_module(family), + }, + .wfp_filters{{ + .layer = sock_addr_bind_wfp_layer(family), + .local_port = static_cast(SOCKET_TEST_PORT), + }}, + .tests{{ + .description = "Third program hard permit overrides WFP block", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + }, + }}, + }); +} + +// Detach first program: 3 programs with first=REJECT -> denied. Detach first -> two PROCEED_SOFTs remain -> allowed. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_detach_first", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_detach_first", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_bind_module(family), + sock_addr_bind_module(family), + sock_addr_bind_module(family), + }, + .tests{ + { + .description = "With first program rejecting, bind is denied", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + { + .description = "After detaching first program, bind succeeds", + .expected_result = connection_test_result::allow, + .before{{.action = attachment_action::do_detach, .target = {.module_index = 0}}}, + .program_policies{ + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + }, + }); +} + +// Detach last program: 3 programs with last=REJECT -> denied. Detach last -> two PROCEED_SOFTs remain -> allowed. +TEMPLATE_TEST_CASE("sock_addr_bind_multi_detach_last", "[bind_tests][multi_attach]", ALL_CONNECTION_TEST_PARAMS) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_bind_multi_detach_last", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_bind_module(family), + sock_addr_bind_module(family), + sock_addr_bind_module(family), + }, + .tests{ + { + .description = "With last program rejecting, bind is denied", + .expected_server_bind_error = WSAEACCES, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(2, BPF_SOCK_ADDR_VERDICT_REJECT), + }, + }, + { + .description = "After detaching last program, bind succeeds", + .expected_result = connection_test_result::allow, + .before{{.action = attachment_action::do_detach, .target = {.module_index = 2}}}, + .program_policies{ + sock_addr_bind_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_bind_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + }, + }); +} + void helper_functions_validation_test( ADDRESS_FAMILY address_family, @@ -1799,6 +2505,197 @@ TEMPLATE_TEST_CASE("connection_test_listen_hard_permit", "[sock_addr_tests]", tc }); } +// --------------------------------------------------------------------------- +// Multi-program listen tests (sock_addr-aligned cgroup/listen4 / cgroup/listen6). +// +// These mirror the bind multi-attach tests but are TCP-only (listen is a +// TCP operation). Each test loads multiple independent instances of +// cgroup_sock_addr, each with its own listen_connection_policy_map. +// --------------------------------------------------------------------------- + +/** + * @brief Create a module_spec for the sock_addr-aligned listen hook. + */ +static module_spec +sock_addr_listen_module(ADDRESS_FAMILY family) +{ + return { + .object_file = "cgroup_sock_addr", + .programs{ + {.program_name = (family == AF_INET) ? "authorize_listen4" : "authorize_listen6", + .attach_type = (family == AF_INET) ? BPF_CGROUP_INET4_LISTEN : BPF_CGROUP_INET6_LISTEN}}, + }; +} + +/** + * @brief Create a program_policy_spec that sets a listen verdict for a specific module. + */ +static program_policy_spec +sock_addr_listen_verdict(size_t module_index, uint32_t verdict) +{ + return {.target = {.module_index = module_index}, .listen_verdict = verdict}; +} + +/** + * @brief Return the WFP layer GUID for listen (ALE_AUTH_LISTEN) given address family. + */ +static GUID +sock_addr_listen_wfp_layer(ADDRESS_FAMILY family) +{ + return (family == AF_INET) ? FWPM_LAYER_ALE_AUTH_LISTEN_V4 : FWPM_LAYER_ALE_AUTH_LISTEN_V6; +} + +// Two programs both PROCEED_SOFT -> listen allowed. +TEMPLATE_TEST_CASE("sock_addr_listen_multi_all_soft", "[sock_addr_tests][multi_attach]", tcp_v4_params, tcp_v6_params) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_listen_multi_all_soft", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_listen_module(family), sock_addr_listen_module(family)}, + .tests{{ + .description = "Two soft permits allow listen", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_listen_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// Second program REJECT -> listen denied. +TEMPLATE_TEST_CASE( + "sock_addr_listen_multi_second_rejects", "[sock_addr_tests][multi_attach]", tcp_v4_params, tcp_v6_params) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_listen_multi_second_rejects", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_listen_module(family), sock_addr_listen_module(family)}, + .tests{{ + .description = "Second program rejects after first soft permit", + .expected_listen_error = WSAEACCES, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_listen_verdict(1, BPF_SOCK_ADDR_VERDICT_REJECT), + }, + }}, + }); +} + +// First program REJECT (short-circuit) -> listen denied. +TEMPLATE_TEST_CASE( + "sock_addr_listen_multi_first_rejects", "[sock_addr_tests][multi_attach]", tcp_v4_params, tcp_v6_params) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_listen_multi_first_rejects", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_listen_module(family), sock_addr_listen_module(family)}, + .tests{{ + .description = "First program rejects, short-circuits accumulation", + .expected_listen_error = WSAEACCES, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_listen_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }}, + }); +} + +// Multi-program with WFP block: HARD permit overrides WFP -> listen allowed. +TEMPLATE_TEST_CASE( + "sock_addr_listen_multi_hard_overrides_wfp", "[sock_addr_tests][multi_attach]", tcp_v4_params, tcp_v6_params) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_listen_multi_hard_overrides_wfp", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_listen_module(family), sock_addr_listen_module(family)}, + .wfp_filters{{ + .layer = sock_addr_listen_wfp_layer(family), + .local_port = static_cast(SOCKET_TEST_PORT), + }}, + .tests{{ + .description = "Hard permit from second program overrides WFP block", + .expected_result = connection_test_result::allow, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_listen_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + }, + }}, + }); +} + +// REJECT + HARD -> REJECT wins (higher priority). +TEMPLATE_TEST_CASE( + "sock_addr_listen_multi_reject_beats_hard", "[sock_addr_tests][multi_attach]", tcp_v4_params, tcp_v6_params) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_listen_multi_reject_beats_hard", + .address_family = family, + .protocol = protocol, + .modules{sock_addr_listen_module(family), sock_addr_listen_module(family)}, + .tests{{ + .description = "Reject from first program overrides hard permit from second", + .expected_listen_error = WSAEACCES, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_listen_verdict(1, BPF_SOCK_ADDR_VERDICT_PROCEED_HARD), + }, + }}, + }); +} + +// Detach middle program: 3 programs with middle=REJECT -> denied. Detach middle -> allowed. +TEMPLATE_TEST_CASE( + "sock_addr_listen_multi_detach_middle", "[sock_addr_tests][multi_attach]", tcp_v4_params, tcp_v6_params) +{ + constexpr ADDRESS_FAMILY family = std::tuple_element_t<0, TestType>::value; + constexpr IPPROTO protocol = std::tuple_element_t<1, TestType>::value; + execute_connection_test({ + .name = "sock_addr_listen_multi_detach_middle", + .address_family = family, + .protocol = protocol, + .modules{ + sock_addr_listen_module(family), + sock_addr_listen_module(family), + sock_addr_listen_module(family), + }, + .tests{ + { + .description = "With middle program rejecting, listen is denied", + .expected_listen_error = WSAEACCES, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_listen_verdict(1, BPF_SOCK_ADDR_VERDICT_REJECT), + sock_addr_listen_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + { + .description = "After detaching middle program, listen succeeds", + .expected_result = connection_test_result::allow, + .before{{.action = attachment_action::do_detach, .target = {.module_index = 1}}}, + .program_policies{ + sock_addr_listen_verdict(0, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + sock_addr_listen_verdict(2, BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT), + }, + }, + }, + }); +} + TEST_CASE("attach_sock_addr_programs", "[sock_addr_tests]") { bpf_prog_info program_info = {}; diff --git a/tests/unit/libbpf_test.cpp b/tests/unit/libbpf_test.cpp index ddbe7a0d5b..c02634bacc 100644 --- a/tests/unit/libbpf_test.cpp +++ b/tests/unit/libbpf_test.cpp @@ -2950,6 +2950,12 @@ _test_bpf_prog_attach(ebpf_execution_type_t execution_type) // Verify we can't use an illegal program fd. REQUIRE(bpf_prog_attach(ebpf_fd_invalid, 0, BPF_CGROUP_INET4_CONNECT, 0) == -EBADF); + // The legacy bind hook has no per-target attach parameter, so a non-zero + // attachable_fd must be rejected rather than silently treated as the + // wildcard attachment. + REQUIRE(bpf_prog_attach(program_fd, 1, BPF_ATTACH_TYPE_BIND, 0) == -EINVAL); + REQUIRE(bpf_prog_detach2(program_fd, 1, BPF_ATTACH_TYPE_BIND) == -EINVAL); + // TODO (issue #1028): Currently one can pass an invalid attachable fd and bpf_prog_attach // will succeed because it's temporarily just treated as a compartment id. The following // should instead return errors.