diff --git a/components/micropython/modfirewall.c b/components/micropython/modfirewall.c index 07ade3e82..68022bc00 100644 --- a/components/micropython/modfirewall.c +++ b/components/micropython/modfirewall.c @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "mpfirewallport.h" @@ -509,6 +511,65 @@ static mp_obj_t rule_get_nth(mp_obj_t interface_idx_in, mp_obj_t protocol_in, mp static MP_DEFINE_CONST_FUN_OBJ_3(rule_get_nth_obj, rule_get_nth); +/* NAT API functions */ + +/* nat_set_enabled(interface, protocol, enabled) — PPC call to TX virtualizer only */ +static mp_obj_t nat_set_enabled(mp_obj_t interface_idx_in, mp_obj_t protocol_in, mp_obj_t enabled_in) +{ + uint8_t interface_idx = mp_obj_get_int(interface_idx_in); + if (!check_interface_index(interface_idx)) { + return mp_const_none; + } + + uint8_t protocol = mp_obj_get_int(protocol_in); + bool enabled = mp_obj_is_true(enabled_in); + + // PPC to the Rx virtualiser, who then PPCs to the Tx virtualiser, all three components update their local copy. + + for (uint8_t i = 0; i < fw_config.interfaces[interface_idx].num_nat_configs; i++) { + if (fw_config.interfaces[interface_idx].nat_configs[i].protocol == protocol) { + microkit_mr_set(NAT_SET_ENABLED_ARG_ENABLED, (seL4_Word)enabled); + (void)microkit_ppcall(fw_config.interfaces[interface_idx].nat_configs[i].webserver_ch, + microkit_msginfo_new(NAT_SET_ENABLED, NAT_SET_ENABLED_NUM_ARGS)); + fw_nat_err_t err = (fw_nat_err_t)microkit_mr_get(NAT_RET_ERR); + if (err != NAT_ERR_OKAY) { + raise_error(OS_ERR_INTERNAL_ERROR); + return mp_const_none; + } + return mp_const_none; + } + } + + raise_error(OS_ERR_INVALID_PROTOCOL); + return mp_const_none; +} + +static MP_DEFINE_CONST_FUN_OBJ_3(nat_set_enabled_obj, nat_set_enabled); + +/* nat_get_enabled(interface, protocol) — direct read from port table shared memory */ +static mp_obj_t nat_get_enabled(mp_obj_t interface_idx_in, mp_obj_t protocol_in) +{ + uint8_t interface_idx = mp_obj_get_int(interface_idx_in); + if (interface_idx >= fw_config.num_interfaces) { + return mp_const_false; + } + + uint8_t protocol = mp_obj_get_int(protocol_in); + + for (uint8_t i = 0; i < fw_config.interfaces[interface_idx].num_nat_configs; i++) { + if (fw_config.interfaces[interface_idx].nat_configs[i].protocol == protocol) { + fw_nat_port_table_t *table = (fw_nat_port_table_t *) + fw_config.interfaces[interface_idx].nat_configs[i].port_table.vaddr; + return mp_obj_new_bool(table->nat_enabled); + } + } + + /* NAT not configured for this interface/protocol — not an error, just disabled */ + return mp_const_false; +} + +static MP_DEFINE_CONST_FUN_OBJ_2(nat_get_enabled_obj, nat_get_enabled); + static const mp_rom_map_elem_t lions_firewall_module_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_lions_firewall) }, { MP_ROM_QSTR(MP_QSTR_interface_ip_get), MP_ROM_PTR(&interface_get_ip_obj) }, @@ -527,6 +588,8 @@ static const mp_rom_map_elem_t lions_firewall_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_rule_count), MP_ROM_PTR(&rule_count_obj) }, { MP_ROM_QSTR(MP_QSTR_filter_get_default_action), MP_ROM_PTR(&filter_get_default_action_obj) }, { MP_ROM_QSTR(MP_QSTR_filter_set_default_action), MP_ROM_PTR(&filter_set_default_action_obj) }, + { MP_ROM_QSTR(MP_QSTR_nat_set_enabled), MP_ROM_PTR(&nat_set_enabled_obj) }, + { MP_ROM_QSTR(MP_QSTR_nat_get_enabled), MP_ROM_PTR(&nat_get_enabled_obj) }, }; static MP_DEFINE_CONST_DICT(lions_firewall_module_globals, lions_firewall_module_globals_table); diff --git a/components/micropython/mpfirewallport.h b/components/micropython/mpfirewallport.h index 4d9e50ba6..640d32df2 100644 --- a/components/micropython/mpfirewallport.h +++ b/components/micropython/mpfirewallport.h @@ -26,6 +26,7 @@ extern fw_webserver_config_t fw_config; typedef struct fw_webserver_interface_state { fw_filter_state_t filter_states[FW_MAX_FILTERS]; bool ping_enabled; + bool nat_enabled; } fw_webserver_interface_state_t; extern fw_webserver_interface_state_t fw_interface_state[FW_MAX_INTERFACES]; diff --git a/examples/firewall/meta.py b/examples/firewall/meta.py index 04634e390..82aa78eaf 100644 --- a/examples/firewall/meta.py +++ b/examples/firewall/meta.py @@ -33,6 +33,7 @@ arp_eth_opcode_request, arp_eth_opcode_response, eththype_ip, + nat_port_table_region, ) from pyfw.component_fw_interface import FirewallInterface @@ -90,10 +91,70 @@ def generate(sdf_file: str, dtb: DeviceTree) -> None: if not path.isdir(iface.out_dir): assert subprocess.run(["mkdir", iface.out_dir]).returncode == 0 + # Create shared port tables for each interface (shared between RX and TX) + # so DNAT (RX) can find mappings created by SNAT (TX) + tcp_port_tables = {} + udp_port_tables = {} + + for iface in fw_interfaces: + tcp_port_tables[iface.index] = FirewallMemoryRegion( + f"nat_port_table_iface{iface.index}_tcp", + nat_port_table_region.region_size + ) + udp_port_tables[iface.index] = FirewallMemoryRegion( + f"nat_port_table_iface{iface.index}_udp", + nat_port_table_region.region_size + ) + router = Router() webserver = Webserver() icmp_module = IcmpModule() + # Create PPC channels from webserver to each virtualizer per protocol/interface, + # then wire them into the NAT configs so the webserver can enable/disable NAT via PPC. + for iface in fw_interfaces: + tcp_tx_ch = SDF_Channel(webserver.pd, iface.tx_virtualiser.pd, pp_a=True) + udp_tx_ch = SDF_Channel(webserver.pd, iface.tx_virtualiser.pd, pp_a=True) + BuildConstants.sdf().add_channel(tcp_tx_ch) + BuildConstants.sdf().add_channel(udp_tx_ch) + + # Configure TCP NAT - RX and TX share the same port table + iface.rx_virtualiser.add_nat_config_with_port_table( + protocol=0x06, + base_port=49152, + capacity=512, + port_table_mr=tcp_port_tables[iface.index], + ) + iface.tx_virtualiser.add_nat_config_with_port_table( + protocol=0x06, + base_port=49152, + capacity=512, + port_table_mr=tcp_port_tables[iface.index], + webserver_ch=tcp_tx_ch.pd_b_id, + ) + + # Configure UDP NAT - RX and TX share the same port table + iface.rx_virtualiser.add_nat_config_with_port_table( + protocol=0x11, + base_port=49152, + capacity=512, + port_table_mr=udp_port_tables[iface.index], + ) + iface.tx_virtualiser.add_nat_config_with_port_table( + protocol=0x11, + base_port=49152, + capacity=512, + port_table_mr=udp_port_tables[iface.index], + webserver_ch=udp_tx_ch.pd_b_id, + ) + + # Map RX DMA region for NAT packet modification (DNAT needs write access) + iface.rx_virtualiser.set_nat_dma_region(iface.rx_dma_region) + + # Register PPC channels in the webserver config for this interface/protocol + webserver.add_nat_ppc_channel(0x06, iface.index, tcp_tx_ch.pd_a_id, tcp_port_tables[iface.index]) + webserver.add_nat_ppc_channel(0x11, iface.index, udp_tx_ch.pd_a_id, udp_port_tables[iface.index]) + # Create timer and serial subsystems serial_node = dtb.node(board.serial) assert serial_node is not None diff --git a/examples/firewall/net_components/firewall_network_components.mk b/examples/firewall/net_components/firewall_network_components.mk index 71bee3b3d..762d0cb64 100644 --- a/examples/firewall/net_components/firewall_network_components.mk +++ b/examples/firewall/net_components/firewall_network_components.mk @@ -16,7 +16,7 @@ FIREWALL_NETWORK_IMAGES:= firewall_network_virt_rx.elf firewall_network_virt_tx. firewall_network/net_components/%.o: ${FIREWALL_COMPONENTS}/%.c ${CC} ${CFLAGS} -c -o $@ $< -FIREWALL_NETWORK_COMPONENT_OBJ := $(addprefix firewall_network/net_components/, network_virt_tx.o network_virt_rx.o) +FIREWALL_NETWORK_COMPONENT_OBJ := $(addprefix firewall_network/net_components/, network_virt_tx.o network_virt_rx.o nat_module.o) CHECK_FIREWALL_NETWORK_FLAGS_MD5:=.firewall_network_cflags-$(shell echo -- ${CFLAGS} ${CFLAGS_network} | shasum | sed 's/ *-//') @@ -33,11 +33,16 @@ ${FIREWALL_NETWORK_COMPONENT_OBJ}: |firewall_network/net_components $(SDDF_LIBC_ ${FIREWALL_NETWORK_COMPONENT_OBJ}: ${CHECK_FIREWALL_NETWORK_FLAGS_MD5} ${FIREWALL_NETWORK_COMPONENT_OBJ}: CFLAGS+=${CFLAGS_FIREWALL_NETWORK} -firewall_network/net_components/firewall_network_virt_%.o: ${SDDF}/firewall_network/net_components/virt_%.c |firewall_network/net_components +firewall_network/net_components/firewall_network_virt_%.o: ${FIREWALL_NETWORK_COMPONENTS_DIR}/firewall_network_virt_%.c |firewall_network/net_components ${CC} ${CFLAGS} -c -o $@ $< -%.elf: firewall_network/net_components/%.o |firewall_network/net_components - ${LD} ${LDFLAGS} -o $@ $< ${LIBS} +firewall_network/net_components/nat_module.o: ${FIREWALL_NETWORK_COMPONENTS_DIR}/nat_module.c |firewall_network/net_components + ${CC} ${CFLAGS} -c -o $@ $< + +firewall_network_virt_%.elf: firewall_network/net_components/firewall_network_virt_%.o \ + firewall_network/net_components/nat_module.o \ + | firewall_network/net_components + ${LD} ${LDFLAGS} -o $@ $^ ${LIBS} clean:: ${RM} -f firewall_network_virt_[rt]x.[od] diff --git a/examples/firewall/net_components/firewall_network_virt_rx.c b/examples/firewall/net_components/firewall_network_virt_rx.c index 5d65c8c8a..c43fd9920 100644 --- a/examples/firewall/net_components/firewall_network_virt_rx.c +++ b/examples/firewall/net_components/firewall_network_virt_rx.c @@ -3,20 +3,24 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include +#include #include #include #include #include #include +#include #include #include #include #include #include #include +#include +#include #include +#include __attribute__((__section__(".net_virt_rx_config"))) net_virt_rx_config_t config; __attribute__((__section__(".fw_net_virt_rx_config"))) fw_net_virt_rx_config_t fw_config; @@ -25,6 +29,9 @@ net_queue_handle_t rx_queue_drv; net_queue_handle_t rx_queue_clients[SDDF_NET_MAX_CLIENTS]; fw_queue_t fw_free_clients[FW_MAX_FW_CLIENTS]; +bool nat_enabled; +nat_module_t nat_modules[FW_MAX_FILTERS]; +uint8_t num_nat_modules; /* Boolean to indicate whether a packet has been enqueued into the driver's free queue during notification handling */ static bool notify_drv; @@ -34,22 +41,29 @@ found. ARP requests and responses are handled as a special case. */ static int get_protocol_match(uintptr_t pkt) { uint16_t ethtype = htons(((eth_hdr_t *)pkt)->ethtype); - for (uint8_t client = 0; client < config.num_clients; client++) { + for (uint8_t client = 0; client < config.num_clients; client++) + { /* First check for ethtype match */ - if (fw_config.active_client_ethtypes[client] != ethtype) { + if (fw_config.active_client_ethtypes[client] != ethtype) + { continue; } - if (ethtype == ETH_TYPE_ARP) { + if (ethtype == ETH_TYPE_ARP) + { /* If ARP traffic, check for opcode match */ arp_pkt_t *arp = (arp_pkt_t *)(pkt + ARP_PKT_OFFSET); - if (fw_config.active_client_subtypes[client] == htons(arp->opcode)) { + if (fw_config.active_client_subtypes[client] == htons(arp->opcode)) + { return client; } - } else if (ethtype == ETH_TYPE_IP) { + } + else if (ethtype == ETH_TYPE_IP) + { /* If IPv4 traffic, check for IPv4 protocol match */ ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(pkt + IPV4_HDR_OFFSET); - if (fw_config.active_client_subtypes[client] == ip_hdr->protocol) { + if (fw_config.active_client_subtypes[client] == ip_hdr->protocol) + { return client; } } @@ -61,15 +75,30 @@ static int get_protocol_match(uintptr_t pkt) static void rx_return(void) { bool reprocess = true; - bool notify_clients[SDDF_NET_MAX_CLIENTS] = { false }; - while (reprocess) { - while (!net_queue_empty_active(&rx_queue_drv)) { + bool notify_clients[SDDF_NET_MAX_CLIENTS] = {false}; + while (reprocess) + { + while (!net_queue_empty_active(&rx_queue_drv)) + { net_buff_desc_t buffer; int err = net_dequeue_active(&rx_queue_drv, &buffer); assert(!err); - buffer.io_or_offset = buffer.io_or_offset - config.data.io_addr; - uintptr_t buffer_vaddr = buffer.io_or_offset + (uintptr_t)config.data.region.vaddr; + uintptr_t data_vaddr; + uintptr_t data_io_addr; + if (fw_config.num_nat_configs > 0) + { + data_vaddr = (uintptr_t)fw_config.nat_dma_region.region.vaddr; + data_io_addr = fw_config.nat_dma_region.io_addr; + } + else + { + data_vaddr = (uintptr_t)config.data.region.vaddr; + data_io_addr = config.data.io_addr; + } + + buffer.io_or_offset = buffer.io_or_offset - data_io_addr; + uintptr_t buffer_vaddr = buffer.io_or_offset + data_vaddr; /* Remove additional 4 byte ethernet header from NIC promiscuous mode */ #if !defined(CONFIG_PLAT_QEMU_ARM_VIRT) @@ -86,12 +115,47 @@ static void rx_return(void) // // [1]: https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Instructions/DC-IVAC--Data-or-unified-Cache-line-Invalidate-by-VA-to-PoC cache_clean_and_invalidate(buffer_vaddr, buffer_vaddr + buffer.len); + + /* Apply DNAT translation if enabled */ + if (fw_config.num_nat_configs > 0) + { + uint16_t ethtype = htons(((eth_hdr_t *)buffer_vaddr)->ethtype); + if (ethtype == ETH_TYPE_IP) + { + ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(buffer_vaddr + IPV4_HDR_OFFSET); + fw_nat_err_t nat_result = NAT_ERR_OKAY; + for (int j = 0; j < num_nat_modules; j++) + { + if (nat_modules[j].protocol == ip_hdr->protocol) + { + nat_result = nat_module_translate(&nat_modules[j], buffer_vaddr, &buffer, true); + break; + } + } + + /* Drop packet if NAT translation fails */ + if (nat_result != NAT_ERR_OKAY) + { + sddf_dprintf("VIRT RX LOG, Interface %u: NAT translation failed for protocol %u, dropping packet\n", + fw_config.interface, ip_hdr->protocol); + buffer.io_or_offset = buffer.io_or_offset + config.data.io_addr; + err = net_enqueue_free(&rx_queue_drv, buffer); + assert(!err); + notify_drv = true; + continue; + } + } + } + int client = get_protocol_match(buffer_vaddr); - if (client >= 0) { + if (client >= 0) + { err = net_enqueue_active(&rx_queue_clients[client], buffer); assert(!err); notify_clients[client] = true; - } else { + } + else + { buffer.io_or_offset = buffer.io_or_offset + config.data.io_addr; err = net_enqueue_free(&rx_queue_drv, buffer); assert(!err); @@ -102,14 +166,17 @@ static void rx_return(void) net_request_signal_active(&rx_queue_drv); reprocess = false; - if (!net_queue_empty_active(&rx_queue_drv)) { + if (!net_queue_empty_active(&rx_queue_drv)) + { net_cancel_signal_active(&rx_queue_drv); reprocess = true; } } - for (int client = 0; client < config.num_clients; client++) { - if (notify_clients[client] && net_require_signal_active(&rx_queue_clients[client])) { + for (int client = 0; client < config.num_clients; client++) + { + if (notify_clients[client] && net_require_signal_active(&rx_queue_clients[client])) + { net_cancel_signal_active(&rx_queue_clients[client]); microkit_notify(config.clients[client].conn.id); } @@ -118,15 +185,17 @@ static void rx_return(void) static void rx_provide(void) { - for (int client = 0; client < config.num_clients; client++) { + for (int client = 0; client < config.num_clients; client++) + { bool reprocess = true; - while (reprocess) { - while (!net_queue_empty_free(&rx_queue_clients[client])) { + while (reprocess) + { + while (!net_queue_empty_free(&rx_queue_clients[client])) + { net_buff_desc_t buffer; int err = net_dequeue_free(&rx_queue_clients[client], &buffer); assert(!err); - assert(!(buffer.io_or_offset % NET_BUFFER_SIZE) - && (buffer.io_or_offset < NET_BUFFER_SIZE * rx_queue_clients[client].capacity)); + assert(!(buffer.io_or_offset % NET_BUFFER_SIZE) && (buffer.io_or_offset < NET_BUFFER_SIZE * rx_queue_clients[client].capacity)); // To avoid having to perform a cache clean here we ensure that // the DMA region is only mapped in read only. This avoids the @@ -141,20 +210,22 @@ static void rx_provide(void) net_request_signal_free(&rx_queue_clients[client]); reprocess = false; - if (!net_queue_empty_free(&rx_queue_clients[client])) { + if (!net_queue_empty_free(&rx_queue_clients[client])) + { net_cancel_signal_free(&rx_queue_clients[client]); reprocess = true; } } } - for (int client = 0; client < fw_config.num_free_clients; client++) { - while (!fw_queue_empty(&fw_free_clients[client])) { + for (int client = 0; client < fw_config.num_free_clients; client++) + { + while (!fw_queue_empty(&fw_free_clients[client])) + { net_buff_desc_t buffer; int err = fw_dequeue(&fw_free_clients[client], &buffer); assert(!err); - assert(!(buffer.io_or_offset % NET_BUFFER_SIZE) - && (buffer.io_or_offset < NET_BUFFER_SIZE * fw_free_clients[client].capacity)); + assert(!(buffer.io_or_offset % NET_BUFFER_SIZE) && (buffer.io_or_offset < NET_BUFFER_SIZE * fw_free_clients[client].capacity)); // To avoid having to perform a cache clean here we ensure that // the DMA region is only mapped in read only. This avoids the @@ -167,7 +238,8 @@ static void rx_provide(void) } } - if (notify_drv && net_require_signal_free(&rx_queue_drv)) { + if (notify_drv && net_require_signal_free(&rx_queue_drv)) + { net_cancel_signal_free(&rx_queue_drv); microkit_deferred_notify(config.driver.id); notify_drv = false; @@ -190,18 +262,64 @@ void init(void) net_buffers_init(&rx_queue_drv, config.data.io_addr); /* Set up net client queues */ - for (int i = 0; i < config.num_clients; i++) { + for (int i = 0; i < config.num_clients; i++) + { net_queue_init(&rx_queue_clients[i], config.clients[i].conn.free_queue.vaddr, config.clients[i].conn.active_queue.vaddr, config.clients[i].conn.num_buffers); } /* Set up firewall queues */ - for (int i = 0; i < fw_config.num_free_clients; i++) { + for (int i = 0; i < fw_config.num_free_clients; i++) + { fw_queue_init(&fw_free_clients[i], fw_config.free_clients[i].queue.vaddr, sizeof(net_buff_desc_t), fw_config.free_clients[i].capacity); } - if (net_require_signal_free(&rx_queue_drv)) { + /* Initialise NAT modules */ + num_nat_modules = 0; + for (int i = 0; i < fw_config.num_nat_configs; i++) + { + fw_nat_port_table_config_t *nat_cfg = &fw_config.nat_configs[i]; + fw_nat_port_table_t *port_table = (fw_nat_port_table_t *)nat_cfg->port_table.vaddr; + + size_t src_port_off, dst_port_off, check_off; + bool check_enabled; + + if (nat_cfg->protocol == IPV4_PROTO_TCP) + { + src_port_off = offsetof(tcp_hdr_t, src_port); + dst_port_off = offsetof(tcp_hdr_t, dst_port); + check_off = offsetof(tcp_hdr_t, check); + check_enabled = true; + } + else if (nat_cfg->protocol == IPV4_PROTO_UDP) + { + src_port_off = offsetof(udp_hdr_t, src_port); + dst_port_off = offsetof(udp_hdr_t, dst_port); + check_off = offsetof(udp_hdr_t, check); + check_enabled = true; + } + else + { + continue; + } + + int result = nat_module_init(&nat_modules[num_nat_modules], + fw_config.interface, + nat_cfg->protocol, + nat_cfg, + port_table, + fw_config.interface_ip, + src_port_off, + dst_port_off, + check_off, + check_enabled); + assert(result == NAT_ERR_OKAY); + num_nat_modules++; + } + + if (net_require_signal_free(&rx_queue_drv)) + { net_cancel_signal_free(&rx_queue_drv); microkit_deferred_notify(config.driver.id); } diff --git a/examples/firewall/net_components/firewall_network_virt_tx.c b/examples/firewall/net_components/firewall_network_virt_tx.c index 2feca7a3f..00cd8b239 100644 --- a/examples/firewall/net_components/firewall_network_virt_tx.c +++ b/examples/firewall/net_components/firewall_network_virt_tx.c @@ -12,7 +12,13 @@ #include #include #include +#include +#include +#include +#include #include +#include +#include __attribute__((__section__(".net_virt_tx_config"))) net_virt_tx_config_t config; __attribute__((__section__(".fw_net_virt_tx_config"))) fw_net_virt_tx_config_t fw_config; @@ -22,12 +28,16 @@ net_queue_handle_t tx_queue_clients[SDDF_NET_MAX_CLIENTS]; fw_queue_t fw_free_clients[FW_MAX_FW_CLIENTS]; fw_queue_t fw_active_clients[FW_MAX_FW_CLIENTS]; +bool nat_enabled; +nat_module_t nat_modules[FW_MAX_FILTERS]; +uint8_t num_nat_modules; static int extract_offset_net_client(uintptr_t *phys) { - for (int client = 0; client < config.num_clients; client++) { - if (*phys >= config.clients[client].data.io_addr - && *phys < config.clients[client].data.io_addr + tx_queue_clients[client].capacity * NET_BUFFER_SIZE) { + for (int client = 0; client < config.num_clients; client++) + { + if (*phys >= config.clients[client].data.io_addr && *phys < config.clients[client].data.io_addr + tx_queue_clients[client].capacity * NET_BUFFER_SIZE) + { *phys = *phys - config.clients[client].data.io_addr; return client; } @@ -37,10 +47,10 @@ static int extract_offset_net_client(uintptr_t *phys) static int extract_offset_fw_client(uintptr_t *phys) { - for (int client = 0; client < fw_config.num_free_clients; client++) { - if (*phys >= fw_config.free_clients[client].data.io_addr - && *phys - < fw_config.free_clients[client].data.io_addr + fw_free_clients[client].capacity * NET_BUFFER_SIZE) { + for (int client = 0; client < fw_config.num_free_clients; client++) + { + if (*phys >= fw_config.free_clients[client].data.io_addr && *phys < fw_config.free_clients[client].data.io_addr + fw_free_clients[client].capacity * NET_BUFFER_SIZE) + { *phys = *phys - fw_config.free_clients[client].data.io_addr; return client; } @@ -51,16 +61,19 @@ static int extract_offset_fw_client(uintptr_t *phys) static void tx_provide(void) { bool enqueued = false; - for (int client = 0; client < config.num_clients; client++) { + for (int client = 0; client < config.num_clients; client++) + { bool reprocess = true; - while (reprocess) { - while (!net_queue_empty_active(&tx_queue_clients[client])) { + while (reprocess) + { + while (!net_queue_empty_active(&tx_queue_clients[client])) + { net_buff_desc_t buffer; int err = net_dequeue_active(&tx_queue_clients[client], &buffer); assert(!err); - if (buffer.io_or_offset % NET_BUFFER_SIZE - || buffer.io_or_offset >= NET_BUFFER_SIZE * tx_queue_clients[client].capacity) { + if (buffer.io_or_offset % NET_BUFFER_SIZE || buffer.io_or_offset >= NET_BUFFER_SIZE * tx_queue_clients[client].capacity) + { sddf_dprintf("VIRT TX LOG, Interface %u: Client provided offset %lx which is not buffer aligned or " "outside of buffer region\n", fw_config.interface, buffer.io_or_offset); @@ -70,6 +83,35 @@ static void tx_provide(void) } uintptr_t buffer_vaddr = buffer.io_or_offset + (uintptr_t)config.clients[client].data.region.vaddr; + + /* Apply SNAT if enabled */ + if (nat_enabled) + { + uint16_t ethtype = htons(((eth_hdr_t *)buffer_vaddr)->ethtype); + if (ethtype == ETH_TYPE_IP) + { + ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(buffer_vaddr + IPV4_HDR_OFFSET); + fw_nat_err_t nat_result = NAT_ERR_UNSUPPORTED; + for (int j = 0; j < num_nat_modules; j++) { + if (nat_modules[j].protocol == ip_hdr->protocol) + { + nat_result = nat_module_translate(&nat_modules[j], buffer_vaddr, &buffer, false); + break; + } + } + + /* Drop packet if NAT translation fails */ + if (nat_result != NAT_ERR_OKAY) + { + sddf_dprintf("VIRT TX LOG, Interface %u: SNAT translation failed for protocol %u, dropping packet\n", + fw_config.interface, ip_hdr->protocol); + err = net_enqueue_free(&tx_queue_clients[client], buffer); + assert(!err); + continue; + } + } + } + cache_clean(buffer_vaddr, buffer_vaddr + buffer.len); buffer.io_or_offset = buffer.io_or_offset + config.clients[client].data.io_addr; @@ -81,35 +123,66 @@ static void tx_provide(void) net_request_signal_active(&tx_queue_clients[client]); reprocess = false; - if (!net_queue_empty_active(&tx_queue_clients[client])) { + if (!net_queue_empty_active(&tx_queue_clients[client])) + { net_cancel_signal_active(&tx_queue_clients[client]); reprocess = true; } } } - for (int client = 0; client < fw_config.num_active_clients; client++) { - while (!fw_queue_empty(&fw_active_clients[client])) { + for (int client = 0; client < fw_config.num_active_clients; client++) + { + while (!fw_queue_empty(&fw_active_clients[client])) + { fw_buff_desc_t buffer; int err = fw_dequeue(&fw_active_clients[client], &buffer); assert(!err); - assert(buffer.offset % NET_BUFFER_SIZE == 0 - && buffer.offset < NET_BUFFER_SIZE * fw_active_clients[client].capacity); + assert(buffer.offset % NET_BUFFER_SIZE == 0 && buffer.offset < NET_BUFFER_SIZE * fw_active_clients[client].capacity); assert(buffer.interface < fw_config.num_data_regions); uintptr_t buffer_vaddr = buffer.offset + (uintptr_t)fw_config.data_regions[buffer.interface].region.vaddr; + + /* Apply SNAT if enabled */ + if (fw_config.num_nat_configs > 0) + { + uint16_t ethtype = htons(((eth_hdr_t *)buffer_vaddr)->ethtype); + if (ethtype == ETH_TYPE_IP) + { + ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(buffer_vaddr + IPV4_HDR_OFFSET); + fw_nat_err_t nat_result = NAT_ERR_OKAY; + for (int j = 0; j < num_nat_modules; j++) + { + if (nat_modules[j].protocol == ip_hdr->protocol) + { + nat_result = nat_module_translate(&nat_modules[j], buffer_vaddr, (net_buff_desc_t *)&buffer, false); + break; + } + } + + /* Drop packet if NAT translation fails */ + if (nat_result != NAT_ERR_OKAY) + { + sddf_dprintf("VIRT TX LOG, Interface %u: SNAT translation failed for protocol %u, dropping packet\n", + fw_config.interface, ip_hdr->protocol); + continue; + } + } + } + cache_clean(buffer_vaddr, buffer_vaddr + buffer.len); uintptr_t io_addr = buffer.offset + fw_config.data_regions[buffer.interface].io_addr; - net_buff_desc_t net_buffer = { .io_or_offset = io_addr, .len = buffer.len }; + net_buff_desc_t net_buffer = {.io_or_offset = io_addr, .len = buffer.len}; err = net_enqueue_active(&tx_queue_drv, net_buffer); assert(!err); enqueued = true; } } - if (enqueued && net_require_signal_active(&tx_queue_drv)) { + if (enqueued && net_require_signal_active(&tx_queue_drv)) + { net_cancel_signal_active(&tx_queue_drv); microkit_deferred_notify(config.driver.id); } @@ -118,16 +191,19 @@ static void tx_provide(void) static void tx_return(void) { bool reprocess = true; - bool notify_net_clients[SDDF_NET_MAX_CLIENTS] = { false }; - bool notify_fw_clients[SDDF_NET_MAX_CLIENTS] = { false }; - while (reprocess) { - while (!net_queue_empty_free(&tx_queue_drv)) { + bool notify_net_clients[SDDF_NET_MAX_CLIENTS] = {false}; + bool notify_fw_clients[SDDF_NET_MAX_CLIENTS] = {false}; + while (reprocess) + { + while (!net_queue_empty_free(&tx_queue_drv)) + { net_buff_desc_t buffer; int err = net_dequeue_free(&tx_queue_drv, &buffer); assert(!err); int client = extract_offset_net_client(&buffer.io_or_offset); - if (client >= 0) { + if (client >= 0) + { err = net_enqueue_free(&tx_queue_clients[client], buffer); assert(!err); notify_net_clients[client] = true; @@ -144,26 +220,65 @@ static void tx_return(void) net_request_signal_free(&tx_queue_drv); reprocess = false; - if (!net_queue_empty_free(&tx_queue_drv)) { + if (!net_queue_empty_free(&tx_queue_drv)) + { net_cancel_signal_free(&tx_queue_drv); reprocess = true; } } - for (int client = 0; client < config.num_clients; client++) { - if (notify_net_clients[client] && net_require_signal_free(&tx_queue_clients[client])) { + for (int client = 0; client < config.num_clients; client++) + { + if (notify_net_clients[client] && net_require_signal_free(&tx_queue_clients[client])) + { net_cancel_signal_free(&tx_queue_clients[client]); microkit_notify(config.clients[client].conn.id); } } - for (int client = 0; client < fw_config.num_free_clients; client++) { - if (notify_fw_clients[client]) { + for (int client = 0; client < fw_config.num_free_clients; client++) + { + if (notify_fw_clients[client]) + { microkit_notify(fw_config.free_clients[client].conn.ch); } } } +microkit_msginfo protected(microkit_channel ch, microkit_msginfo msginfo) +{ + switch (microkit_msginfo_get_label(msginfo)) + { + case NAT_SET_ENABLED: + { + bool enabled = (bool)microkit_mr_get(NAT_SET_ENABLED_ARG_ENABLED); + for (int i = 0; i < fw_config.num_nat_configs; i++) + { + if (fw_config.nat_configs[i].webserver_ch == ch) + { + for (int j = 0; j < num_nat_modules; j++) + { + if (nat_modules[j].protocol == fw_config.nat_configs[i].protocol) + { + nat_modules[j].port_table->nat_enabled = enabled; + break; + } + } + microkit_mr_set(NAT_RET_ERR, NAT_ERR_OKAY); + return microkit_msginfo_new(0, 1); + } + } + microkit_mr_set(NAT_RET_ERR, NAT_ERR_FAILURE); + return microkit_msginfo_new(0, 1); + } + default: + sddf_dprintf("TX VIRT %u: unknown PPC label %lu on channel %u\n", + fw_config.interface, microkit_msginfo_get_label(msginfo), ch); + break; + } + return microkit_msginfo_new(0, 0); +} + void notified(microkit_channel ch) { tx_return(); @@ -178,20 +293,69 @@ void init(void) net_queue_init(&tx_queue_drv, config.driver.free_queue.vaddr, config.driver.active_queue.vaddr, config.driver.num_buffers); - for (int i = 0; i < config.num_clients; i++) { + for (int i = 0; i < config.num_clients; i++) + { net_queue_init(&tx_queue_clients[i], config.clients[i].conn.free_queue.vaddr, config.clients[i].conn.active_queue.vaddr, config.clients[i].conn.num_buffers); } /* Set up firewall queues */ - for (int i = 0; i < fw_config.num_active_clients; i++) { + for (int i = 0; i < fw_config.num_active_clients; i++) + { fw_queue_init(&fw_active_clients[i], fw_config.active_clients[i].queue.vaddr, sizeof(fw_buff_desc_t), fw_config.active_clients[i].capacity); } - for (int i = 0; i < fw_config.num_free_clients; i++) { + for (int i = 0; i < fw_config.num_free_clients; i++) + { fw_queue_init(&fw_free_clients[i], fw_config.free_clients[i].conn.queue.vaddr, sizeof(net_buff_desc_t), fw_config.free_clients[i].conn.capacity); } + + /* Initialise NAT modules */ + num_nat_modules = 0; + for (int i = 0; i < fw_config.num_nat_configs; i++) + { + fw_nat_port_table_config_t *nat_cfg = &fw_config.nat_configs[i]; + fw_nat_port_table_t *port_table = (fw_nat_port_table_t *)nat_cfg->port_table.vaddr; + + size_t src_port_off, dst_port_off, check_off; + bool check_enabled; + + if (nat_cfg->protocol == IPV4_PROTO_TCP) + { + src_port_off = offsetof(tcp_hdr_t, src_port); + dst_port_off = offsetof(tcp_hdr_t, dst_port); + check_off = offsetof(tcp_hdr_t, check); + check_enabled = true; + } + else if (nat_cfg->protocol == IPV4_PROTO_UDP) + { + src_port_off = offsetof(udp_hdr_t, src_port); + dst_port_off = offsetof(udp_hdr_t, dst_port); + check_off = offsetof(udp_hdr_t, check); + check_enabled = true; + } + else + { + continue; + } + + int result = nat_module_init(&nat_modules[num_nat_modules], + fw_config.interface, + nat_cfg->protocol, + nat_cfg, + port_table, + fw_config.interface_ip, + src_port_off, + dst_port_off, + check_off, + check_enabled); + assert(result == NAT_ERR_OKAY); + + port_table->nat_enabled = nat_cfg->enabled; + num_nat_modules++; + } + tx_provide(); } diff --git a/examples/firewall/net_components/nat_module.c b/examples/firewall/net_components/nat_module.c new file mode 100644 index 000000000..5b43079fc --- /dev/null +++ b/examples/firewall/net_components/nat_module.c @@ -0,0 +1,215 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ +#include "microkit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Initialise the NAT module + */ +fw_nat_err_t nat_module_init(nat_module_t *nat, + uint8_t interface, + uint8_t protocol, + fw_nat_port_table_config_t *config, + fw_nat_port_table_t *port_table, + uint32_t interface_ip, + size_t src_port_off, + size_t dst_port_off, + size_t check_off, + bool check_enabled) +{ + if (!nat || !config || !port_table) + { + return NAT_ERR_FAILURE; + } + + nat->protocol = protocol; + nat->port_table = port_table; + nat->interface_ip = interface_ip; + nat->src_port_off = src_port_off; // maybe build time in the future. + nat->dst_port_off = dst_port_off; // maybe build time in the future. + nat->check_off = check_off; + nat->check_enabled = check_enabled; + + if (FW_DEBUG_OUTPUT) + { + sddf_printf("%s%s NAT Module: initialized, base port = %u, capacity = %u\n", + "iface", + "protocol", + config->base_port, + config->ports_capacity); + } + + return NAT_ERR_OKAY; +} + +/** + * Translate a packet using NAT + */ +fw_nat_err_t nat_module_translate(nat_module_t *nat, + uintptr_t pkt_vaddr, + net_buff_desc_t *buffer, + bool do_dnat) +{ + if (!nat || !pkt_vaddr) + { + return NAT_ERR_INVALID_PACKET; + } + + /* Extract IP header */ + ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(pkt_vaddr + IPV4_HDR_OFFSET); + if (!ip_hdr) + { + return NAT_ERR_INVALID_PACKET; + } + + /* Extract transport header */ + char *transport_hdr = (char *)(pkt_vaddr + transport_layer_offset(ip_hdr)); + + /* Get pointers to port fields */ + uint16_t *src_port = (uint16_t *)(transport_hdr + nat->src_port_off); + uint16_t *dst_port = (uint16_t *)(transport_hdr + nat->dst_port_off); + uint16_t *check = (uint16_t *)(transport_hdr + nat->check_off); + + /* TODO */ + uint64_t now = 0; + + bool checksum_dirty = false; + bool dnat_done = false; + + /* Log packet before translation */ + if (FW_DEBUG_OUTPUT) + { + sddf_printf("%s%s NAT Module: before translation:\n", + "iface", + "protocol"); + sddf_printf("%s%s NAT Module: src = %s:%u\n", + "iface", + "protocol", + ipaddr_to_string(ip_hdr->src_ip, ip_addr_buf0), + htons(*src_port)); + sddf_printf("%s%s NAT Module: dst = %s:%u\n", + "iface", + "protocol", + ipaddr_to_string(ip_hdr->dst_ip, ip_addr_buf0), + htons(*dst_port)); + } + + /* DNAT: returning traffic arrives on the external interface addressed to iface_ip:ephemeral_port; + * reverse-map destination back to the original internal host. */ + if (do_dnat && nat->port_table->nat_enabled) + { + if (ip_hdr->dst_ip == nat->interface_ip) + { + uint16_t dst_port_host = htons(*dst_port); + + if (dst_port_host >= nat->config->base_port && + dst_port_host < nat->config->base_port + nat->port_table->largest_index) + { + uint16_t table_index = dst_port_host - nat->config->base_port; + fw_nat_port_mapping_t *mapping = &nat->port_table->mappings[table_index]; + + if (mapping->is_valid) + { + if (FW_DEBUG_OUTPUT) + { + sddf_printf("%s%s NAT Module: returning traffic detected (DNAT)\n", + "iface", + "protocol"); + } + + mapping->last_used_ts = now; + + *dst_port = mapping->src_port; + ip_hdr->dst_ip = mapping->src_ip; + ip_hdr->check = 0; + + checksum_dirty = true; + dnat_done = true; + } + } + } + } + + /* SNAT: outbound traffic leaving the internal network; rewrite source to iface_ip:ephemeral_port + * and record the mapping so returning traffic can be DNAT-ed back. */ + if (!dnat_done && nat->port_table->nat_enabled && ip_hdr->dst_ip != nat->interface_ip) + { + uint16_t ephemeral_port = fw_nat_find_ephemeral_port( + *nat->config, + nat->port_table, + ip_hdr->src_ip, + *src_port, + now); + + if (ephemeral_port) + { + ip_hdr->src_ip = nat->interface_ip; + *src_port = ephemeral_port; + ip_hdr->check = 0; + + checksum_dirty = true; + + if (FW_DEBUG_OUTPUT) + { + sddf_printf("%s%s NAT Module: SNAT translated to %s:%u\n", + "iface", + "protocol", + ipaddr_to_string(nat->interface_ip, ip_addr_buf0), + htons(*src_port)); + } + } + else + { + sddf_printf("%s%s NAT Module: ERROR: ephemeral ports exhausted!\n", + "iface", + "protocol"); + return NAT_ERR_PORT_EXHAUSTED; + } + } + + /* Recalculate checksum if headers were modified */ + if (checksum_dirty && nat->check_enabled) + { + *check = 0; + *check = calculate_transport_checksum( + transport_hdr, + htons(ip_hdr->tot_len) - ipv4_header_length(ip_hdr), + nat->protocol, + ip_hdr->src_ip, + ip_hdr->dst_ip); + + ip_hdr->check = 0; + uint8_t ihl_bytes = ip_hdr->ihl * 4; + ip_hdr->check = fw_internet_checksum(ip_hdr, ihl_bytes); + } + + /* Log packet after translation */ + if (FW_DEBUG_OUTPUT) + { + sddf_printf("%s%s NAT Module: after translation:\n", + "iface", + "protocol"); + sddf_printf("%s%s NAT Module: src = %s:%u\n", + "iface", + "protocol", + ipaddr_to_string(ip_hdr->src_ip, ip_addr_buf0), + htons(*src_port)); + sddf_printf("%s%s NAT Module: dst = %s:%u\n", + "iface", + "protocol", + ipaddr_to_string(ip_hdr->dst_ip, ip_addr_buf0), + htons(*dst_port)); + } + + return NAT_ERR_OKAY; +} diff --git a/examples/firewall/pyfw/component_net_virt.py b/examples/firewall/pyfw/component_net_virt.py index 65e01e378..3a612f5ef 100644 --- a/examples/firewall/pyfw/component_net_virt.py +++ b/examples/firewall/pyfw/component_net_virt.py @@ -7,6 +7,8 @@ BuildConstants, dma_buffer_queue, dma_buffer_queue_region, + nat_port_table_region, + supported_protocols, ) from pyfw.specs import FirewallMemoryRegion, TrackedNet from config_structs import ( @@ -15,6 +17,8 @@ FwDataConnectionResource, FwNetVirtRxConfig, FwNetVirtTxConfig, + FwNatPortTableConfig, + RegionResource, ) SDF_Channel = SystemDescription.Channel @@ -34,14 +38,18 @@ def __init__(self, # Store the network interface so sDDF net clients can be added self._sddf_net: TrackedNet = sddf_net + self._net_interface = net_interface # Initialise Rx virtualiser config class FwNetVirtRxConfig.__init__( self, interface=net_interface.index, + interface_ip=net_interface.ip_int, active_client_ethtypes=[], active_client_subtypes=[], free_clients=[], + nat_dma_region=None, + nat_configs=[], ) def add_active_net_client(self, @@ -87,6 +95,24 @@ def add_free_fw_client(self, client: Component) -> FwConnectionResource: ch=ch.pd_b_id, ) + def add_nat_config_with_port_table(self, protocol: int, base_port: int, capacity: int, port_table_mr: FirewallMemoryRegion, webserver_ch: int = 0) -> None: + """Configure NAT with a shared port table (for RX/TX sharing)""" + nat_config = FwNatPortTableConfig( + base_port=base_port, + ports_capacity=capacity, + port_table=port_table_mr.map(self.pd, "rw"), + protocol=protocol, + enabled=True, + webserver_ch=webserver_ch, + ) + + assert self.nat_configs is not None + self.nat_configs.append(nat_config) + + def set_nat_dma_region(self, dma_region) -> None: + """Map RX DMA region with write permissions for NAT packet modification""" + self.nat_dma_region = dma_region.map_device(self.pd, "rw") + def finalise_config(self) -> None: assert self.active_client_ethtypes is not None assert self.active_client_subtypes is not None @@ -110,14 +136,17 @@ def __init__( # Store data region as a dictionary to be sorted into list upon finalisation self._data_regions: dict[int, DeviceRegionResource] = {} + self._net_interface = net_interface # Initialise Tx virtualiser config class FwNetVirtTxConfig.__init__( self, interface=net_interface.index, + interface_ip=net_interface.ip_int, active_clients=[], data_regions=[], free_clients=[], + nat_configs=[], ) def add_active_fw_client(self, client: Component) -> FwConnectionResource: @@ -156,7 +185,8 @@ def add_free_fw_client(self, assert data.mr.paddr not in (data_map.io_addr for data_map in self._data_regions.values()) # Add data region to list assert interface_idx not in self._data_regions.keys() - self._data_regions[interface_idx] = data.map_device(self.pd, "r") + # Map as read-write so NAT can modify packet headers + self._data_regions[interface_idx] = data.map_device(self.pd, "rw") assert self.free_clients is not None self.free_clients.append( @@ -166,6 +196,20 @@ def add_free_fw_client(self, ) ) + def add_nat_config_with_port_table(self, protocol: int, base_port: int, capacity: int, port_table_mr: FirewallMemoryRegion, webserver_ch: int = 0) -> None: + """Configure NAT with a shared port table (for RX/TX sharing)""" + nat_config = FwNatPortTableConfig( + base_port=base_port, + ports_capacity=capacity, + port_table=port_table_mr.map(self.pd, "rw"), + protocol=protocol, + enabled=True, + webserver_ch=webserver_ch, + ) + + assert self.nat_configs is not None + self.nat_configs.append(nat_config) + def finalise_config(self) -> None: assert self.data_regions is not None and len(self.data_regions) == 0 for i in range(len(self._data_regions)): diff --git a/examples/firewall/pyfw/component_webserver.py b/examples/firewall/pyfw/component_webserver.py index eee6e0f4e..c76400ea0 100644 --- a/examples/firewall/pyfw/component_webserver.py +++ b/examples/firewall/pyfw/component_webserver.py @@ -9,6 +9,7 @@ ) from config_structs import ( EthHwaddrLen, + FwNatPortTableConfig, FwWebserverConfig, FwWebserverInterfaceConfig, ) @@ -42,6 +43,7 @@ def __init__( filters=[], data=None, rx_free=None, + nat_configs=[], ) ) @@ -54,6 +56,18 @@ def __init__( tx_interface=webserver_tx_interface_idx, ) + def add_nat_ppc_channel(self, protocol, interface, tx_ch, port_table_mr): + """Register PPC channel to TX virtualizer and port table region for NAT enable/disable""" + nat_config = FwNatPortTableConfig( + base_port=0, + ports_capacity=0, + port_table=port_table_mr.map(self.pd, "ro"), + protocol=protocol, + enabled=True, + webserver_ch=tx_ch, + ) + self._interfaces[interface].nat_configs.append(nat_config) + def finalise_config(self) -> None: assert self.interfaces is not None and len(self.interfaces) == len(interfaces) for iface in self.interfaces: diff --git a/examples/firewall/pyfw/constants.py b/examples/firewall/pyfw/constants.py index dffa8216f..6ca8f9aa8 100644 --- a/examples/firewall/pyfw/constants.py +++ b/examples/firewall/pyfw/constants.py @@ -91,7 +91,7 @@ def output_dir(cls) -> str: ### ----------------------------------------------------------------------- ### ### Filtering ### ### ----------------------------------------------------------------------- ### -supported_protocols = {0x01: "icmp", 0x06: "tcp", 0x11: "udp"} +supported_protocols = {0x01: "icmp", 0x06: "tcp", 0x11: "udp", 0x17: "tpp"} FILTER_ACTION_ALLOW = 1 FILTER_ACTION_DROP = 2 @@ -102,7 +102,8 @@ def output_dir(cls) -> str: supported_filter_actions = { 0x01: [1, 1, 1, 1], 0x06: [1, 1, 0, 1], - 0x11: [1, 1, 1, 1] + 0x11: [1, 1, 1, 1], + 0x17: [1, 1, 1, 1], } def construct_rule(action: int, src_ip: int, src_subnet: int, src_port: int, src_port_any: bool, @@ -140,6 +141,21 @@ def default_action_rule(action: int) -> FwRule: }, ] +### ----------------------------------------------------------------------- ### +### NAT ### +### ----------------------------------------------------------------------- ### + +nat_state = [ + { + enabled: true, + init_enabled: true, + protocols: [0x11: {src_port: 0x5, dst_port: 0x6}, 0x06, 0x11] + }, + { + enabled: false, + } +] + ### ----------------------------------------------------------------------- ### ### Routing ### ### ----------------------------------------------------------------------- ### @@ -262,6 +278,30 @@ def construct_route(ip: int, subnet: int, interface: int, next_hop: int) -> FwRo data_structures=[filter_instances_wrapper, filter_instances_buffer] ) +# --------------------------------------------- # +# NAT port table - stores ephemeral port mappings per interface/protocol +# Using manual sizes since NAT structs are not in early-built ELFs +nat_port_table_wrapper = FirewallDataStructure( + entry_size=16 # fw_nat_port_table_t header (size, largest_index, free_head) +) +nat_port_table_buffer = FirewallDataStructure( + entry_size=32, # fw_nat_port_mapping_t (src_ip, src_port, next_free, is_valid, last_used_ts) + capacity=512 +) +nat_port_table_region = FirewallMemoryRegions( + data_structures=[nat_port_table_wrapper, nat_port_table_buffer] +) + +# --------------------------------------------- # +# NAT webserver state - shared SNAT configuration +nat_webserver_state_buffer = FirewallDataStructure( + entry_size=64, # fw_nat_webserver_state_t - extra space for alignment/padding + capacity=1 +) +nat_webserver_state_region = FirewallMemoryRegions( + data_structures=[nat_webserver_state_buffer] +) + ### ----------------------------------------------------------------------- ### ### Network constants ### ### ----------------------------------------------------------------------- ### diff --git a/examples/firewall/test_nat.py b/examples/firewall/test_nat.py new file mode 100644 index 000000000..09e2bb263 --- /dev/null +++ b/examples/firewall/test_nat.py @@ -0,0 +1,75 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause +import argparse +import socket +import threading + +""" +Script that listens for TCP/UDP packets and prints source IP and port. +Optionally accepts an expected IP to test against. +Use -m/--multi for concurrent TCP connections. +""" + +parser = argparse.ArgumentParser() + +parser.add_argument("address", nargs="?") +parser.add_argument("-u", "--udp", action="store_true") +parser.add_argument("-m", "--multi", action="store_true", help="Handle multiple concurrent TCP connections") + +args = parser.parse_args() + +expected_ip = args.address +udp = args.udp +multi = args.multi + + +def print_status(addr: tuple[str, int], expected_ip: str, data: bytes): + status = ( + ("| PASS" if addr[0] == expected_ip else "| FAIL") + if expected_ip is not None + else "" + ) + + print(f"[{addr[0]}:{addr[1]}{status}] {data}", flush=True) + + +def handle_tcp_conn(conn, addr): + with conn: + print(f"Connection established from {addr[0]}:{addr[1]}", flush=True) + while True: + data = conn.recv(1024) + if not data: + break + print_status(addr, expected_ip, data) + conn.sendall(b"return traffic\n") + print(f"Connection closed from {addr[0]}:{addr[1]}", flush=True) + + +HOST = "" +PORT = 65444 +with socket.socket( + socket.AF_INET, socket.SOCK_DGRAM if udp else socket.SOCK_STREAM +) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((HOST, PORT)) + if not udp: + s.listen() + while True: + if udp: + data, addr = s.recvfrom(1024) + print_status(addr, expected_ip, data) + s.sendto(b"return traffic\n", addr) + elif multi: + conn, addr = s.accept() + t = threading.Thread(target=handle_tcp_conn, args=(conn, addr), daemon=True) + t.start() + else: + conn, addr = s.accept() + with conn: + print(f"Connection established from {addr[0]}:{addr[1]}", flush=True) + while True: + data = conn.recv(1024) + if not data: + break + print_status(addr, expected_ip, data) + conn.sendall(b"return traffic\n") diff --git a/examples/firewall/ui_server.py b/examples/firewall/ui_server.py index 7bc5b25ef..acad950c0 100644 --- a/examples/firewall/ui_server.py +++ b/examples/firewall/ui_server.py @@ -426,6 +426,38 @@ def getPing(request, interfaceInt): print(f"UI SERVER|ERR: Unknown Error: getRules: {exception}.") return {"error": UnknownErrStr}, 404 +###### NAT configuration methods ###### + +@app.route('/api/nat///enabled', methods=['GET', 'PUT']) +def nat_enabled_handler(request, protocolStr, interfaceInt): + try: + if interfaceInt < 0 or interfaceInt >= lions_firewall.interface_count_get(): + raise OSError(OSErrInvalidInterface, OSErrStrings[OSErrInvalidInterface]) + interface = interfaceInt + + if protocolStr not in protocolNums.keys(): + print(f"UI SERVER|ERR: Supplied protocol string {protocolStr} does not match any protocols.") + raise OSError(OSErrInvalidInput, OSErrStrings[OSErrInvalidInput]) + protocol = protocolNums[protocolStr] + + if request.method == 'GET': + enabled = lions_firewall.nat_get_enabled(interface, protocol) + print(f"UI SERVER|NAT GET: iface={interface} proto={hex(protocol)} enabled={enabled}") + return {"enabled": bool(enabled)} + else: + body = request.json + if body is None or "enabled" not in body: + return {"error": "missing 'enabled' field"}, 400 + enabled = bool(body["enabled"]) + print(f"UI SERVER|NAT SET: iface={interface} proto={hex(protocol)} enabled={enabled}") + lions_firewall.nat_set_enabled(interface, protocol, enabled) + return {"status": "ok"} + except OSError as OSErr: + print(f"UI SERVER|ERR: OS Error: nat_enabled_handler: {OSErrStrings[OSErr.errno]}") + return {"error": OSErrStrings[OSErr.errno]}, 404 + except Exception as exception: + print(f"UI SERVER|ERR: Unknown Error: nat_enabled_handler: {exception}.") + return {"error": UnknownErrStr}, 404 ############ Web UI routes ############ @@ -442,7 +474,7 @@ def index(request):

Firewall Configuration

@@ -462,7 +494,7 @@ def index(request):

Firewall Configuration

@@ -524,7 +556,7 @@ def config(request):

Routing Configuration Page

Routing Table

@@ -684,7 +716,7 @@ def rules(request, protocol):

Firewall Rules

UDP @@ -920,7 +952,7 @@ def rules(request):

Firewall Rules

UDP @@ -950,6 +982,61 @@ def css(request): } """ return Response(body=css, headers={'Content-Type': 'text/css'}) + +@app.route("/nat_settings") +def nat_settings(request): + html = r""" + + + + + Firewall Network Address Translation + + + +

Firewall Network Address Translation

+ +

External Interface NAT

+

NAT translates outbound traffic from the internal network using the external interface IP.

+

TCP

+
+ + + +
+

UDP

+
+ + + +
+ + + +""" + return Response(body=html, headers={'Content-Type': 'text/html'}) + @app.route('/ping_settings') def ping_settings(request): html = """ diff --git a/include/lions/firewall/checksum.h b/include/lions/firewall/checksum.h index e0a5f41f5..99b8ba7dd 100644 --- a/include/lions/firewall/checksum.h +++ b/include/lions/firewall/checksum.h @@ -73,11 +73,11 @@ typedef struct fw_pseudo_header { * @param dst_ip Destination IP address in big endian byte order. * @return The calculated 16-bit Internet Checksum. */ -uint16_t calculate_transport_checksum(void *pkt, - uint16_t len, - uint8_t protocol, - uint32_t src_ip, - uint32_t dst_ip) +static inline uint16_t calculate_transport_checksum(void *pkt, + uint16_t len, + uint8_t protocol, + uint32_t src_ip, + uint32_t dst_ip) { uint32_t sum = 0; uint16_t *pkt_ptr = (uint16_t *)pkt; diff --git a/include/lions/firewall/config.h b/include/lions/firewall/config.h index bf88536f6..5446cf9ca 100644 --- a/include/lions/firewall/config.h +++ b/include/lions/firewall/config.h @@ -39,18 +39,33 @@ typedef struct fw_data_connection_resource { device_region_resource_t data; } fw_data_connection_resource_t; +/* NAT port table configuration (one per protocol per interface, used by both RX and TX virtualizers) */ +typedef struct fw_nat_port_table_config { + uint16_t base_port; + uint16_t ports_capacity; + region_resource_t port_table; + uint8_t protocol; +} fw_nat_port_table_config_t; + typedef struct fw_net_virt_tx_config { uint8_t interface; + uint32_t interface_ip; fw_connection_resource_t active_clients[FW_MAX_FW_CLIENTS]; uint8_t num_active_clients; device_region_resource_t data_regions[FW_MAX_INTERFACES]; uint8_t num_data_regions; fw_data_connection_resource_t free_clients[FW_MAX_FW_CLIENTS]; uint8_t num_free_clients; + fw_nat_port_table_config_t nat_configs[FW_MAX_FILTERS]; + uint8_t num_nat_configs; + bool possible_to_enable_nat; + bool enabled; /* build-time initial state */ + uint16_t webserver_ch; /* PPC channel from webserver for NAT enable/disable */ } fw_net_virt_tx_config_t; typedef struct fw_net_virt_rx_config { uint8_t interface; + uint32_t interface_ip; /* Eth-type of traffic to be routed to each client */ uint16_t active_client_ethtypes[SDDF_NET_MAX_CLIENTS]; /* Sub-type of traffic to be routed to each client. If ethtype == IPv4, this @@ -59,6 +74,12 @@ typedef struct fw_net_virt_rx_config { uint16_t active_client_subtypes[SDDF_NET_MAX_CLIENTS]; fw_connection_resource_t free_clients[FW_MAX_FW_CLIENTS]; uint8_t num_free_clients; + /* RX DMA region mapped rw so NAT can modify packet headers */ + device_region_resource_t nat_dma_region; + fw_nat_port_table_config_t nat_configs[FW_MAX_FILTERS]; + uint8_t num_nat_configs; + bool enabled; /* build-time initial state */ + uint16_t webserver_ch; /* PPC channel from webserver for NAT enable/disable */ } fw_net_virt_rx_config_t; typedef struct fw_arp_connection { @@ -161,6 +182,9 @@ typedef struct fw_webserver_interface_config { uint8_t num_filters; region_resource_t data; fw_connection_resource_t rx_free; + fw_nat_port_table_config_t nat_configs[FW_MAX_FILTERS]; + uint8_t num_nat_configs; + bool possible_to_enable_nat; } fw_webserver_interface_config_t; typedef struct fw_webserver_config { diff --git a/include/lions/firewall/nat_module.h b/include/lions/firewall/nat_module.h new file mode 100644 index 000000000..967855d79 --- /dev/null +++ b/include/lions/firewall/nat_module.h @@ -0,0 +1,263 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef enum { + /* no error */ + NAT_ERR_OKAY = 0, + /* null pointer passed or port table not initialised */ + NAT_ERR_FAILURE, + /* all ephemeral ports in the table are in use */ + NAT_ERR_PORT_EXHAUSTED, + /* packet is too short or has an invalid IP header */ + NAT_ERR_INVALID_PACKET, +} fw_nat_err_t; + +/* NAT timeout interval in nanoseconds */ +#define NAT_TIMEOUT_INTERVAL_NS (5 * NS_IN_S) + +/** + * NAT table structures + */ + +/** + * Stores original source and destination corresponding to a NAT ephemeral port. + * This is an endpoint independent mapping since only source address and port are used. + */ +typedef struct fw_nat_port_mapping fw_nat_port_mapping_t; +struct fw_nat_port_mapping +{ + /* timeout per protocol*/ + uint32_t timeout; + /* Original source IP of traffic */ + uint32_t src_ip; + /* Original source port of traffic (network byte order) */ + uint16_t src_port; + /* Next free node (for nodes in free list only) */ + fw_nat_port_mapping_t *next_free; + bool is_valid; + /* SDDF timer timestamp (nanoseconds) for last time a packet was sent/received */ + uint64_t last_used_ts; +}; + +/** + * Port table that manages ephemeral port allocations + */ +typedef struct fw_nat_port_table +{ + // /* Runtime enable flag*/ + // bool nat_enabled; + /* Number of valid NAT entries */ + uint16_t size; + /* Largest initialized entry in the NAT table (could be valid or free) */ + uint16_t largest_index; + uint32_t base_port; + uint16_t capacity; + /* Head of free nodes */ + fw_nat_port_mapping_t *free_head; + fw_nat_port_mapping_t mappings[]; +} fw_nat_port_table_t; + +/* Magic number to indicate webserver_state has been initialized */ +#define FW_NAT_WEBSERVER_STATE_MAGIC 0x4E415457 + +/** + * Structure shared with webserver to configure NAT for all interfaces with this protocol. + * The SNAT IP is static (from build-time config); only enable/disable and timeout are runtime. + */ +// typedef struct fw_nat_webserver_state +// { +// uint32_t magic; /* Magic number for initialization check */ +// /* Timeout in nanoseconds */ +// uint64_t timeout; +// } fw_nat_webserver_state_t; -- Just initialise in one virtualiser, handle timeout later + + +/** + * Find the ephemeral port to use for a source IP and port. + * Attempts to reuse an existing mapping for that IP and port, + * only creating a new entry if not found. + * + * @param config NAT config for this interface + * @param ports Ephemeral port table for this interface + * @param src_ip Source IP in network byte order + * @param src_port Source port in network byte order + * @param now Current timestamp in nanoseconds + * + * @return Ephemeral port in network byte order, or 0 if no port available + */ +static inline uint16_t fw_nat_find_ephemeral_port(fw_nat_port_table_config_t config, + fw_nat_port_table_t *ports, + uint32_t src_ip, + uint16_t src_port, + uint64_t now) +{ + /* Search for an existing mapping */ + for (uint16_t i = 0; i < ports->size; i++) + { + if (ports->mappings[i].src_ip == src_ip && ports->mappings[i].src_port == src_port && ports->mappings[i].is_valid) + { + ports->mappings[i].last_used_ts = now; + return htons(config.base_port + i); + } + } + + /* Try to reuse a free entry */ + if (ports->free_head) + { + ports->size++; + + /* Remove from front of list */ + fw_nat_port_mapping_t *mapping = ports->free_head; + ports->free_head = mapping->next_free; + + mapping->src_port = src_port; + mapping->src_ip = src_ip; + mapping->last_used_ts = now; + mapping->is_valid = true; + return htons(config.base_port + (mapping - ports->mappings)); + } + // Don't forget to initialise in port table init function + + return FULL; +} + +/** + * Free an ephemeral port, prepending it to the head of the free list. + * + * @param config NAT config for this interface + * @param ports Ephemeral port table for this interface + * @param port Ephemeral port to be freed in host byte order + */ +static inline void fw_nat_free_ephemeral_port(fw_nat_port_table_config_t config, + fw_nat_port_table_t *ports, + uint16_t port) +{ + fw_nat_port_mapping_t *mapping = &ports->mappings[port - config.base_port]; + + mapping->src_ip = 0; + mapping->src_port = 0; + mapping->next_free = ports->free_head; + mapping->is_valid = false; + mapping->last_used_ts = 0; + + /* This index is now the new head */ + ports->free_head = mapping; + + ports->size--; +} + +/** + * Frees all port mappings older than the timeout duration. + * + * @param config NAT config for this interface + * @param ports Ephemeral port table for this interface + * @param timeout Duration in nanoseconds for which entries older than it will be freed + * @param now The time now as an SDDF timestamp + */ +static inline void fw_nat_free_expired_mappings(fw_nat_port_table_config_t config, + fw_nat_port_table_t *ports, + uint64_t timeout, + uint64_t now) +{ + for (uint16_t i = 0; i < ports->largest_index; i++) + { + fw_nat_port_mapping_t *mapping = &ports->mappings[i]; + + if (mapping->is_valid && now > timeout && mapping->last_used_ts <= now - timeout) + { + fw_nat_free_ephemeral_port(config, ports, config.base_port + i); + +#ifdef FW_DEBUG_OUTPUT + sddf_printf("NAT LOG: freed port: %u, %u remaining\n", config.base_port + i, ports->size); +#endif + } + } +} + +/** + * NAT module handle + * + * This structure encapsulates all state needed for NAT translation + * within a single component. It references shared memory structures + * for NAT tables and configuration. + */ +typedef struct nat_module +{ + /* Protocol (IPPROTO_TCP or IPPROTO_UDP) */ + uint8_t protocol; + + /* Port table reference (shared memory) — contains nat_enabled flag */ + fw_nat_port_table_t *port_table; + + /* IP address of the firewall's outbound interface */ + uint32_t interface_ip; + + /* Byte offsets for protocol-specific header parsing */ + size_t src_port_off; /* Offset to source port in transport header */ + size_t dst_port_off; /* Offset to destination port in transport header */ + size_t check_off; /* Offset to checksum in transport header */ + + /* Whether to recalculate checksum */ + bool check_enabled; + +} nat_module_t; + +/** + * Initialize the NAT module + * + * @param nat Pointer to NAT module structure to initialize + * @param interface Interface identifier (0 or 1) + * @param protocol Protocol (IPPROTO_TCP or IPPROTO_UDP) + * @param interface_config Pointer to interface configuration (shared memory) + * @param port_table Pointer to port table (shared memory) + * @param src_port_off Byte offset to source port in transport header + * @param dst_port_off Byte offset to destination port in transport header + * @param check_off Byte offset to checksum in transport header + * @param check_enabled Whether to recalculate checksums + * + * @return NAT_ERR_OKAY on success, NAT_ERR_FAILURE on error + */ +fw_nat_err_t nat_module_init(nat_module_t *nat, + uint8_t interface, + uint8_t protocol, + fw_nat_port_table_config_t *config, + fw_nat_port_table_t *port_table, + uint32_t interface_ip, + size_t src_port_off, + size_t dst_port_off, + size_t check_off, + bool check_enabled); + +/** + * Translate a packet using NAT + * + * This function performs both DNAT (for returning traffic) and SNAT (for outbound traffic). + * It modifies the packet headers in-place and updates checksums if enabled. + * + * @param nat Pointer to initialized NAT module + * @param pkt_vaddr Virtual address of packet data + * @param buffer Buffer descriptor (for potential future use) + * @param do_dnat True on the inbound (external→internal) path: attempt DNAT before SNAT. + * False on the outbound (internal→external) path: only SNAT applies. + * + * @return NAT_ERR_OKAY on success, NAT_ERR_PORT_EXHAUSTED if no ephemeral ports available, + * NAT_ERR_INVALID_PACKET if packet is malformed. + */ +fw_nat_err_t nat_module_translate(nat_module_t *nat, + uintptr_t pkt_vaddr, + net_buff_desc_t *buffer, + bool do_dnat); + diff --git a/include/lions/firewall/nat_protocol.h b/include/lions/firewall/nat_protocol.h new file mode 100644 index 000000000..71e4581b6 --- /dev/null +++ b/include/lions/firewall/nat_protocol.h @@ -0,0 +1,28 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +/* PP call parameters for webserver to enable/disable NAT on a TX virtualizer */ +typedef enum fw_nat_pp_type { + NAT_SET_ENABLED = 0, +} fw_nat_pp_type_t; + +/* Argument indices for NAT_SET_ENABLED */ +typedef enum { + NAT_SET_ENABLED_ARG_ENABLED = 0, /* bool: 1 = enable, 0 = disable */ + NAT_SET_ENABLED_NUM_ARGS, +} fw_nat_set_enabled_args_t; + +/* Return value indices */ +typedef enum { + NAT_RET_ERR = 0, +} fw_nat_ret_args_t; + +/* Error codes */ +typedef enum { + NAT_ERR_OKAY = 0, + NAT_ERR_FAILURE, +} fw_nat_err_t;