Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f512a17
Add docker/qemu support for the firewall (#231)
Courtney3141 Oct 17, 2025
fbb237b
firewall: Collection of small fixes for container release (#235)
Courtney3141 Oct 22, 2025
158eb13
Firewall: Refactor the meta program and routing component. (#273)
cazb2 Mar 24, 2026
1082b7f
feat: added the NAT moduel infrastructure to main's architecture
Hazingoo Mar 27, 2026
9193b2c
feat: added the NAT translation component into virt_rx/tx
Hazingoo Mar 27, 2026
e3b2605
feat: added test_nat and configured meta.py with nat
Hazingoo Mar 27, 2026
07a8196
feat: fixed meta.py issues
Hazingoo Mar 27, 2026
200786f
feat: fixed merge conflict issues + integrated NAT module in meta config
Hazingoo May 24, 2026
a28d369
Revert "Merge remote-tracking branch 'origin/main' into nat_integrati…
Hazingoo May 25, 2026
f5c2a4b
Reapply "Merge remote-tracking branch 'origin/main' into nat_integrat…
Hazingoo May 25, 2026
9d53f17
feat: fix config parsing issue
Hazingoo May 27, 2026
a4e1e52
fix: updated submodule
Hazingoo May 30, 2026
e7fc873
feat: modified test_nat to handle multiple interfaces
Hazingoo May 31, 2026
df38de9
feat: Improved readability + added PPC + fixed port table memory region
Hazingoo Jun 5, 2026
12c9bb2
FW NAT: Revert formatting and comment deletions, remove remnants of e…
Courtney3141 Jun 5, 2026
284666e
feat: implemented NAT enable/disable with PPC
Hazingoo Jun 6, 2026
ec8419d
feat: moved NAT enabled flag to port table for protocol level consist…
Hazingoo Jun 10, 2026
f4a8e45
feat: replaced hardcoded interface string lookup in NAT endpoint
Hazingoo Jun 10, 2026
8882aee
feat: resolved comments
Hazingoo Jun 16, 2026
9f89270
feat: removed legacy code
Hazingoo Jun 16, 2026
3482f32
feat: fixed #define NAT values to use correct enum pattern
Hazingoo Jun 16, 2026
162930f
feat: removed legacy code
Hazingoo Jun 16, 2026
e29581a
More comments
Courtney3141 Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions components/micropython/modfirewall.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include <lions/firewall/filter.h>
#include <lions/firewall/ip.h>
#include <lions/firewall/routing.h>
#include <lions/firewall/nat_module.h>
#include <lions/firewall/nat_protocol.h>

#include "mpfirewallport.h"

Expand Down Expand Up @@ -509,6 +511,71 @@ 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 and RX virtualizers */
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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have now wrapped this error handling in a function, please do the same:

uint8_t interface_idx = mp_obj_get_int(interface_idx_in);
if (!check_interface_index(interface_idx)) {
    return mp_const_none;
}

if (interface_idx >= FW_NUM_INTERFACES) {
sddf_dprintf("WEBSERVER|LOG: %s\n", fw_os_err_str[OS_ERR_INVALID_INTERFACE]);
mp_raise_OSError(OS_ERR_INVALID_INTERFACE);
return mp_const_none;
}

uint8_t protocol = mp_obj_get_int(protocol_in);
bool enabled = mp_obj_is_true(enabled_in);

for (uint8_t i = 0; i < fw_config.num_nat_state; i++) {
if (fw_config.nat_state[i].protocol == protocol &&
fw_config.nat_state[i].interface == interface_idx) {
microkit_mr_set(NAT_SET_ENABLED_ARG_ENABLED, (seL4_Word)enabled);
(void)microkit_ppcall(fw_config.nat_state[i].tx_ch,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I am not so happy about calling two virtualisers. For one, it introduces a race condition. I propose that you just call one, and you designate responsibility to the "master" virtualiser for updating the enabled field in the table. Since the Tx virtualiser writes to the table much more frequently, I propose the master being the Tx virt. Then the Rx virtualiser just needs to check the table each time it receives a packet.

NOTE: Let's return to this later, as it can be needlessly expensive for a non-NAT'd Rx virtualiser to have to check the table each packet. I think we could store a cached value in the Rx virt, so if the cached variable says NAT is disabled, you don't bother checking the table. Then, the Tx virt could PPC (or signal?) the Rx virt when it changes.... We can discuss later.

Additionally, you need to check for errors here, i.e.

fw_os_err_t os_err = filter_err_to_os_err(microkit_mr_get(FILTER_RET_ERR));

microkit_msginfo_new(NAT_SET_ENABLED, NAT_SET_ENABLED_NUM_ARGS));
microkit_mr_set(NAT_SET_ENABLED_ARG_ENABLED, (seL4_Word)enabled);
(void)microkit_ppcall(fw_config.nat_state[i].rx_ch,
microkit_msginfo_new(NAT_SET_ENABLED, NAT_SET_ENABLED_NUM_ARGS));
return mp_const_none;
}
}

sddf_dprintf("WEBSERVER|LOG: %s\n", fw_os_err_str[OS_ERR_INVALID_PROTOCOL]);
mp_raise_OSError(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) — PPC call to TX virtualizer, returns False if not configured */
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_NUM_INTERFACES) {
return mp_const_false;
}

uint8_t protocol = mp_obj_get_int(protocol_in);

for (uint8_t i = 0; i < fw_config.num_nat_state; i++) {
if (fw_config.nat_state[i].protocol == protocol &&
fw_config.nat_state[i].interface == interface_idx) {
microkit_msginfo reply = microkit_ppcall(fw_config.nat_state[i].tx_ch,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the config file, this can be read directly from the table without a ppc

microkit_msginfo_new(NAT_GET_ENABLED, 0));
fw_nat_err_t err = (fw_nat_err_t)microkit_mr_get(NAT_RET_ERR);
(void)reply;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this line of code does, but regardless please handle errors in the same way as the other functions in this file. The wrappers I wrote ensure the error gets propagated to Python in a useful way.

if (err != NAT_ERR_OKAY) {
return mp_const_false;
}
return mp_obj_new_bool((bool)microkit_mr_get(NAT_RET_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) },
Expand All @@ -527,6 +594,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);
Expand Down
79 changes: 79 additions & 0 deletions examples/firewall/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
arp_eth_opcode_request,
arp_eth_opcode_response,
eththype_ip,
nat_webserver_state_region,
nat_port_table_region,
)
from pyfw.component_fw_interface import FirewallInterface

Expand Down Expand Up @@ -90,10 +92,87 @@ def generate(sdf_file: str, dtb: DeviceTree) -> None:
if not path.isdir(iface.out_dir):
assert subprocess.run(["mkdir", iface.out_dir]).returncode == 0

# Configure NAT - only enable on external interface (interface 0)
# Create shared webserver NAT state region
nat_webserver_state_mr = FirewallMemoryRegion(
"nat_webserver_state",
nat_webserver_state_region.region_size
)

# 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
)

# Configure NAT on all interfaces (required for config serialization)
# but only enable on interface 0 (external)
for iface in fw_interfaces:
# Configure TCP NAT - RX and TX share the same port table
iface.rx_virtualiser.add_nat_config_with_port_table(
protocol=0x06, # TCP
base_port=49152,
capacity=512,
interface_ip=iface.ip,
snat_ip=iface.ip,
port_table_mr=tcp_port_tables[iface.index]
)
iface.tx_virtualiser.add_nat_config_with_port_table(
protocol=0x06, # TCP
base_port=49152,
capacity=512,
interface_ip=iface.ip,
snat_ip=iface.ip,
port_table_mr=tcp_port_tables[iface.index]
)

# Configure UDP NAT - RX and TX share the same port table
iface.rx_virtualiser.add_nat_config_with_port_table(
protocol=0x11, # UDP
base_port=49152,
capacity=512,
interface_ip=iface.ip,
snat_ip=iface.ip,
port_table_mr=udp_port_tables[iface.index]
)
iface.tx_virtualiser.add_nat_config_with_port_table(
protocol=0x11, # UDP
base_port=49152,
capacity=512,
interface_ip=iface.ip,
snat_ip=iface.ip,
port_table_mr=udp_port_tables[iface.index]
)

# Set shared webserver state
iface.rx_virtualiser.set_nat_webserver_state(nat_webserver_state_mr)
iface.tx_virtualiser.set_nat_webserver_state(nat_webserver_state_mr)

# Map RX DMA region for NAT packet modification (DNAT needs write access)
iface.rx_virtualiser.set_nat_dma_region(iface.rx_dma_region)

# Enable NAT only on external interface (interface 0)
external_iface = fw_interfaces[0]
external_iface.rx_virtualiser.enable_nat()
external_iface.tx_virtualiser.enable_nat()

router = Router()
webserver = Webserver()
icmp_module = IcmpModule()

# Configure webserver NAT state (shared with network virtualizers)
webserver.add_nat_state(0x06, nat_webserver_state_mr) # TCP
webserver.add_nat_state(0x11, nat_webserver_state_mr) # UDP

# Create timer and serial subsystems
serial_node = dtb.node(board.serial)
assert serial_node is not None
Expand Down
14 changes: 10 additions & 4 deletions examples/firewall/net_components/firewall_network_components.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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/ *-//')

Expand All @@ -33,11 +33,17 @@ ${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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, but please create a similar rule for the nat object file.

${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_rx.elf: firewall_network/net_components/firewall_network_virt_rx.o firewall_network/net_components/nat_module.o |firewall_network/net_components

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to combine these two rules using a wildcard, as was previously done. You are just adding one extra dependency. Look into the definitions of the automatic variables ($@ $&lt; etc) to see if you can figure it out.

${LD} ${LDFLAGS} -o $@ firewall_network/net_components/firewall_network_virt_rx.o firewall_network/net_components/nat_module.o ${LIBS}

firewall_network_virt_tx.elf: firewall_network/net_components/firewall_network_virt_tx.o firewall_network/net_components/nat_module.o |firewall_network/net_components
${LD} ${LDFLAGS} -o $@ firewall_network/net_components/firewall_network_virt_tx.o firewall_network/net_components/nat_module.o ${LIBS}

clean::
${RM} -f firewall_network_virt_[rt]x.[od]
Expand Down
Loading
Loading