Firewall Implementation: NAT - #323
Conversation
3771714 to
1e0fd77
Compare
* 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>
…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>
ae5ede9 to
9d53f17
Compare
Signed-off-by: Harry Guan <z5479173@ad.unsw.edu.au>
ee30a8a to
a4e1e52
Compare
…arlier commits Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>
Courtney3141
left a comment
There was a problem hiding this comment.
I spent a couple of hours looking through, I have identified a number of things to work on.
- I would start by reading through my comments on config.h, which motivate my other comments.
- 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.
- 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.
- I deleted some old code from
ui_server.pythat 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); |
There was a problem hiding this comment.
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;
}| #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 |
There was a problem hiding this comment.
Max NAT what? what is a NAT?
There was a problem hiding this comment.
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.
| } fw_data_connection_resource_t; | ||
|
|
||
| /* NAT interface configuration (used by both RX and TX virtualizers) */ | ||
| typedef struct fw_nat_interface_config { |
There was a problem hiding this comment.
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.
| uint16_t base_port; | ||
| uint16_t ports_capacity; | ||
| region_resource_t port_table; | ||
| uint32_t ip; |
There was a problem hiding this comment.
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.
| uint32_t ip; | ||
| } fw_nat_interface_config_t; | ||
|
|
||
| typedef struct fw_virt_rx_nat_config { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Perhaps we want to configure what we do if the protocol does not match with a port table entry
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Is this really required? Doesn't seem like it.
| /** | ||
| * NAT module success/error codes | ||
| */ | ||
| #define NAT_SUCCESS 0 |
There was a problem hiding this comment.
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;There was a problem hiding this comment.
Also, best to add descriptions, what is NAT_FAILURE?
| #define NAT_PORT_EXHAUSTED -2 | ||
| #define NAT_INVALID_PACKET -3 | ||
|
|
||
| /* NAT timeout interval in nanoseconds */ |
There was a problem hiding this comment.
Is this the amount of time that passes before a port mapping becomes invalid? If so, 5 seconds seems a little short.
| uint64_t timeout; | ||
| } fw_nat_webserver_state_t; | ||
|
|
||
| /** |
There was a problem hiding this comment.
If you have a .c file, you may as well define all your functions in the C file rather than some in the header...
| /* 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++) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
ip_hdr->dst_ip != nat->interface_config->ip this should never happen - the firewall won't send out a packet for itself.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
| 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; |
There was a problem hiding this comment.
These should really be arrays
Signed-off-by: Courtney Darville <courtneydarville94@outlook.com>
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:
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
nowis currently hardcoded to0nat_module_cleanup_expired()exists but is never called, needs to be implemented.