diff --git a/external/usersim b/external/usersim index 7b5f0d775f..d47ad6d2d9 160000 --- a/external/usersim +++ b/external/usersim @@ -1 +1 @@ -Subproject commit 7b5f0d775fe3292c2c85a693bbc4cec7d72a4fab +Subproject commit d47ad6d2d943652ec9c9f01d9e8a5f711fbb3c4f diff --git a/include/ebpf_nethooks.h b/include/ebpf_nethooks.h index 525bce2b5d..975376a8f4 100644 --- a/include/ebpf_nethooks.h +++ b/include/ebpf_nethooks.h @@ -149,7 +149,9 @@ typedef enum _bpf_sock_op_type /** @brief Indicates when a passive (inbound) connection is established. **/ BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB, /** @brief Indicates when a connection is deleted. **/ - BPF_SOCK_OPS_CONNECTION_DELETED_CB + BPF_SOCK_OPS_CONNECTION_DELETED_CB, + /** @brief Indicates when a connection transitions to the listening state. **/ + BPF_SOCK_OPS_LISTEN_CB, } bpf_sock_op_type_t; typedef struct _bpf_sock_ops diff --git a/netebpfext/net_ebpf_ext.c b/netebpfext/net_ebpf_ext.c index f24a6e97ed..f6252e8175 100644 --- a/netebpfext/net_ebpf_ext.c +++ b/netebpfext/net_ebpf_ext.c @@ -23,7 +23,7 @@ #include "net_ebpf_ext_sock_ops.h" #include "net_ebpf_ext_xdp.h" -#define SECONDSTO100NS(x) ((x)*10000000) +#define SECONDSTO100NS(x) ((x) * 10000000) #define SUBLAYER_WEIGHT_MAXIMUM 0xFFFF // Globals. @@ -223,9 +223,31 @@ static net_ebpf_ext_wfp_callout_state_t _net_ebpf_ext_wfp_callout_states[] = { net_ebpf_extension_sock_ops_flow_established_classify, net_ebpf_ext_filter_change_notify, net_ebpf_extension_sock_ops_flow_delete, - L"ALE Flow Established Callout v4", + L"ALE Flow Established Callout v6", L"ALE Flow Established callout for eBPF", FWP_ACTION_CALLOUT_TERMINATING, + }, + // EBPF_HOOK_ALE_AUTH_LISTEN_V4 + { + &EBPF_HOOK_ALE_AUTH_LISTEN_V4_CALLOUT, + &FWPM_LAYER_ALE_AUTH_LISTEN_V4, + net_ebpf_extension_sock_ops_listen_classify, + net_ebpf_ext_filter_change_notify, + NULL, // No flow delete callback for listen + L"ALE Auth Listen Callout v4", + L"ALE Auth Listen callout for eBPF", + FWP_ACTION_CALLOUT_TERMINATING, + }, + // EBPF_HOOK_ALE_AUTH_LISTEN_V6 + { + &EBPF_HOOK_ALE_AUTH_LISTEN_V6_CALLOUT, + &FWPM_LAYER_ALE_AUTH_LISTEN_V6, + net_ebpf_extension_sock_ops_listen_classify, + net_ebpf_ext_filter_change_notify, + NULL, // No flow delete callback for listen + L"ALE Auth Listen Callout v6", + L"ALE Auth Listen callout for eBPF", + FWP_ACTION_CALLOUT_TERMINATING, }}; // WFP globals @@ -365,6 +387,12 @@ net_ebpf_extension_get_hook_id_from_wfp_layer_id(uint16_t wfp_layer_id) case FWPS_LAYER_ALE_FLOW_ESTABLISHED_V6: hook_id = EBPF_HOOK_ALE_FLOW_ESTABLISHED_V6; break; + case FWPS_LAYER_ALE_AUTH_LISTEN_V4: + hook_id = EBPF_HOOK_ALE_AUTH_LISTEN_V4; + break; + case FWPS_LAYER_ALE_AUTH_LISTEN_V6: + hook_id = EBPF_HOOK_ALE_AUTH_LISTEN_V6; + break; case FWPS_LAYER_ALE_CONNECT_REDIRECT_V4: hook_id = EBPF_HOOK_ALE_CONNECT_REDIRECT_V4; break; diff --git a/netebpfext/net_ebpf_ext.h b/netebpfext/net_ebpf_ext.h index 60e9018262..d0d4fbbc16 100644 --- a/netebpfext/net_ebpf_ext.h +++ b/netebpfext/net_ebpf_ext.h @@ -249,7 +249,9 @@ typedef enum _net_ebpf_extension_hook_id EBPF_HOOK_ALE_AUTH_RECV_ACCEPT_V4, // 10 EBPF_HOOK_ALE_AUTH_RECV_ACCEPT_V6, EBPF_HOOK_ALE_FLOW_ESTABLISHED_V4, - EBPF_HOOK_ALE_FLOW_ESTABLISHED_V6 + EBPF_HOOK_ALE_FLOW_ESTABLISHED_V6, + EBPF_HOOK_ALE_AUTH_LISTEN_V4, + EBPF_HOOK_ALE_AUTH_LISTEN_V6 } net_ebpf_extension_hook_id_t; /** diff --git a/netebpfext/net_ebpf_ext_sock_ops.c b/netebpfext/net_ebpf_ext_sock_ops.c index ddc382349e..19e3176c39 100644 --- a/netebpfext/net_ebpf_ext_sock_ops.c +++ b/netebpfext/net_ebpf_ext_sock_ops.c @@ -52,7 +52,17 @@ const net_ebpf_extension_wfp_filter_parameters_t _net_ebpf_extension_sock_ops_wf NULL, // Default sublayer. &EBPF_HOOK_ALE_FLOW_ESTABLISHED_V6_CALLOUT, L"net eBPF sock_ops hook", - L"net eBPF sock_ops hook WFP filter"}}; + L"net eBPF sock_ops hook WFP filter"}, + {&FWPM_LAYER_ALE_AUTH_LISTEN_V4, + NULL, // Default sublayer. + &EBPF_HOOK_ALE_AUTH_LISTEN_V4_CALLOUT, + L"net eBPF sock_ops listen hook", + L"net eBPF sock_ops listen hook WFP filter"}, + {&FWPM_LAYER_ALE_AUTH_LISTEN_V6, + NULL, // Default sublayer. + &EBPF_HOOK_ALE_AUTH_LISTEN_V6_CALLOUT, + L"net eBPF sock_ops listen hook", + L"net eBPF sock_ops listen hook WFP filter"}}; #define NET_EBPF_SOCK_OPS_FILTER_COUNT EBPF_COUNT_OF(_net_ebpf_extension_sock_ops_wfp_filter_parameters) @@ -386,6 +396,27 @@ wfp_ale_layer_fields_t wfp_flow_established_fields[] = { FWPS_FIELD_ALE_FLOW_ESTABLISHED_V6_COMPARTMENT_ID, FWPS_FIELD_ALE_FLOW_ESTABLISHED_V6_IP_LOCAL_INTERFACE}}; +// WFP layer fields for listen hooks +wfp_ale_layer_fields_t wfp_auth_listen_fields[] = { + // EBPF_HOOK_ALE_AUTH_LISTEN_V4 + {FWPS_FIELD_ALE_AUTH_LISTEN_V4_IP_LOCAL_ADDRESS, + FWPS_FIELD_ALE_AUTH_LISTEN_V4_IP_LOCAL_PORT, + 0, // No remote address for listen + 0, // No remote port for listen + 0, // No protocol field for listen layer + 0, // No direction field for listen (always inbound) + FWPS_FIELD_ALE_AUTH_LISTEN_V4_COMPARTMENT_ID, + FWPS_FIELD_ALE_AUTH_LISTEN_V4_IP_LOCAL_INTERFACE}, + // EBPF_HOOK_ALE_AUTH_LISTEN_V6 + {FWPS_FIELD_ALE_AUTH_LISTEN_V6_IP_LOCAL_ADDRESS, + FWPS_FIELD_ALE_AUTH_LISTEN_V6_IP_LOCAL_PORT, + 0, // No remote address for listen + 0, // No remote port for listen + 0, // No protocol field for listen layer + 0, // No direction field for listen (always inbound) + FWPS_FIELD_ALE_AUTH_LISTEN_V6_COMPARTMENT_ID, + FWPS_FIELD_ALE_AUTH_LISTEN_V6_IP_LOCAL_INTERFACE}}; + static void _net_ebpf_extension_sock_ops_copy_wfp_connection_fields( _In_ const FWPS_INCOMING_VALUES* incoming_fixed_values, @@ -437,6 +468,53 @@ _net_ebpf_extension_sock_ops_copy_wfp_connection_fields( } } +static void +_net_ebpf_extension_sock_ops_copy_wfp_listen_fields( + _In_ const FWPS_INCOMING_VALUES* incoming_fixed_values, + _In_ const FWPS_INCOMING_METADATA_VALUES* incoming_metadata_values, + _Out_ net_ebpf_sock_ops_t* sock_ops_context) +{ + uint16_t wfp_layer_id = incoming_fixed_values->layerId; + net_ebpf_extension_hook_id_t hook_id = net_ebpf_extension_get_hook_id_from_wfp_layer_id(wfp_layer_id); + wfp_ale_layer_fields_t* fields = &wfp_auth_listen_fields[hook_id - EBPF_HOOK_ALE_AUTH_LISTEN_V4]; + bpf_sock_ops_t* sock_ops = &sock_ops_context->context; + + FWPS_INCOMING_VALUE0* incoming_values = incoming_fixed_values->incomingValue; + + // Listen operations are always of type BPF_SOCK_OPS_LISTEN_CB + sock_ops->op = BPF_SOCK_OPS_LISTEN_CB; + + // Copy IP address fields. + if (hook_id == EBPF_HOOK_ALE_AUTH_LISTEN_V4) { + sock_ops->family = AF_INET; + sock_ops->local_ip4 = htonl(incoming_values[fields->local_ip_address_field].value.uint32); + sock_ops->remote_ip4 = 0; // No remote address for listen + } else { + sock_ops->family = AF_INET6; + RtlCopyMemory( + sock_ops->local_ip6, + incoming_values[fields->local_ip_address_field].value.byteArray16, + sizeof(FWP_BYTE_ARRAY16)); + memset(sock_ops->remote_ip6, 0, sizeof(sock_ops->remote_ip6)); // No remote address for listen + } + sock_ops->local_port = htons(incoming_values[fields->local_port_field].value.uint16); + sock_ops->remote_port = 0; // No remote port for listen + sock_ops->protocol = IPPROTO_TCP; // Listen operations are typically TCP + sock_ops->compartment_id = incoming_values[fields->compartment_id_field].value.uint32; + sock_ops->interface_luid = *incoming_values[fields->interface_luid_field].value.uint64; + if (incoming_metadata_values->currentMetadataValues & FWPS_METADATA_FIELD_PROCESS_ID) { + sock_ops_context->process_id = incoming_metadata_values->processId; + } else { + NET_EBPF_EXT_LOG_MESSAGE_UINT64( + NET_EBPF_EXT_TRACELOG_LEVEL_VERBOSE, + NET_EBPF_EXT_TRACELOG_KEYWORD_SOCK_OPS, + "FWPS_METADATA_FIELD_PROCESS_ID not present", + hook_id); + + sock_ops_context->process_id = 0; + } +} + // // WFP callout callback function. // @@ -624,6 +702,100 @@ net_ebpf_extension_sock_ops_flow_delete(uint16_t layer_id, uint32_t callout_id, } } +// +// WFP classifyFn callback function for listen operations. +// +void +net_ebpf_extension_sock_ops_listen_classify( + _In_ const FWPS_INCOMING_VALUES* incoming_fixed_values, + _In_ const FWPS_INCOMING_METADATA_VALUES* incoming_metadata_values, + _Inout_opt_ void* layer_data, + _In_opt_ const void* classify_context, + _In_ const FWPS_FILTER* filter, + uint64_t flow_context, + _Inout_ FWPS_CLASSIFY_OUT* classify_output) +{ + uint32_t result; + net_ebpf_extension_sock_ops_wfp_filter_context_t* filter_context = NULL; + net_ebpf_sock_ops_t sock_ops_context = {0}; + uint32_t client_compartment_id = UNSPECIFIED_COMPARTMENT_ID; + ebpf_result_t program_result; + + UNREFERENCED_PARAMETER(layer_data); + UNREFERENCED_PARAMETER(classify_context); + UNREFERENCED_PARAMETER(flow_context); + + NET_EBPF_EXT_LOG_ENTRY(); + + classify_output->actionType = FWP_ACTION_PERMIT; + + filter_context = (net_ebpf_extension_sock_ops_wfp_filter_context_t*)filter->context; + ASSERT(filter_context != NULL); + if (filter_context == NULL) { + goto Exit; + } + + if (filter_context->base.context_deleting) { + NET_EBPF_EXT_LOG_MESSAGE_NTSTATUS( + NET_EBPF_EXT_TRACELOG_LEVEL_VERBOSE, + NET_EBPF_EXT_TRACELOG_KEYWORD_SOCK_OPS, + "net_ebpf_extension_sock_ops_listen_classify - Client detach detected.", + STATUS_INVALID_PARAMETER); + goto Exit; + } + + // Copy WFP fields for listen operation + _net_ebpf_extension_sock_ops_copy_wfp_listen_fields( + incoming_fixed_values, incoming_metadata_values, &sock_ops_context); + + client_compartment_id = filter_context->compartment_id; + ASSERT( + (client_compartment_id == UNSPECIFIED_COMPARTMENT_ID) || + (client_compartment_id == sock_ops_context.context.compartment_id)); + if (client_compartment_id != UNSPECIFIED_COMPARTMENT_ID && + client_compartment_id != sock_ops_context.context.compartment_id) { + // The client is not interested in this compartment Id. + NET_EBPF_EXT_LOG_MESSAGE_UINT32( + NET_EBPF_EXT_TRACELOG_LEVEL_VERBOSE, + NET_EBPF_EXT_TRACELOG_KEYWORD_SOCK_OPS, + "The sock_ops eBPF program is not interested in this compartmentId", + sock_ops_context.context.compartment_id); + goto Exit; + } + + program_result = net_ebpf_extension_hook_invoke_programs(&sock_ops_context.context, &filter_context->base, &result); + if (program_result == EBPF_OBJECT_NOT_FOUND) { + // No program is attached to this hook. + NET_EBPF_EXT_LOG_MESSAGE( + NET_EBPF_EXT_TRACELOG_LEVEL_WARNING, + NET_EBPF_EXT_TRACELOG_KEYWORD_SOCK_OPS, + "net_ebpf_extension_sock_ops_listen_classify - No attached client."); + goto Exit; + } else if (program_result != EBPF_SUCCESS) { + NET_EBPF_EXT_LOG_MESSAGE_UINT32( + NET_EBPF_EXT_TRACELOG_LEVEL_ERROR, + NET_EBPF_EXT_TRACELOG_KEYWORD_SOCK_OPS, + "net_ebpf_extension_sock_ops_listen_classify - Program invocation failed.", + program_result); + goto Exit; + } + + // For listen operations, the result determines whether to allow or block the listen operation + classify_output->actionType = (result == 0) ? FWP_ACTION_PERMIT : FWP_ACTION_BLOCK; + if (classify_output->actionType == FWP_ACTION_BLOCK) { + classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE; + } + + NET_EBPF_EXT_LOG_MESSAGE_UINT32( + NET_EBPF_EXT_TRACELOG_LEVEL_VERBOSE, + NET_EBPF_EXT_TRACELOG_KEYWORD_SOCK_OPS, + "Listen operation processed, port:", + sock_ops_context.context.local_port); + +Exit: + NET_EBPF_EXT_LOG_EXIT(); +} + static ebpf_result_t _ebpf_sock_ops_context_create( _In_reads_bytes_opt_(data_size_in) const uint8_t* data_in, diff --git a/netebpfext/net_ebpf_ext_sock_ops.h b/netebpfext/net_ebpf_ext_sock_ops.h index 861117db24..485a776cee 100644 --- a/netebpfext/net_ebpf_ext_sock_ops.h +++ b/netebpfext/net_ebpf_ext_sock_ops.h @@ -7,6 +7,7 @@ // Callout GUIDs +// clang-format off // f53b4577-bc47-11ec-9a30-18602489beee DEFINE_GUID( EBPF_HOOK_ALE_FLOW_ESTABLISHED_V4_CALLOUT, @@ -37,6 +38,37 @@ DEFINE_GUID( 0xbe, 0xee); +// f53b4579-bc47-11ec-9a30-18602489beee +DEFINE_GUID( + EBPF_HOOK_ALE_AUTH_LISTEN_V4_CALLOUT, + 0xf53b4579, + 0xbc47, + 0x11ec, + 0x9a, + 0x30, + 0x18, + 0x60, + 0x24, + 0x89, + 0xbe, + 0xee); + +// f53b457a-bc47-11ec-9a30-18602489beee +DEFINE_GUID( + EBPF_HOOK_ALE_AUTH_LISTEN_V6_CALLOUT, + 0xf53b457a, + 0xbc47, + 0x11ec, + 0x9a, + 0x30, + 0x18, + 0x60, + 0x24, + 0x89, + 0xbe, + 0xee); +// clang-format on + /** * @brief WFP classifyFn callback for EBPF_HOOK_ALE_FLOW_ESTABLISHED_V4/6_CALLOUT. */ @@ -56,6 +88,19 @@ net_ebpf_extension_sock_ops_flow_established_classify( void net_ebpf_extension_sock_ops_flow_delete(uint16_t layer_id, uint32_t callout_id, uint64_t flow_context); +/** + * @brief WFP classifyFn callback for EBPF_HOOK_ALE_AUTH_LISTEN_V4/6. + */ +void +net_ebpf_extension_sock_ops_listen_classify( + _In_ const FWPS_INCOMING_VALUES* incoming_fixed_values, + _In_ const FWPS_INCOMING_METADATA_VALUES* incoming_metadata_values, + _Inout_opt_ void* layer_data, + _In_opt_ const void* classify_context, + _In_ const FWPS_FILTER* filter, + uint64_t flow_context, + _Inout_ FWPS_CLASSIFY_OUT* classify_output); + /** * @brief Unregister SOCK_OPS NPI providers. * diff --git a/tests/netebpfext_unit/netebpf_ext_helper.h b/tests/netebpfext_unit/netebpf_ext_helper.h index 3afd726f4a..2a31fe8fb8 100644 --- a/tests/netebpfext_unit/netebpf_ext_helper.h +++ b/tests/netebpfext_unit/netebpf_ext_helper.h @@ -98,6 +98,18 @@ typedef class _netebpf_ext_helper usersim_fwp_sock_ops_v4_remove_flow_context(flow_id); } + FWP_ACTION_TYPE + test_sock_ops_listen_v4(_In_ fwp_classify_parameters_t* parameters) + { + return usersim_fwp_sock_ops_listen_v4(parameters); + } + + FWP_ACTION_TYPE + test_sock_ops_listen_v6(_In_ fwp_classify_parameters_t* parameters) + { + return usersim_fwp_sock_ops_listen_v6(parameters); + } + private: bool trace_initiated = false; bool ndis_handle_initialized = false; diff --git a/tests/netebpfext_unit/netebpfext_unit.cpp b/tests/netebpfext_unit/netebpfext_unit.cpp index ddeb856a51..c91578d3db 100644 --- a/tests/netebpfext_unit/netebpfext_unit.cpp +++ b/tests/netebpfext_unit/netebpfext_unit.cpp @@ -1073,6 +1073,7 @@ TEST_CASE("sock_ops_context", "[netebpfext]") REQUIRE(output_context.compartment_id == 0x12345679); REQUIRE(output_context.interface_luid == 0x1234567890abcdee); } + // Thread function for concurrent sock_ops invocation. void sock_ops_thread_function( @@ -1227,4 +1228,147 @@ TEST_CASE("sock_ops_invoke_concurrent2", "[netebpfext_concurrent]") REQUIRE(failure_count == 0); } + +TEST_CASE("sock_ops_listen_invoke", "[netebpfext]") +{ + ebpf_extension_data_t npi_specific_characteristics = { + .header = EBPF_ATTACH_CLIENT_DATA_HEADER_VERSION, + }; + test_sock_ops_client_context_header_t client_context_header = {0}; + test_sock_ops_client_context_t* client_context = &client_context_header.context; + fwp_classify_parameters_t parameters = {}; + + netebpf_ext_helper_t helper( + &npi_specific_characteristics, + (_ebpf_extension_dispatch_function)netebpfext_unit_invoke_sock_ops_program, + (netebpfext_helper_base_client_context_t*)client_context); + + netebpfext_initialize_fwp_classify_parameters(¶meters); + + // Test listen operations that should be allowed. + client_context->sock_ops_action = 0; + + FWP_ACTION_TYPE result = helper.test_sock_ops_listen_v4(¶meters); + REQUIRE(result == FWP_ACTION_PERMIT); + + result = helper.test_sock_ops_listen_v6(¶meters); + REQUIRE(result == FWP_ACTION_PERMIT); + + // Test listen operations that should be blocked. + client_context->sock_ops_action = -1; + + result = helper.test_sock_ops_listen_v4(¶meters); + REQUIRE(result == FWP_ACTION_BLOCK); + + result = helper.test_sock_ops_listen_v6(¶meters); + REQUIRE(result == FWP_ACTION_BLOCK); +} + +TEST_CASE("sock_ops_listen_context", "[netebpfext]") +{ + netebpf_ext_helper_t helper; + auto sock_ops_program_data = helper.get_program_info_provider_data(EBPF_PROGRAM_TYPE_SOCK_OPS); + REQUIRE(sock_ops_program_data != nullptr); + + size_t output_data_size = 0; + + // Test BPF_SOCK_OPS_LISTEN_CB context for IPv4 + bpf_sock_ops_t input_context_v4 = { + BPF_SOCK_OPS_LISTEN_CB, + AF_INET, + 0x7f000001, // 127.0.0.1 in host byte order + 8080, // Port 8080 + 0, // No remote IP for listen + 0, // No remote port for listen + IPPROTO_TCP, // TCP protocol + 0x12345678, // Compartment ID + 0x1234567890abcdef, // Interface LUID + }; + size_t output_context_size = sizeof(bpf_sock_ops_t); + bpf_sock_ops_t output_context = {}; + bpf_sock_ops_t* sock_ops_context = nullptr; + + REQUIRE( + sock_ops_program_data->context_create( + nullptr, 0, (const uint8_t*)&input_context_v4, sizeof(input_context_v4), (void**)&sock_ops_context) == 0); + + // Verify the context was created with the correct listen operation type + REQUIRE(sock_ops_context->op == BPF_SOCK_OPS_LISTEN_CB); + REQUIRE(sock_ops_context->family == AF_INET); + REQUIRE(sock_ops_context->local_ip4 == 0x7f000001); + REQUIRE(sock_ops_context->local_port == 8080); + REQUIRE(sock_ops_context->remote_ip4 == 0); // No remote address for listen + REQUIRE(sock_ops_context->remote_port == 0); // No remote port for listen + REQUIRE(sock_ops_context->protocol == IPPROTO_TCP); + + // Modify the context to test updates + sock_ops_context->local_port = 9090; + sock_ops_context->compartment_id = 0x87654321; + + output_context_size = sizeof(bpf_sock_ops_t); + output_data_size = 0; + + sock_ops_program_data->context_destroy( + sock_ops_context, nullptr, &output_data_size, (uint8_t*)&output_context, &output_context_size); + + REQUIRE(output_data_size == 0); + REQUIRE(output_context_size == sizeof(bpf_sock_ops_t)); + REQUIRE(output_context.op == BPF_SOCK_OPS_LISTEN_CB); + REQUIRE(output_context.family == AF_INET); + REQUIRE(output_context.local_ip4 == 0x7f000001); + REQUIRE(output_context.local_port == 9090); // Modified value + REQUIRE(output_context.remote_ip4 == 0); + REQUIRE(output_context.remote_port == 0); + REQUIRE(output_context.protocol == IPPROTO_TCP); + REQUIRE(output_context.compartment_id == 0x87654321); // Modified value + + // Test BPF_SOCK_OPS_LISTEN_CB context for IPv6 + uint32_t local_ipv6[4] = {0x00000000, 0x00000000, 0x00000000, 0x00000001}; // ::1 localhost IPv6 + uint32_t remote_ipv6[4] = {0x00000000, 0x00000000, 0x00000000, 0x00000000}; // no remote for listen + bpf_sock_ops_t input_context_v6{}; + input_context_v6.op = BPF_SOCK_OPS_LISTEN_CB; + input_context_v6.family = AF_INET6; + memcpy(input_context_v6.local_ip6, local_ipv6, sizeof(local_ipv6)); + input_context_v6.local_port = 443; + memcpy(input_context_v6.remote_ip6, remote_ipv6, sizeof(remote_ipv6)); + input_context_v6.remote_port = 0; // no remote port for listen + input_context_v6.protocol = IPPROTO_TCP; + input_context_v6.compartment_id = 0x11223344; + input_context_v6.interface_luid = 0xfedcba0987654321; + + sock_ops_context = nullptr; + REQUIRE( + sock_ops_program_data->context_create( + nullptr, 0, (const uint8_t*)&input_context_v6, sizeof(input_context_v6), (void**)&sock_ops_context) == 0); + + // Verify IPv6 listen context + REQUIRE(sock_ops_context->op == BPF_SOCK_OPS_LISTEN_CB); + REQUIRE(sock_ops_context->family == AF_INET6); + REQUIRE(sock_ops_context->local_ip6[0] == 0); + REQUIRE(sock_ops_context->local_ip6[1] == 0); + REQUIRE(sock_ops_context->local_ip6[2] == 0); + REQUIRE(sock_ops_context->local_ip6[3] == 1); + REQUIRE(sock_ops_context->local_port == 443); + REQUIRE(sock_ops_context->remote_ip6[0] == 0); + REQUIRE(sock_ops_context->remote_ip6[1] == 0); + REQUIRE(sock_ops_context->remote_ip6[2] == 0); + REQUIRE(sock_ops_context->remote_ip6[3] == 0); + REQUIRE(sock_ops_context->remote_port == 0); + REQUIRE(sock_ops_context->protocol == IPPROTO_TCP); + + output_context_size = sizeof(bpf_sock_ops_t); + output_data_size = 0; + memset(&output_context, 0, sizeof(output_context)); + + sock_ops_program_data->context_destroy( + sock_ops_context, nullptr, &output_data_size, (uint8_t*)&output_context, &output_context_size); + + REQUIRE(output_context.op == BPF_SOCK_OPS_LISTEN_CB); + REQUIRE(output_context.family == AF_INET6); + REQUIRE(output_context.local_ip6[3] == 1); + REQUIRE(output_context.local_port == 443); + REQUIRE(output_context.remote_port == 0); + REQUIRE(output_context.protocol == IPPROTO_TCP); +} + #pragma endregion sock_ops diff --git a/tests/sample/sockops.c b/tests/sample/sockops.c index e2fcf20a97..9eda55af72 100644 --- a/tests/sample/sockops.c +++ b/tests/sample/sockops.c @@ -93,6 +93,9 @@ connection_monitor(bpf_sock_ops_t* ctx) outbound = false; connected = false; break; + case BPF_SOCK_OPS_LISTEN_CB: + outbound = false; + connected = false; default: result = -1; } diff --git a/tests/socket/socket_tests.cpp b/tests/socket/socket_tests.cpp index 5dbbf0bc12..ded62cee86 100644 --- a/tests/socket/socket_tests.cpp +++ b/tests/socket/socket_tests.cpp @@ -481,6 +481,233 @@ TEST_CASE("attach_sockops_programs", "[sock_ops_tests]") SAFE_REQUIRE(result == 0); } +void +listen_monitor_test(ADDRESS_FAMILY address_family, uint16_t listen_port, uint32_t protocol) +{ + native_module_helper_t helper; + helper.initialize("sockops", _is_main_thread); + struct bpf_object* object = bpf_object__open(helper.get_file_name().c_str()); + bpf_object_ptr object_ptr(object); + + SAFE_REQUIRE(object != nullptr); + // Load the programs. + SAFE_REQUIRE(bpf_object__load(object) == 0); + + // Ring buffer event callback context. + std::unique_ptr context = std::make_unique(); + context->test_event_count = 1; // Expect one listen event + + bpf_program* _program = bpf_object__find_program_by_name(object, "connection_monitor"); + SAFE_REQUIRE(_program != nullptr); + + uint64_t process_id = get_current_pid_tgid(); + // Ignore the thread Id. + process_id >>= 32; + + // Create expected audit entry for listen operation + connection_tuple_t tuple{}; + if (address_family == AF_INET) { + tuple.local_ip.ipv4 = htonl(INADDR_LOOPBACK); + } else { + memcpy(tuple.local_ip.ipv6, &in6addr_loopback, sizeof(tuple.local_ip.ipv6)); + } + tuple.local_port = htons(listen_port); + tuple.remote_port = 0; // No remote port for listen + tuple.protocol = protocol; + NET_LUID net_luid = {}; + net_luid.Info.IfType = IF_TYPE_SOFTWARE_LOOPBACK; + tuple.interface_luid = net_luid.Value; + + std::vector> audit_entry_list; + audit_entry_t audit_entry = {0}; + + // Listen operation + audit_entry.tuple = tuple; + audit_entry.process_id = process_id; + audit_entry.connected = true; // Listen is considered a connection state change + audit_entry.outbound = false; // Listen is always inbound-facing + char* p = reinterpret_cast(&audit_entry); + audit_entry_list.push_back(std::vector(p, p + sizeof(audit_entry_t))); + + context->records = &audit_entry_list; + + // Get the std::future from the promise field in ring buffer event context + auto ring_buffer_event_callback = context->ring_buffer_event_promise.get_future(); + + // Create a new ring buffer manager and subscribe to ring buffer events. + bpf_map* ring_buffer_map = bpf_object__find_map_by_name(object, "audit_map"); + SAFE_REQUIRE(ring_buffer_map != nullptr); + context->ring_buffer = ring_buffer__new( + bpf_map__fd(ring_buffer_map), (ring_buffer_sample_fn)ring_buffer_test_event_handler, context.get(), nullptr); + SAFE_REQUIRE(context->ring_buffer != nullptr); + + bpf_map* connection_map = bpf_object__find_map_by_name(object, "connection_map"); + SAFE_REQUIRE(connection_map != nullptr); + + // Update connection map to allow listen operation + uint32_t verdict = BPF_SOCK_ADDR_VERDICT_PROCEED; + SAFE_REQUIRE(bpf_map_update_elem(bpf_map__fd(connection_map), &tuple, &verdict, EBPF_ANY) == 0); + + // Attach the sockops program. + int result = bpf_prog_attach(bpf_program__fd(const_cast(_program)), 0, BPF_CGROUP_SOCK_OPS, 0); + SAFE_REQUIRE(result == 0); + + // Create a listening socket to trigger the listen hook + SOCKET listen_socket = INVALID_SOCKET; + if (protocol == IPPROTO_TCP) { + listen_socket = socket(address_family, SOCK_STREAM, IPPROTO_TCP); + } else { + listen_socket = socket(address_family, SOCK_DGRAM, IPPROTO_UDP); + } + SAFE_REQUIRE(listen_socket != INVALID_SOCKET); + + // Bind to the specified port + sockaddr_storage bind_address{}; + int addr_size = 0; + if (address_family == AF_INET) { + sockaddr_in* addr_v4 = reinterpret_cast(&bind_address); + addr_v4->sin_family = AF_INET; + addr_v4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr_v4->sin_port = htons(listen_port); + addr_size = sizeof(sockaddr_in); + } else { + sockaddr_in6* addr_v6 = reinterpret_cast(&bind_address); + addr_v6->sin6_family = AF_INET6; + addr_v6->sin6_addr = in6addr_loopback; + addr_v6->sin6_port = htons(listen_port); + addr_size = sizeof(sockaddr_in6); + } + + int bind_result = bind(listen_socket, reinterpret_cast(&bind_address), addr_size); + SAFE_REQUIRE(bind_result == 0); + + // For TCP, call listen() to trigger the listen hook + if (protocol == IPPROTO_TCP) { + int listen_result = listen(listen_socket, 5); + SAFE_REQUIRE(listen_result == 0); + } + + // Wait for event handler getting notifications for the listen audit event + SAFE_REQUIRE(ring_buffer_event_callback.wait_for(1s) == std::future_status::ready); + + // Mark the event context as canceled, such that the event callback stops processing events. + context->canceled = true; + + // Unsubscribe. + context->unsubscribe(); + + // Clean up the socket + closesocket(listen_socket); +} + +TEST_CASE("listen_monitor_test_tcp_v4", "[sock_ops_tests]") +{ + listen_monitor_test(AF_INET, SOCKET_TEST_PORT + 100, IPPROTO_TCP); +} + +TEST_CASE("listen_monitor_test_tcp_v6", "[sock_ops_tests]") +{ + listen_monitor_test(AF_INET6, SOCKET_TEST_PORT + 101, IPPROTO_TCP); +} + +TEST_CASE("listen_monitor_test_udp_v4", "[sock_ops_tests]") +{ + listen_monitor_test(AF_INET, SOCKET_TEST_PORT + 102, IPPROTO_UDP); +} + +TEST_CASE("listen_monitor_test_udp_v6", "[sock_ops_tests]") +{ + listen_monitor_test(AF_INET6, SOCKET_TEST_PORT + 103, IPPROTO_UDP); +} + +TEST_CASE("listen_hook_enforcement_test", "[sock_ops_tests]") +{ + native_module_helper_t helper; + helper.initialize("sockops", _is_main_thread); + struct bpf_object* object = bpf_object__open(helper.get_file_name().c_str()); + bpf_object_ptr object_ptr(object); + + SAFE_REQUIRE(object != nullptr); + // Load the programs. + SAFE_REQUIRE(bpf_object__load(object) == 0); + + bpf_program* _program = bpf_object__find_program_by_name(object, "connection_monitor"); + SAFE_REQUIRE(_program != nullptr); + + bpf_map* connection_map = bpf_object__find_map_by_name(object, "connection_map"); + SAFE_REQUIRE(connection_map != nullptr); + + // Create a tuple for the listen operation that will be blocked + connection_tuple_t tuple{}; + tuple.local_ip.ipv4 = htonl(INADDR_LOOPBACK); + tuple.local_port = htons(SOCKET_TEST_PORT + 200); + tuple.remote_port = 0; // No remote port for listen + tuple.protocol = IPPROTO_TCP; + NET_LUID net_luid = {}; + net_luid.Info.IfType = IF_TYPE_SOFTWARE_LOOPBACK; + tuple.interface_luid = net_luid.Value; + + // Set the verdict to block the listen operation + uint32_t verdict = BPF_SOCK_ADDR_VERDICT_REJECT; + SAFE_REQUIRE(bpf_map_update_elem(bpf_map__fd(connection_map), &tuple, &verdict, EBPF_ANY) == 0); + + // Attach the sockops program. + int result = bpf_prog_attach(bpf_program__fd(const_cast(_program)), 0, BPF_CGROUP_SOCK_OPS, 0); + SAFE_REQUIRE(result == 0); + + // Create a listening socket + SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SAFE_REQUIRE(listen_socket != INVALID_SOCKET); + + // Bind to the specified port + sockaddr_in bind_address{}; + bind_address.sin_family = AF_INET; + bind_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + bind_address.sin_port = htons(SOCKET_TEST_PORT + 200); + + int bind_result = bind(listen_socket, reinterpret_cast(&bind_address), sizeof(bind_address)); + SAFE_REQUIRE(bind_result == 0); + + // Try to listen - this should be blocked by the eBPF program + int listen_result = listen(listen_socket, 5); + + // The listen should fail because the eBPF program blocked it + // Note: The exact error code may vary depending on the WFP implementation + SAFE_REQUIRE(listen_result != 0); + SAFE_REQUIRE(WSAGetLastError() != 0); + + // Clean up the socket + closesocket(listen_socket); + + // Now test with a permitted listen operation + connection_tuple_t permitted_tuple{}; + permitted_tuple.local_ip.ipv4 = htonl(INADDR_LOOPBACK); + permitted_tuple.local_port = htons(SOCKET_TEST_PORT + 201); + permitted_tuple.remote_port = 0; + permitted_tuple.protocol = IPPROTO_TCP; + permitted_tuple.interface_luid = net_luid.Value; + + // Set the verdict to allow the listen operation + verdict = BPF_SOCK_ADDR_VERDICT_PROCEED; + SAFE_REQUIRE(bpf_map_update_elem(bpf_map__fd(connection_map), &permitted_tuple, &verdict, EBPF_ANY) == 0); + + // Create another listening socket + SOCKET permitted_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SAFE_REQUIRE(permitted_socket != INVALID_SOCKET); + + // Bind to the permitted port + bind_address.sin_port = htons(SOCKET_TEST_PORT + 201); + bind_result = bind(permitted_socket, reinterpret_cast(&bind_address), sizeof(bind_address)); + SAFE_REQUIRE(bind_result == 0); + + // This listen should succeed + listen_result = listen(permitted_socket, 5); + SAFE_REQUIRE(listen_result == 0); + + // Clean up the socket + closesocket(permitted_socket); +} + // This function populates map polcies for multi-attach tests. // It assumes that the destination and proxy are loopback addresses. static void