Skip to content

Firewall Implementation: NAT - #323

Open
Hazingoo wants to merge 23 commits into
mainfrom
nat_integration_clean
Open

Firewall Implementation: NAT#323
Hazingoo wants to merge 23 commits into
mainfrom
nat_integration_clean

Conversation

@Hazingoo

Copy link
Copy Markdown

Implements #188

This PR adds NAT (Network Address Translation) support to the LionsOS firewall, enabling internal network devices to share a single public IP address when communicating with external networks.

Design

The NAT implementation is split across two layers:

NAT Module (nat_module.h / nat_module.c)
A translation engine used by both the RX and TX virtualisers. Each virtualiser holds two module instances, one for TCP, one for UDP. The module handles:

  • SNAT (outbound): rewrites the internal source IP:port to the public SNAT IP and an assigned ephemeral port
  • DNAT (inbound/returning): uses the ephemeral port as a direct table index to reverse the translation back to the original internal IP:port
  • Checksum recalculation for both the IP header and TCP/UDP transport header after any modification

Port Table
A fixed pre allocated array shared between the RX and TX virtualisers for the same interface and protocol. Each slot stores the original internal IP and port, a validity flag, a last used timestamp, and a linked list pointer for the free pool. The port number itself is used as the array index (port - base_port), giving O(1) lookup with no hashing.

Webserver State
A shared memory region mapped into the RX virtualiser, TX virtualiser, and webserver simultaneously. The webserver writes the SNAT IP (configured via the web UI) and timeout value; the NAT modules read them directly. A magic number prevents double initialisation when multiple virtualisers start up.

Features to be added later

  • Timer integration: now is currently hardcoded to 0
  • Periodic cleanup: nat_module_cleanup_expired() exists but is never called, needs to be implemented.
  • Persistent SNAT IP config: the configured SNAT IP lives in runtime memory only and is lost on reboot
  • Per protocol timeouts: TCP and UDP currently share the same 5-second default timeout
  • Fragmented packet handling: assumes the transport header is present in the first fragment

@Hazingoo
Hazingoo force-pushed the nat_integration_clean branch from 3771714 to 1e0fd77 Compare May 25, 2026 03:08
Courtney3141 and others added 11 commits May 28, 2026 15:08
* Update eth/ip protocol header definitions, store all network values big-endian, calculate checksums, update virt_rx - filter/arp connection

* Add qemu and docker environment

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

---------

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
* Only calculate firewall memory region sizes if region_size is 0

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Remove systemctl from firewall docker container

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Remove unnecessary docker tun device

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Update firewall configuration variable descriptions

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Update firewall testing descriptions

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Swap firewall QEMU script argument order

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

---------

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
* Firewall: Function-ise filter protocol match index search

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Firewall: No longer hardcode NUM_INTERFACES, no internal/external hardcoding, some style changes, more support for routing out both interfaces

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Firewall: Add names to interfaces + support in sdfgen_helper

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Combine routing components + including an initial set of rules/routes in the firewall config

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Re-order makefile based on interfaces

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Firewall: Intermediate Commit (needs reviewing)

This commit implements a very simple modification to the filter actions.
To handle more than 1 external interface we have to maintain an array of
external interface instance tables. Then when checking if we match with
an instance we search through all the instance tables.

Future work will be to improve the mechanism to check if there exists
a matching instance, and address the read and write concerns.

Signed-off-by: Callum <callum.berry76@gmail.com>

* Firewall: Small Changes

Consistent use of double quotes as well as alerting on success.

Signed-off-by: Callum <callum.berry76@gmail.com>

* Firewall: Style + Minor fix

Style and ensures the meta program aligns with the struct names (using consistent fw prefix)

Extract the FW_MAX_INTERFACES into the common.h since this constant would create a cyclic include issue.
Maybe we should have a constants.h?

Signed-off-by: Callum <callum.berry76@gmail.com>

* Firewall: Remove unecessary changes

Remove modifications to the makefile.
Remove some previous modifications made to the sdfgen tool.

Signed-off-by: Callum <callum.berry76@gmail.com>

* Firewall: Create Component Abstractions

Separated the components into separate file. Separated the region abstraction.
Tested using the python type checker only issue was the C style string.
Simplifies the API that wraps the memory region and lets the user create their
own connections.

The config structs are instantiated in the component classes. Thus these components
have methods to create the various fields they need to create these configs.

Added assertions in the config creation step, and the __init__.py to satisfy
the type checking + it makes sense to validate at this stage.

Signed-off-by: Callum <callum.berry76@gmail.com>

* Firewall: Time complexity optimisation for packet queue insertion

This commit implements a small optimisation for insertion into the packet
waiting queue, improving the time complexity from O(n) to O(1).

Signed-off-by: Callum <callum.berry76@gmail.com>

* Firewall: Per interface packet waiting queue.

This PR separates the storage of packets awaiting an ARP respone,
on a per interface basis. This ensures that the Tx load on one
interface will not impact another interface due to the load on
the packet queue data structure.

Signed-off-by: Callum <callum.berry76@gmail.com>

* Add python class directory dependency

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

* Fix build directory import

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>

---------

Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>
Signed-off-by: Callum <callum.berry76@gmail.com>
Co-authored-by: Courtney Darville <courtneydarville94@outlook.com>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
…on_clean"

This reverts commit aa28da2, reversing
changes made to 14a2848.

Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
…ion_clean"

This reverts commit ede3565.

Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
@Hazingoo
Hazingoo force-pushed the nat_integration_clean branch from ae5ede9 to 9d53f17 Compare May 30, 2026 13:27
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
@Hazingoo
Hazingoo force-pushed the nat_integration_clean branch from ee30a8a to a4e1e52 Compare May 31, 2026 01:10

@Courtney3141 Courtney3141 left a comment

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.

I spent a couple of hours looking through, I have identified a number of things to work on.

  1. I would start by reading through my comments on config.h, which motivate my other comments.
  2. The main issue I have identified so far with the implementation is that you half-half whether you implement NAT on a protocol level or an interface level. My comments assume that we should implement on a protocol level, which I don't think is a bad idea. But a lot of your code assumes that NAT is either enabled or disabled on an interface - you need to fix that.
  3. If NAT is enabled, the virtualisers are required to search through their port tables, which reside in shared memory. This can actually be expensive if it is done on a per-packet basis. Thus, I think we need to implement some optimisations for the case where NAT is disabled. I described how the enabled boolean should reside in the port table itself - this still stands, but you may want to introduce a local cache for each virtualiser of whether any protocol NAT is enabled. This allows the virtualiser to skip port table searching in the totally disabled case. The virt can then rely on a ppc from the webserver (or potentially other virt) to indicate whether the cache needs to be updated. (This cache could actually be a uint8_t indicating how many protocols it is enabled on). Feel free to discuss this more with me next week if you want.
  4. I deleted some old code from ui_server.py that seemed to be lingering in your repository, however your endpoints may have depended on it. You will need to update them if so (this was old code assuming that there are only 2 interfaces, so you can't assume this any more.)

If you are confused about any of my comments so far, let me know and we can discuss online or in-person.

/* 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;
}

Comment thread include/lions/firewall/config.h Outdated
#define FW_MAX_INITIAL_FILTER_RULES 16
#define FW_MAX_INITIAL_ROUTES 16
#define FW_MAX_ARP_REQUESTER_CLIENTS 2
#define FW_MAX_NAT 16

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.

Max NAT what? what is a NAT?

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.

I see that this is the max number of port tables per interface (although this is not used consistently in the webserver config).

Since for each interface, there is one port table per protocol, I think you can use the value FW_MAX_FILTERS instead, which is the maximum number of (protocol) filters per interface. Then you can remove this new macro.

Comment thread include/lions/firewall/config.h Outdated
} fw_data_connection_resource_t;

/* NAT interface configuration (used by both RX and TX virtualizers) */
typedef struct fw_nat_interface_config {

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.

This is not an "interface" config (i.e. a config that is allocated per interface). Each interface will have multiple of these. Thus I would rename this to fw_nat_port_table_config -> as it contains all the initial information needed to use a port mapping table.

Comment thread include/lions/firewall/config.h Outdated
uint16_t base_port;
uint16_t ports_capacity;
region_resource_t port_table;
uint32_t ip;

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.

Since the IP address is fixed (source IP --> IP of the firewall on the transmission interface), this field should not be here (as it does not vary per port mapping). Since the virtualiser's do not have the interface IP address in their config by default, you can add it as a field in the fw_net_virt_[rt]x_config_t structs.

Comment thread include/lions/firewall/config.h Outdated
uint32_t ip;
} fw_nat_interface_config_t;

typedef struct fw_virt_rx_nat_config {

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.

Firstly, this name does not make sense since the type is used by both the rx and tx virtualisers. More importantly though, I don't think the protocol and enabled fields need to be in a separate struct - I think you should move them into the above struct.


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;

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.

It might be cleaner to always use the read write permission region (shouldn't do any harm if you are not modifying the packets)

cache_clean_and_invalidate(buffer_vaddr, buffer_vaddr + buffer.len);

/* Apply NAT translation if enabled */
if (fw_config.nat_enabled)

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.

NAT is performed per protocol, so you need to extract the protocol and check the corresponding table...

{
ipv4_hdr_t *ip_hdr = (ipv4_hdr_t *)(buffer_vaddr + IPV4_HDR_OFFSET);
/* RX path receives packets from external network, so they are inbound */
bool is_inbound = true;

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.

This variable is always true, I don't think you need to define it, you can just pass true directly.

bool is_inbound = true;

int nat_result = NAT_SUCCESS;
if (ip_hdr->protocol == IPV4_PROTO_TCP)

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.

This will be a for loop through your port tables, searching for the protocol number. If the protocol number is found, check the table to see if NAT is currently enabled.

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.

NOTE: We may want to have a special case, where if no NAT is enabled, we skip the search altogether (i.e. cache the value of enabled_0 || enabled_1 || ... || enabled_n)

}

/* Drop packet if NAT translation fails */
if (nat_result != NAT_SUCCESS)

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.

Perhaps we want to configure what we do if the protocol does not match with a port table entry

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.

Additionally, we may want to have a way to add a port table wildcard protocol - i.e., you receive a packet whose protocol does not have a table, it can be placed in the wildcard table.

* Copyright 2026, UNSW
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "microkit.h"

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.

Is this really required? Doesn't seem like it.

Comment thread include/lions/firewall/nat_module.h Outdated
/**
* NAT module success/error codes
*/
#define NAT_SUCCESS 0

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.

In other places in the code, we use enums for this, could you please match the surrounding style?

typedef enum {
    /* no error */
    ROUTING_ERR_OKAY = 0,
    /* data structure is full */
    ROUTING_ERR_FULL,
    /* duplicate entry exists */
    ROUTING_ERR_DUPLICATE,
    /* entry clashes with existing entry */
    ROUTING_ERR_CLASH,
    /* node ID does not point to a valid node */
    ROUTING_ERR_INVALID_ID,
    /* route is invalid */
    ROUTING_ERR_INVALID_ROUTE,
} fw_routing_err_t;

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.

Also, best to add descriptions, what is NAT_FAILURE?

#define NAT_PORT_EXHAUSTED -2
#define NAT_INVALID_PACKET -3

/* NAT timeout interval in nanoseconds */

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.

Is this the amount of time that passes before a port mapping becomes invalid? If so, 5 seconds seems a little short.

Comment thread include/lions/firewall/nat_module.h Outdated
uint64_t timeout;
} fw_nat_webserver_state_t;

/**

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.

If you have a .c file, you may as well define all your functions in the C file rather than some in the header...

Comment thread include/lions/firewall/nat_module.h Outdated
/* Since dst_port is used as an index here it must be in host byte order */
dst_port = htons(dst_port);

for (uint16_t i = 0; i < num_interfaces; i++)

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.

I don't understand why you are looking through all interfaces here, shouldn't we only look through the port mappings of the interface it was received on ?

int nat_module_translate(nat_module_t *nat,
uintptr_t pkt_vaddr,
net_buff_desc_t *buffer,
bool do_dnat)

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.

I really think these should be different functions to be honest. (Translation on the way in/out)


/* 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_config->ip)

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.

ip_hdr->dst_ip != nat->interface_config->ip this should never happen - the firewall won't send out a packet for itself.

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.

I also feel like the nat enabled check should be done once at the beginning of the function (or even before calling it).

ip_hdr->src_ip,
ip_hdr->dst_ip);

ip_hdr->check = 0;

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.

actually, this always has to be done. The reason the check enabled exists is about whether the transport layer protocol has a checksum. UDP and TCP do, ICMP doesn't. But regardless of transport layer, IP will always need to be recalculated.


ip_hdr->check = 0;
uint8_t ihl_bytes = ip_hdr->ihl * 4;
ip_hdr->check = fw_internet_checksum(ip_hdr, ihl_bytes);

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.

Suggested change
ip_hdr->check = fw_internet_checksum(ip_hdr, ihl_bytes);
ip_hdr->check = fw_internet_checksum(ip_hdr, ipv4_header_length(ip_hdr))```

fw_queue_t fw_active_clients[FW_MAX_FW_CLIENTS];

/* NAT modules for TCP and UDP */
static nat_module_t nat_tcp_module;

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.

These should really be arrays

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants