Proof of concept: netns for WAN isolation and wireguard VPN

Wireguard has a pesky behavior where the UDP socket is locked to the context the interface is created in. this means that associating a WAN interface with a VRF is not possible to also associate a wireguard interface, even with ip rules. I found that the outbound handshakes will exit the VRF/table, but the replies are dropped since the UDP socket exists in the default VRF.

However, Wireguard supports namespaces, just like macvlans, where the logical interface can be moved to any namespace and the Socket or physical interface can exist in another name space. To leverage this I created a few scripts to enable route isolation between multiple WAN/untrusted connections and allow wireguard to use these isolated routes, while allowing the VPN routes to exist in the default namespace (such as using BGP)
A feature of this approach which uses macvlans to attach the physical interfaces into the namespaces allows any interface driver, including wifi station interfaces, to be used.

by using hotplug, it allows the interfaces to be controlled/reconfigured by luci/other tools simply by starting/stopping the uci interface. making it reliable and consistent

The 3 scripts operate in this way

  1. netns script simply reats the uci netns config to gather configured namespaces and create them
  2. netns-veth creates a veth pair between each namespace and the host, and sets up static routes if specified (this is necessary if we need to manage the webui of a modem, for example)
  3. hotplug script runs if the interfaces is listed in netns uci configuration to set it up for two roles: wan, wireguard

The hotplug script does the following for wan roles on hotplug ifup

  1. create a macvlan interface associated with the wan physical interface as passthru
  2. move this linked macvlan to the associated name space
  3. sets up a static ip and gateway (if configured)
  4. starts udhcpcd and odhcp6c (if configured)
  5. configures a secondary staticip (if configured, even if dhcp is configured, i found some modems that assign a public IP but use a RFC1918 address for web management, need the interface to also have this subnet configured)
  6. configures the namespace with a light set of hardned sysctl and firewall rules for the macvlan, including masquerading

Native openwrt uci interfaces should be configured as unmanaged with no ip address. interfaces for the default namespace (named veth-) should also be created and assigned the WAN. Port forwarding isn't supported for this use case, however would be possible by extending the script to take portforward options and configure the namespace nftables to forward them to the default namespace address of the veth pair

For wireguard interfaces, on hotplug ifup

  1. read the wireguard uci config for the interface
  2. remove the wireguard interface from the default namespace
  3. recreate the wireguard interface in the associated namespace
  4. move the wireguard interface to the default namespace
  5. assign the wireguard uci configured IP information

It doesn't consider setting up routes based on wireguard allowed IP, I just allow 0.0.0.0,::/0, I use BGP and so should you.

Here's an example config:

cat /etc/config/netns
config netns 'wan'
        option netns 'sat'
        option interface 'wan'
        option type 'wan'
        option hostname 'hddjejdjdbdbdj'
        option broadcast '1'
        option client_id '0001000123456789abcdef012345'
        option dhcp '1'
        option dhcp6 '1'
        list routes '192.168.100.1/32'
        option secondaryip '192.168.100.10/24'

config netns 'wg0'
        option netns 'sat'
        option interface 'wg0'
        option type 'wireguard'

config netns 'wwan0'
        option netns 'cell'
        option interface 'wwan0'
        option type 'wan'
        option hostname '5fjihhbvv'
        option broadcast '0'
        option client_id '0001000123456789abcdef012345'
        option dhcp '1'
        option dhcp6 '1'
        list routes '192.168.3.1/32'

config netns 'wg1'
        option netns 'cell'
        option interface 'wg1'
        option type 'wireguard'

config netns 'trm_wwan'
        option netns 'wifi'
        option interface 'trm_wwan'
        option type 'wan'
        option hostname 'asdfasdfsf'
        option broadcast '0'
        option client_id '0001000123456789abcdef012345'
        option dhcp '1'
        option dhcp6 '1'
        list routes '192.168.12.163/32'

config netns 'wg2'
        option netns 'wifi'
        option interface 'wg2'
        option type 'wireguard'

here is the netns service

cat /etc/init.d/netns
#!/bin/sh /etc/rc.common

START=19
STOP=90

USE_PROCD=1

# Logging function
log() {
    logger -t netns "$@"
}

setup_namespace() {
    local cfg=$1
    local netns nsname

    log "Processing netns config section: $cfg"

    # Get config values
    config_get netns "$cfg" netns
    nsname=$netns

    # Create namespace if it doesn't exist
    if ! ip netns list | grep -q "${nsname}-ns"; then
        log "Creating namespace ${nsname}-ns"
        ip netns add ${nsname}-ns
        if [ $? -ne 0 ]; then
            log "Failed to create namespace ${nsname}-ns"
            return 1
        fi
    else
        log "Namespace ${nsname}-ns already exists"
    fi
}

start_service() {
    log "Starting netns service"

    # Clean up existing namespaces
    stop_service

    # Load netns config
    log "Loading netns UCI configuration"
    config_load netns

    # Process each netns configuration
    log "Processing netns configurations"
    config_foreach setup_namespace netns
    log "Netns service startup complete"
}

stop_service() {
    log "Stopping netns service"
    # Delete all namespaces ending with -ns
    for ns in $(ip netns list | awk '{print $1}'); do
        log "Processing namespace $ns"
        # Only process namespaces ending with -ns
        if echo "$ns" | grep -q '\-ns$'; then
            # Delete namespace
            log "Deleting namespace $ns"
            ip netns delete "$ns"
            if [ $? -eq 0 ]; then
                log "Deleted namespace $ns"
            else
                log "Failed to delete namespace $ns"
            fi
        else
            log "Skipping namespace $ns (does not end with -ns)"
        fi
    done
    log "Netns service stopped"
}

restart_service() {
    log "Restarting netns service"
    stop_service
    start_service
    log "Netns service restarted"
}

service_triggers() {
    procd_add_reload_trigger "netns"
}

here is the netns-veth service

cat /etc/init.d/netns-veth
#!/bin/sh /etc/rc.common

START=91
STOP=09

USE_PROCD=1

# Logging function
log() {
    logger -t netns-veth "$@"
}

# Global variable to track processed namespaces
PROCESSED_NAMESPACES=""

create_veth_pair() {
    local nsname=$1
    local ns_ip=$2
    local host_ip=$3
    local veth_name="veth-${nsname}"

    log "Creating veth pair for namespace ${nsname}-ns, ns_ip=$ns_ip, host_ip=$host_ip"

    # Check if veth-<nsname> exists in host namespace
    if ip link show "$veth_name" >/dev/null 2>&1; then
        log "$veth_name already exists in host namespace, attempting to delete"
        ip link set "$veth_name" down
        ip link delete "$veth_name"
        if [ $? -ne 0 ]; then
            log "Failed to delete existing $veth_name"
            return 1
        fi
        log "Deleted existing $veth_name"
    fi

    # Create veth pair
    ip link add "$veth_name" type veth peer name veth-host
    if [ $? -eq 0 ]; then
        log "veth pair $veth_name/veth-host created successfully"
    else
        log "Failed to create veth pair for ${nsname}"
        return 1
    fi

    # Move peer to namespace
    ip link set veth-host netns ${nsname}-ns
    if [ $? -eq 0 ]; then
        log "Moved veth-host to ${nsname}-ns"
    else
        log "Failed to move veth-host to ${nsname}-ns"
        return 1
    fi

    # Configure host side
    ip addr add ${host_ip}/30 dev "$veth_name"
    ip link set "$veth_name" up
    if [ $? -eq 0 ]; then
        log "Configured $veth_name with ${host_ip}/30 and set up"
    else
        log "Failed to configure $veth_name"
        return 1
    fi

    # Configure namespace side
    ip netns exec ${nsname}-ns ip addr add ${ns_ip}/30 dev veth-host
    ip netns exec ${nsname}-ns ip link set veth-host up
    if [ $? -eq 0 ]; then
        log "Configured veth-host in ${nsname}-ns with ${ns_ip}/30 and set up"
    else
        log "Failed to configure veth-host in ${nsname}-ns"
        return 1
    fi
}

setup_veth_and_routes() {
    local cfg=$1
    local netns routes nsname

    log "Processing netns config section: $cfg"

    # Get config values
    config_get netns "$cfg" netns
    nsname=$netns

    # Check if namespace has already been processed
    if echo "$PROCESSED_NAMESPACES" | grep -qw "$nsname"; then
        log "Warning: Namespace $nsname already processed, skipping section $cfg (possible UCI configuration error)"
        return 0
    fi

    # Get routes for this specific section
    routes=$(uci -q get netns."$cfg".routes | tr '\n' ' ' | sed 's/[[:space:]]*$//')
    log "Config values: netns=$netns, routes='$routes'"

    # Skip if no routes are defined
    if [ -z "$routes" ]; then
        log "No routes defined for $nsname in section $cfg, skipping veth creation"
        return 0
    fi

    # Mark namespace as processed
    PROCESSED_NAMESPACES="$PROCESSED_NAMESPACES $nsname"

    # Check if namespace exists
    if ! ip netns list | grep -q "${nsname}-ns"; then
        log "Namespace ${nsname}-ns does not exist, skipping"
        return 1
    fi

    # Calculate unique IPs in 169.254.0.0/16 space for /30 subnets
    local idx=$(ip netns list | grep -n "${nsname}-ns" | cut -d: -f1)
    # Each /30 subnet uses 4 IPs, so calculate the subnet base
    local subnet_base=$(( (idx - 1) * 4 ))
    # Calculate the second and third IPs (usable hosts) in the /30 subnet
    local ns_ip_octet=$((subnet_base % 256))
    local ns_ip_third_octet=$((subnet_base / 256))
    local ns_ip="169.254.$ns_ip_third_octet.$((ns_ip_octet + 1))"  # Second IP
    local host_ip="169.254.$ns_ip_third_octet.$((ns_ip_octet + 2))" # Third IP
    log "Calculated IPs: ns_ip=$ns_ip, host_ip=$host_ip, idx=$idx, subnet_base=$subnet_base"

    # Create veth pair
    create_veth_pair "$nsname" "$ns_ip" "$host_ip" || return 1

    # Verify veth is up
    if ip link show "veth-${nsname}" | grep -q "state UP"; then
        log "veth-${nsname} is up and ready for routing"
    else
        log "veth-${nsname} is not up, route addition may fail"
    fi

    # Add routes to default namespace, explicitly associating with veth-${nsname}
    for route in $routes; do
        local veth_name="veth-${nsname}"
        log "Adding route $route via $ns_ip dev $veth_name in default namespace"
        # Check if route already exists
        if ip route show "$route" | grep -q "via $ns_ip dev $veth_name"; then
            log "Route $route via $ns_ip dev $veth_name already exists, skipping"
            continue
        fi
        ip route add "$route" via "$ns_ip" dev "$veth_name"
        if [ $? -eq 0 ]; then
            log "Route $route added successfully"
        else
            log "Failed to add route $route: $(ip route add "$route" via "$ns_ip" dev "$veth_name" 2>&1)"
        fi
        # Log current routing table for debugging
        log "Current routing table: $(ip route show | grep "$route")"
    done
}

start_service() {
    log "Starting netns-veth service"

    # Reset processed namespaces
    PROCESSED_NAMESPACES=""

    # Clean up existing veth interfaces and routes
    stop_service

    # Load netns config
    log "Loading netns UCI configuration"
    config_load netns

    # Process each netns configuration
    log "Processing netns-veth configurations"
    config_foreach setup_veth_and_routes netns
    log "Netns-veth service startup complete"
}

stop_service() {
    log "Stopping netns-veth service"

    # Load netns config to get namespaces defined in UCI
    log "Loading netns UCI configuration"
    config_load netns

    # Process each netns configuration to clean up associated veth interfaces and routes
    cleanup_veth_and_routes() {
        local cfg=$1
        local netns nsname

        # Get config values
        config_get netns "$cfg" netns
        nsname=$netns
        local veth_name="veth-${nsname}"

        log "Cleaning up veth and routes for namespace ${nsname}-ns"

        # Check if veth interface exists in host namespace
        if ip link show "$veth_name" >/dev/null 2>&1; then
            # Remove routes pointing to the veth interface
            log "Removing routes for $veth_name"
            ip route show | grep "dev $veth_name" | while read -r route; do
                local route_dest=$(echo "$route" | awk '{print $1}')
                ip route del "$route_dest"
                if [ $? -eq 0 ]; then
                    log "Deleted route for $veth_name: $route_dest"
                else
                    log "Failed to delete route for $veth_name: $route_dest"
                fi
            done
            # Bring down the veth interface
            log "Bringing down $veth_name"
            ip link set "$veth_name" down
            if [ $? -eq 0 ]; then
                log "$veth_name set down"
            else
                log "Failed to set $veth_name down"
            fi
            # Delete veth interface
            log "Deleting $veth_name in default namespace"
            ip link delete "$veth_name"
            if [ $? -eq 0 ]; then
                log "Deleted $veth_name"
            else
                log "Failed to delete $veth_name"
            fi
        else
            log "No $veth_name found in default namespace"
        fi

        # Check if namespace exists and clean up veth-host
        if ip netns list | grep -q "${nsname}-ns"; then
            if ip netns exec "${nsname}-ns" ip link show veth-host >/dev/null 2>&1; then
                log "Found veth-host in ${nsname}-ns, deleting"
                ip netns exec "${nsname}-ns" ip link set veth-host down
                ip netns exec "${nsname}-ns" ip link delete veth-host
                if [ $? -eq 0 ]; then
                    log "Deleted veth-host in ${nsname}-ns"
                else
                    log "Failed to delete veth-host in ${nsname}-ns"
                fi
            else
                log "No veth-host found in ${nsname}-ns"
            fi
        else
            log "Namespace ${nsname}-ns does not exist, skipping veth-host cleanup"
        fi
    }

    # Process each netns configuration
    log "Processing netns configurations for cleanup"
    config_foreach cleanup_veth_and_routes netns

    log "Netns-veth service stopped"
}

restart_service() {
    log "Restarting netns-veth service"
    stop_service
    start_service
    log "Netns-veth service restarted"
}

service_triggers() {
    procd_add_reload_trigger "netns"
}

eventually, I will work on an openwrt package to extend uci/luci to enable namespaces for interface and firewall configuration, so it could possibly allow injecting "ip netns exec " infront of the commands if the advanced "netns" feature is configured. as well as specify the "create netns" and "destination netns" for interfaces. But for now, these do exactly what I need.

3 Likes

and finally the hotplug script

cat /etc/hotplug.d/iface/90-netns-wan
#!/bin/sh
# /etc/hotplug.d/iface/90-wan-netns
# Hotplug script to manage interfaces in network namespaces using script-managed macvlan for wan and wireguard

[ "$ACTION" = "ifup" -o "$ACTION" = "ifdown" ] || exit 0

# Load OpenWrt functions
. /lib/functions.sh
. /lib/functions/network.sh

# Logger tag
LOG_TAG="netns-hotplug"

logger -t "$LOG_TAG" "Hotplug triggered: ACTION=$ACTION, INTERFACE=$INTERFACE"

# Resolve physical device
network_get_device PHYSICAL_DEVICE "$INTERFACE"
[ -z "$PHYSICAL_DEVICE" ] && {
    logger -t "$LOG_TAG" "Error: Could not resolve physical device for $INTERFACE"
    exit 0
}
logger -t "$LOG_TAG" "Resolved $INTERFACE to physical device $PHYSICAL_DEVICE"

# Function to process WireGuard peer configuration
config_wireguard_peer() {
    local cfg="$1"
    local interface="$2"
    local private_key="$3"
    local listen_port="$4"
    local public_key preshared_key allowed_ips endpoint_host endpoint_port persistent_keepalive

    # Get peer options
    config_get public_key "$cfg" public_key
    config_get preshared_key "$cfg" preshared_key
    config_get allowed_ips "$cfg" allowed_ips
    config_get endpoint_host "$cfg" endpoint_host
    config_get endpoint_port "$cfg" endpoint_port
    config_get persistent_keepalive "$cfg" persistent_keepalive

    logger -t "$LOG_TAG" "Processing peer $cfg: public_key=$public_key, endpoint=$endpoint_host:$endpoint_port, allowed_ips='$allowed_ips', preshared_key=(hidden), keepalive=$persistent_keepalive"

    # Configure peer if required options are present
    if [ -n "$public_key" ] && [ -n "$allowed_ips" ]; then
        # Format allowed_ips by joining with commas
        local formatted_ips=$(echo "$allowed_ips" | tr ' ' ',' | sed 's/,+/,/g; s/^,//; s/,$//')

        # Check IPv6 support in default namespace
        local use_ipv6=1
        if sysctl -n net.ipv6.conf.all.disable_ipv6 | grep -q "1"; then
            use_ipv6=0
            logger -t "$LOG_TAG" "IPv6 disabled in default namespace, filtering out IPv6 allowed_ips"
            # Filter out IPv6 addresses from allowed_ips
            local filtered_ips=""
            for ip in $allowed_ips; do
                if ! echo "$ip" | grep -q ":"; then
                    [ -n "$filtered_ips" ] && filtered_ips="$filtered_ips,$ip" || filtered_ips="$ip"
                fi
            done
            formatted_ips="$filtered_ips"
        fi
        logger -t "$LOG_TAG" "Final allowed_ips for peer $cfg: '$formatted_ips'"

        # Create temporary file for private key
        local privkey_file="/tmp/privkey-$$"
        echo "$private_key" > "$privkey_file"

        # Create temporary file for preshared key (if present)
        local psk_file="/tmp/wg-psk-$cfg-$$"
        [ -n "$preshared_key" ] && echo "$preshared_key" > "$psk_file"

        # Build single wg set command
        local wg_cmd="wg set $PHYSICAL_DEVICE private-key $privkey_file"
        [ -n "$listen_port" ] && wg_cmd="$wg_cmd listen-port $listen_port"
        wg_cmd="$wg_cmd peer $public_key allowed-ips '$formatted_ips'"
        [ -n "$endpoint_host" ] && [ -n "$endpoint_port" ] && wg_cmd="$wg_cmd endpoint $endpoint_host:$endpoint_port"
        [ -n "$preshared_key" ] && wg_cmd="$wg_cmd preshared-key $psk_file"
        [ -n "$persistent_keepalive" ] && wg_cmd="$wg_cmd persistent-keepalive $persistent_keepalive"

        logger -t "$LOG_TAG" "Configuring WireGuard for $PHYSICAL_DEVICE: $wg_cmd"
        sh -c "$wg_cmd" 2> /tmp/wg-error-$$ || {
            local wg_error=$(cat /tmp/wg-error-$$)
            logger -t "$LOG_TAG" "Error: Failed to configure $PHYSICAL_DEVICE with private key and peer $public_key (allowed_ips '$formatted_ips'): $wg_error"
            [ -f "$privkey_file" ] && rm -f "$privkey_file"
            [ -f "$psk_file" ] && rm -f "$psk_file"
            rm -f /tmp/wg-error-$$
            exit 1
        }

        # Clean up temporary files
        [ -f "$privkey_file" ] && rm -f "$privkey_file"
        [ -f "$psk_file" ] && rm -f "$psk_file"
    else
        logger -t "$LOG_TAG" "Skipping peer $cfg: missing public_key or allowed_ips"
    fi
}

# Process netns configuration
process_netns() {
    local cfg="$1"
    local interface netns_name type
    config_get interface "$cfg" interface
    config_get netns_name "$cfg" netns
    config_get type "$cfg" type

    # Skip if interface doesn't match
    [ "$interface" != "$INTERFACE" ] && return 0

    # Append -ns to namespace name
    netns_name="${netns_name}-ns"

    logger -t "$LOG_TAG" "Processing $INTERFACE: namespace=$netns_name, type=$type"

    # Ensure namespace exists
    if ! ip netns list | grep -q "$netns_name"; then
        logger -t "$LOG_TAG" "Creating namespace $netns_name"
        ip netns add "$netns_name" || {
            logger -t "$LOG_TAG" "Error: Failed to create namespace $netns_name"
            exit 1
        }
    fi

    # Enable IP forwarding in the namespace
    logger -t "$LOG_TAG" "Enabling IP forwarding in $netns_name"
    ip netns exec "$netns_name" sysctl -w net.ipv4.ip_forward=1
    ip netns exec "$netns_name" sysctl -w net.ipv6.conf.all.forwarding=1

    # Apply security sysctls
    logger -t "$LOG_TAG" "Applying security sysctls in $netns_name"
    ip netns exec "$netns_name" sysctl -w net.ipv4.conf.all.rp_filter=1
    ip netns exec "$netns_name" sysctl -w net.ipv6.conf.all.rp_filter=1
    ip netns exec "$netns_name" sysctl -w net.ipv4.conf.all.accept_source_route=0
    ip netns exec "$netns_name" sysctl -w net.ipv6.conf.all.accept_source_route=0
    ip netns exec "$netns_name" sysctl -w net.ipv4.conf.all.accept_redirects=0
    ip netns exec "$netns_name" sysctl -w net.ipv6.conf.all.accept_redirects=0

    # Handle interface based on type
    case "$type" in
        wan)
            # Define macvlan interface name
            local macvlan_if="macvlan-${netns_name%%-ns}"  # Remove -ns suffix
            local udhcpc_pid="/var/run/udhcpc-$macvlan_if.pid"
            local odhcp6c_pid="/var/run/odhcp6c-$macvlan_if.pid"

            if [ "$ACTION" = "ifup" ]; then
                # Get netns options
                config_load netns
                local hostname broadcast client_id dhcp dhcp6 secondaryip primaryip gateway
                config_get hostname "$cfg" hostname
                config_get broadcast "$cfg" broadcast
                config_get client_id "$cfg" client_id
                config_get dhcp "$cfg" dhcp
                config_get dhcp6 "$cfg" dhcp6
                config_get secondaryip "$cfg" secondaryip
                config_get primaryip "$cfg" primaryip
                config_get gateway "$cfg" gateway
                logger -t "$LOG_TAG" "Netns options: hostname=$hostname, broadcast=$broadcast, client_id=$client_id, dhcp=$dhcp, dhcp6=$dhcp6, secondaryip=$secondaryip, primaryip=$primaryip, gateway=$gateway"

                # Remove IPs from physical device
                logger -t "$LOG_TAG" "Removing IPs from $PHYSICAL_DEVICE"
                ip addr flush dev "$PHYSICAL_DEVICE"

                # Cleanup existing macvlan in both namespaces
                logger -t "$LOG_TAG" "Cleaning up existing macvlan interface $macvlan_if"
                ip netns exec "$netns_name" ip link delete "$macvlan_if" 2>/dev/null
                ip link delete "$macvlan_if" 2>/dev/null

                # Get MAC address of physical device
                local phy_mac
                phy_mac=$(ip link show "$PHYSICAL_DEVICE" | grep ether | awk '{print $2}')
                [ -z "$phy_mac" ] && {
                    logger -t "$LOG_TAG" "Error: Could not retrieve MAC address for $PHYSICAL_DEVICE"
                    exit 1
                }
                logger -t "$LOG_TAG" "Retrieved MAC address for $PHYSICAL_DEVICE: $phy_mac"

                # Create macvlan interface in default namespace
                logger -t "$LOG_TAG" "Creating $macvlan_if in default namespace"
                ip link add name "$macvlan_if" link "$PHYSICAL_DEVICE" type macvlan mode passthru || {
                    logger -t "$LOG_TAG" "Error: Failed to create $macvlan_if"
                    exit 1
                }

                # Move macvlan interface to namespace
                logger -t "$LOG_TAG" "Moving $macvlan_if to namespace $netns_name"
                ip link set "$macvlan_if" netns "$netns_name" || {
                    logger -t "$LOG_TAG" "Error: Failed to move $macvlan_if to $netns_name"
                    ip link delete "$macvlan_if" 2>/dev/null
                    exit 1
                }

                # Configure macvlan in namespace
                ip netns exec "$netns_name" ip link set "$macvlan_if" address "$phy_mac"
                ip netns exec "$netns_name" ip link set "$macvlan_if" up

                # Apply interface-specific security sysctls
                logger -t "$LOG_TAG" "Applying interface-specific security sysctls for $macvlan_if"
                ip netns exec "$netns_name" sysctl -w net.ipv4.conf.$macvlan_if.rp_filter=1
                ip netns exec "$netns_name" sysctl -w net.ipv6.conf.$macvlan_if.rp_filter=1
                ip netns exec "$netns_name" sysctl -w net.ipv4.conf.$macvlan_if.accept_source_route=0
                ip netns exec "$netns_name" sysctl -w net.ipv6.conf.$macvlan_if.accept_source_route=0
                ip netns exec "$netns_name" sysctl -w net.ipv4.conf.$macvlan_if.accept_redirects=0
                ip netns exec "$netns_name" sysctl -w net.ipv6.conf.$macvlan_if.accept_redirects=0
                ip netns exec "$netns_name" sysctl -w net.ipv4.conf.$macvlan_if.proxy_arp=0

                # Configure static IP addresses and gateway if specified
                if [ -n "$secondaryip" ]; then
                    logger -t "$LOG_TAG" "Adding secondary IP $secondaryip to $macvlan_if in $netns_name"
                    ip netns exec "$netns_name" ip addr add "$secondaryip" dev "$macvlan_if" || {
                        logger -t "$LOG_TAG" "Error: Failed to add secondary IP $secondaryip to $macvlan_if"
                        exit 1
                    }
                fi

                if [ "$dhcp" != "1" ] && [ -n "$primaryip" ]; then
                    logger -t "$LOG_TAG" "Adding primary IP $primaryip to $macvlan_if in $netns_name (DHCP disabled)"
                    ip netns exec "$netns_name" ip addr add "$primaryip" dev "$macvlan_if" || {
                        logger -t "$LOG_TAG" "Error: Failed to add primary IP $primaryip to $macvlan_if"
                        exit 1
                    }
                fi

                if [ -n "$gateway" ]; then
                    logger -t "$LOG_TAG" "Configuring default gateway $gateway for $macvlan_if in $netns_name"
                    ip netns exec "$netns_name" ip route add default via "$gateway" || {
                        logger -t "$LOG_TAG" "Error: Failed to configure default gateway $gateway"
                        exit 1
                    }
                fi

                # Apply nftables rules in the namespace
                logger -t "$LOG_TAG" "Applying nftables rules for $macvlan_if in $netns_name"
                ip netns exec "$netns_name" nft flush ruleset
                ip netns exec "$netns_name" nft -f - <<EOF
table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state invalid drop
        iifname "veth-host" accept
        iifname "$macvlan_if" meta l4proto udp th sport 67 th dport 68 limit rate 10/second accept  # IPv4 DHCP
        iifname "$macvlan_if" meta l4proto udp th sport 547 th dport 546 limit rate 10/second accept # IPv6 DHCPv6
        iifname "$macvlan_if" ip6 saddr fe80::/10 icmpv6 type 134 limit rate 5/second accept         # Router Advertisements
        iifname "$macvlan_if" ip6 saddr fe80::/10 icmpv6 type { 135, 136 } limit rate 10/second accept # NS/NA
        iifname "$macvlan_if" icmpv6 type 1 limit rate 10/second accept                              # Dest Unreach
        ct state established,related accept
        iifname "$macvlan_if" log prefix "dropped_input: " drop
    }
    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state invalid drop
        iifname "veth-host" oifname "$macvlan_if" ct state new,established,related accept
        iifname "veth-host" ip6 saddr 2001:db8:100::/64 oifname "$macvlan_if" ct state new,established,related accept
        iifname "$macvlan_if" oifname "veth-host" ct state established,related accept
        log prefix "dropped_forward: " drop
    }
}
table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        oifname "$macvlan_if" masquerade
    }
}
EOF
                [ $? -eq 0 ] || {
                    logger -t "$LOG_TAG" "Error: Failed to apply nftables rules in $netns_name"
                    exit 1
                }

                # Run DHCP clients in namespace based on netns options
                if [ "$dhcp" = "1" ]; then
                    local udhcpc_cmd="udhcpc -p $udhcpc_pid -t 0 -i $macvlan_if"
                    [ -n "$hostname" ] && udhcpc_cmd="$udhcpc_cmd -x hostname:$hostname"
                    udhcpc_cmd="$udhcpc_cmd -C -b"
                    [ "$broadcast" = "1" ] && udhcpc_cmd="$udhcpc_cmd -B"
                    udhcpc_cmd="$udhcpc_cmd -O 121"

                    logger -t "$LOG_TAG" "Starting udhcpc in $netns_name on $macvlan_if: $udhcpc_cmd"
                    ip netns exec "$netns_name" $udhcpc_cmd || {
                        logger -t "$LOG_TAG" "Error: Failed to start udhcpc in $netns_name"
                        exit 1
                    }
                else
                    logger -t "$LOG_TAG" "Skipping udhcpc: dhcp option is not enabled ($dhcp)"
                fi

                if [ "$dhcp6" = "1" ]; then
                    local odhcp6c_cmd="odhcp6c -p $odhcp6c_pid -Ntry -P0"
                    [ -n "$client_id" ] && odhcp6c_cmd="$odhcp6c_cmd -c $client_id"
                    odhcp6c_cmd="$odhcp6c_cmd -t120 -d $macvlan_if"

                    logger -t "$LOG_TAG" "Starting odhcp6c in $netns_name on $macvlan_if: $odhcp6c_cmd"
                    ip netns exec "$netns_name" $odhcp6c_cmd || {
                        logger -t "$LOG_TAG" "Error: Failed to start odhcp6c in $netns_name"
                        exit 1
                    }
                else
                    logger -t "$LOG_TAG" "Skipping odhcp6c: dhcp6 option is not enabled ($dhcp6)"
                fi
            elif [ "$ACTION" = "ifdown" ]; then
                # Clear nftables rules in the namespace
                logger -t "$LOG_TAG" "Clearing nftables rules in $netns_name"
                ip netns exec "$netns_name" nft flush ruleset 2>/dev/null

                # Kill DHCP clients using PID files
                logger -t "$LOG_TAG" "Killing DHCP clients for $macvlan_if"
                if [ -f "$udhcpc_pid" ]; then
                    local pid=$(cat "$udhcpc_pid" 2>/dev/null)
                    [ -n "$pid" ] && {
                        logger -t "$LOG_TAG" "Killing udhcpc (PID $pid) for $macvlan_if"
                        kill "$pid" 2>/dev/null
                        rm -f "$udhcpc_pid"
                    }
                fi
                if [ -f "$odhcp6c_pid" ]; then
                    local pid=$(cat "$odhcp6c_pid" 2>/dev/null)
                    [ -n "$pid" ] && {
                        logger -t "$LOG_TAG" "Killing odhcp6c (PID $pid) for $macvlan_if"
                        kill "$pid" 2>/dev/null
                        rm -f "$odhcp6c_pid"
                    }
                fi

                # Delete macvlan from namespace
                logger -t "$LOG_TAG" "Deleting $macvlan_if from $netns_name"
                ip netns exec "$netns_name" ip link delete "$macvlan_if" 2>/dev/null
            fi
            ;;
        wireguard)
            if [ "$ACTION" = "ifup" ]; then
                # Cleanup existing WireGuard interface in both namespaces
                logger -t "$LOG_TAG" "Cleaning up existing $PHYSICAL_DEVICE interface in $netns_name"
                ip netns exec "$netns_name" ip link delete "$PHYSICAL_DEVICE" 2>/dev/null
                logger -t "$LOG_TAG" "Cleaning up existing $PHYSICAL_DEVICE interface in default namespace"
                ip link delete "$PHYSICAL_DEVICE" 2>/dev/null

                # Parse UCI network config for WireGuard interface
                config_load network
                local private_key listen_port addresses mtu
                config_get private_key "$INTERFACE" private_key
                config_get listen_port "$INTERFACE" listen_port
                config_get addresses "$INTERFACE" addresses
                config_get mtu "$INTERFACE" mtu
                logger -t "$LOG_TAG" "Interface $INTERFACE options: private_key=(hidden), listen_port=$listen_port, addresses=$addresses, mtu=$mtu"

                # Create WireGuard interface in namespace
                logger -t "$LOG_TAG" "Creating $PHYSICAL_DEVICE in namespace $netns_name"
                ip netns exec "$netns_name" ip link add "$PHYSICAL_DEVICE" type wireguard || {
                    logger -t "$LOG_TAG" "Error: Failed to create $PHYSICAL_DEVICE in $netns_name"
                    exit 1
                }

                # Move WireGuard interface to default namespace
                logger -t "$LOG_TAG" "Moving $PHYSICAL_DEVICE from $netns_name to default namespace"
                ip netns exec "$netns_name" ip link set "$PHYSICAL_DEVICE" netns 1 || {
                    logger -t "$LOG_TAG" "Error: Failed to move $PHYSICAL_DEVICE to default namespace"
                    exit 1
                }

                # Assign IP addresses in default namespace
                logger -t "$LOG_TAG" "Assigning addresses to $PHYSICAL_DEVICE in default namespace"
                [ -n "$addresses" ] && {
                    for addr in $addresses; do
                        logger -t "$LOG_TAG" "Adding address $addr to $PHYSICAL_DEVICE"
                        ip addr add "$addr" dev "$PHYSICAL_DEVICE" || {
                            logger -t "$LOG_TAG" "Error: Failed to add address $addr to $PHYSICAL_DEVICE"
                        }
                    done
                }

                # Set MTU in default namespace
                [ -n "$mtu" ] && {
                    logger -t "$LOG_TAG" "Setting MTU $mtu on $PHYSICAL_DEVICE"
                    ip link set "$PHYSICAL_DEVICE" mtu "$mtu"
                }

                # Set interface up in default namespace
                logger -t "$LOG_TAG" "Setting $PHYSICAL_DEVICE up in default namespace"
                ip link set "$PHYSICAL_DEVICE" up || {
                    logger -t "$LOG_TAG" "Error: Failed to set $PHYSICAL_DEVICE up"
                    exit 1
                }

                # Process WireGuard peer configuration (includes private key)
                logger -t "$LOG_TAG" "Configuring private key and peers for $INTERFACE"
                config_foreach config_wireguard_peer "wireguard_$INTERFACE" "$INTERFACE" "$private_key" "$listen_port"
            elif [ "$ACTION" = "ifdown" ]; then
                # Remove WireGuard interface
                logger -t "$LOG_TAG" "Removing $PHYSICAL_DEVICE interface on ifdown"
                ip link delete "$PHYSICAL_DEVICE" 2>/dev/null
            fi
            ;;
        *)
            logger -t "$LOG_TAG" "Unknown type $type for $INTERFACE, skipping"
            ;;
    esac

    # Log namespace state
    logger -t "$LOG_TAG" "Namespace $netns_name interfaces: $(ip netns exec $netns_name ip link)"
    logger -t "$LOG_TAG" "Namespace $netns_name routes: $(ip netns exec $netns_name ip route)"
}

# Load and process netns config
config_load netns
config_foreach process_netns netns

logger -t "$LOG_TAG" "Completed processing for $INTERFACE"

Here's some example of the veth pairs and the macvlan

#ip add
64: phy0-sta0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether a4:34:d9:66:ed:8d brd ff:ff:ff:ff:ff:ff
75: veth-sat@if74: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether da:9a:c9:40:dc:b6 brd ff:ff:ff:ff:ff:ff link-netns sat-ns
    inet 169.254.0.10/30 scope global veth-sat
       valid_lft forever preferred_lft forever
    inet6 fe80::d89a:c9ff:fe40:dcb6/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
77: veth-cell@if76: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 2e:ca:5a:c3:10:6a brd ff:ff:ff:ff:ff:ff link-netns cell-ns
    inet 169.254.0.6/30 scope global veth-cell
       valid_lft forever preferred_lft forever
    inet6 fe80::2cca:5aff:fec3:106a/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
79: veth-wifi@if78: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether a6:a5:80:42:6e:3a brd ff:ff:ff:ff:ff:ff link-netns wifi-ns
    inet 169.254.0.2/30 scope global veth-wifi
       valid_lft forever preferred_lft forever
    inet6 fe80::a4a5:80ff:fe42:6e3a/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
ip ns exec wifi-ns ip add
Object "ns" is unknown, try "ip help".
root@nvpthwangw02:/# ip netns exec wifi-ns ip add
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
78: veth-host@if79: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether f2:20:44:e8:52:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 169.254.0.1/30 scope global veth-host
       valid_lft forever preferred_lft forever
    inet6 fe80::f020:44ff:fee8:52ee/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
98: macvlan-wifi@if64: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether a4:34:d9:66:ed:8d brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.2.227/24 brd 192.168.2.255 scope global macvlan-wifi
       valid_lft forever preferred_lft forever
    inet6 fe80::a634:d9ff:fe66:ed8d/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever

requires kmod-netns ip-full kmod-macvlan