Skip to content

Instantly share code, notes, and snippets.

@Palatis
Last active January 22, 2026 16:45
Show Gist options
  • Select an option

  • Save Palatis/d8947cb416be61355cfcc4981a4b0ad8 to your computer and use it in GitHub Desktop.

Select an option

Save Palatis/d8947cb416be61355cfcc4981a4b0ad8 to your computer and use it in GitHub Desktop.
OpenWrt podman use br-lan
[engine]
hooks_dir = [
"/etc/containers/oci/hooks.d/",
]
{
"version": "1.0.0",
"hook": {
"path": "/root/bin/podman-lan-bridge-hook.sh"
},
"when": {
"annotations": {
"io.podman.annotations.lan-bridge": "true"
}
},
"stages": [ "prestart", "poststart", "poststop" ]
}
#!/usr/bin/env bash
LOG="/dev/null"
#LOG="/tmp/podman-hook.log"
PIDFILE_DIR="/var/run/podman/dhcp"
echo "=== Hook called at $(date) ===" >> "$LOG"
# Read all input
input=$(cat)
echo "Stdin: $input" >> "$LOG"
CONTAINER_ID=$(echo "$input" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
CONTAINER_PID=$(echo "$input" | sed -n 's/.*"pid":\([0-9]*\).*/\1/p')
STATUS=$(echo "$input" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
echo "Container ID: $CONTAINER_ID" >> "$LOG"
echo "Container PID: $CONTAINER_PID" >> "$LOG"
echo "Status: $STATUS" >> "$LOG"
DHCP4_PIDFILE="$PIDFILE_DIR/$CONTAINER_ID.udhcpc.pid"
DHCP6_PIDFILE="$PIDFILE_DIR/$CONTAINER_ID.odhcp6c.pid"
# Determine stage based on status
if [ "$STATUS" = "created" ] && [ -n "$CONTAINER_PID" ]; then
STAGE="prestart"
elif [ "$STATUS" = "running" ]; then
STAGE="poststart"
elif [ "$STATUS" = "stopped" ]; then
STAGE="poststop"
else
echo "Unknown stage, exiting" >> "$LOG"
exit 0
fi
echo "Stage: $STAGE" >> "$LOG"
case "$STAGE" in
prestart)
mkdir -p $PIDFILE_DIR
# IPv6
nsenter -t "$CONTAINER_PID" -a sysctl -w net.ipv6.conf.all.accept_ra=2 >> "$LOG" 2>&1
nsenter -t "$CONTAINER_PID" -a sysctl -w net.ipv6.conf.default.accept_ra=2 >> "$LOG" 2>&1
nsenter -t "$CONTAINER_PID" -a sysctl -w net.ipv6.conf.eth0.accept_ra=2 >> "$LOG" 2>&1
nsenter -t "$CONTAINER_PID" -a sysctl -w net.ipv6.conf.eth0.autoconf=1 >> "$LOG" 2>&1
;;
poststart)
# Hostname
CONTAINER_HOSTNAME=$(nsenter -t $CONTAINER_PID -u cat /proc/sys/kernel/hostname)
[ -z "$CONTAINER_HOSTNAME" ] && CONTAINER_HOSTNAME="ctr-${CONTAINER_ID:0:10}"
echo "Container Hostname: $CONTAINER_HOSTNAME" >> "$LOG"
# IPv4
nsenter -t $CONTAINER_PID -m umount /etc/resolv.conf >> "$LOG"
nsenter -t $CONTAINER_PID -a udhcpc -f -R -S -i eth0 -x hostname:$CONTAINER_HOSTNAME 2>&1 >> "$LOG" &
UDHCPC_PID=$!
echo "$UDHCPC_PID" > "$DHCP4_PIDFILE"
echo "udhcpc pid: ${UDHCPC_PID}" >> "$LOG"
# IPv6
DUID="00048e3c${CONTAINER_ID:0:26}"
nsenter -t "$CONTAINER_PID" -n -u odhcp6c -s /root/bin/dhcpv6.simple.script -c "$DUID" -t120 eth0 2>&1 >> "$LOG" &
ODHCP6C_PID=$!
echo "$ODHCP6C_PID" > "$DHCP6_PIDFILE"
echo "odhcp6c pid: ${ODHCP6C_PID}" >> "$LOG"
;;
poststop)
echo "Stopping DHCP client..." >> "$LOG"
# kill IPv4 udhcpc
[ -f "$DHCP4_PIDFILE" ] && kill $(cat "$DHCP4_PIDFILE") 2>/dev/null
rm -f "$DHCP4_PIDFILE"
# kill IPv6 odhcpc6
[ -f "$DHCP6_PIDFILE" ] && kill $(cat "$DHCP6_PIDFILE") 2>/dev/null
rm -f "$DHCP6_PIDFILE"
echo "Cleanup complete" >> "$LOG"
;;
esac
echo "=== Hook finished ===" >> "$LOG"
exit 0
#!/bin/sh
# Minimal odhcp6c script for container (IPv6 only)
# Works with RA_ADDRESSES / ADDRESSES
IF="$1"
ACTION="$2"
case "$ACTION" in
ra-updated|add|bound)
# assign addresses from ADDRESSES
for addr in $ADDRESSES; do
ip_addr=$(echo "$addr" | cut -d',' -f1)
[ -n "$ip_addr" ] && ip -6 addr add "$ip_addr" dev "$IF"
done
# set default routes from RA_ROUTES
for route in $RA_ROUTES; do
dst=$(echo "$route" | cut -d',' -f1)
gw=$(echo "$route" | cut -d',' -f2)
[ -n "$dst" ] && [ -n "$gw" ] && ip -6 route add "$dst" via "$gw" dev "$IF" || true
done
# update resolv.conf from RDNSS
if [ -n "$RDNSS" ]; then
echo > /etc/resolv.conf
for dns in $RDNSS; do
echo "nameserver $dns" >> /etc/resolv.conf
done
fi
;;
del|debound)
# remove assigned addresses
for addr in $ADDRESSES; do
ip_addr=$(echo "$addr" | cut -d',' -f1)
[ -n "$ip_addr" ] && ip -6 addr del "$ip_addr" dev "$IF"
done
;;
esac
exit 0
# podman network create --disable-dns -d bridge --ipv6 --interface-name br-lan --help --ipam-driver dhcp br-lan
# podman inspect br-lan
[
{
"name": "br-lan",
"id": "...",
"driver": "bridge",
"network_interface": "br-lan",
"created": "2026-01-20T20:37:44.707081309+08:00",
"ipv6_enabled": true,
"internal": false,
"dns_enabled": false,
"ipam_options": {
"driver": "dhcp"
}
}
]
# podman run -d --name samba --network=br-lan \
--annotation io.podman.annotations.lan-bridge=true \
-h samba \
--mac-address "11:22:33:44:55:66" \
ghcr.io/servercontainers/samba:smbd-wsdd2-latest
@Palatis
Copy link
Author

Palatis commented Jan 21, 2026

Environment

OpenWrt (25.10) + podman (5.2.2)
podman network stack: netavark

Problem

i want to run a samba / syncthing container on my OpenWrt router, as if they're a physical machine plugged into the LAN port.
the container should try to obtain it's ip address from the dhcp server running on the router (dnsmasq & odhcpd).

{ Internet } --[br-wan]-- ( Router ) --[br-lan]--+-- lan1@eth0 -- desktop computer
                                                 +-- lan2@eth1 -- notebook
                                                 +-- lan3@eth2 --
                                                 +-- lan4@eth3 --
                                                 +-- veth0 -- samba container
                                                 +-- veth1 -- syncthing container
                                                 +-- vethN -- ...

the ip addresses are managed with dhcp/dns server running on the router, and netwokr access is protected by router's firewall rules.

Solution

  1. use podman hooks to run a script for our manual network setup
  2. the script obtain the container id and hostname, request an IP under container's namespace, and set it to container's veth interface.
    • send hostname to dhcp so the router knows who we are
    • veth mac is randomly generated during device creation, so use container id for dhcp 0x3d (device id) field, so the server assigns the same ip to the container, instead of a different ip everytime.
    • give it a persistent mac with podman --mac-address "11:22:33:44:55:66"
    • DUID mimics linux does for physical NIC (starts with 00048e3c), since we're in a linux container.

Usage

  1. modify /etc/containers/containers.conf, add the hooks.
  2. place the hook JSON file under /etc/containers/oci/hooks.d/
  3. place the actual hook script somewhere it can find (as what defined in the lan-bridge.json)
  4. place the dhcpv6.simple.script somewhere the hook script can find (see line#63)
  5. use the provided podman network create command to create a network
    • you can podman inspect <network_name> to check the configuration
  6. use the provided podman run command to create a container
    • annotate with io.podman.annotations.lan-bridge=true so the container will use the hook

FAQ

  1. Why not use macvlan?
    • macvlan in bridge mode cannot talk to the host.
    • cannot create macvlan device with a parent which is already a bridge (br-lan)
    • macvlan needs a physical port with carrier, which might not present.

TODO

  • DNS: udhcpc ran with nsenter cannot replace /etc/resolv.conf inside a container, dunno how.
    • the file is mounted, so just enter the NS and umount it first.
  • IPv6: udhcpc should obtain a v6 address, however it's not, dunno why.
    • use odhcp6c for the task, and sysctl parameters needed to enable autoconf.
  • maybe, if netavark dhcp-proxy can do the task properly then when don't need this hook.
    • convert this to a netavark plugin and submit upstream?

@Palatis
Copy link
Author

Palatis commented Jan 21, 2026

Issues

  • i was going to use podman inspect to obtain the hostname, as it might be more reliable. however can't use that in a hook script because inside hooks the container is in a limbo state and podman inspect just hang forever (kill -9 to stop it).
  • hostname can only be obtained during poststart phase. it's not available during prestart, the command just retrieve router's hostname instead container's hostname during prestart.
  • cannot just podman exec $CONTAINER_ID udhcpc, container don't have the permission to modify network settings.
  • was going to use logger so things logs into syslog, but dunno why a simple logger hello doesn't write anything to logread.

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