diff --git a/net/jool/Makefile b/net/jool/Makefile index 8021d43584f3a..98f37d03e2a1b 100644 --- a/net/jool/Makefile +++ b/net/jool/Makefile @@ -112,6 +112,19 @@ define Package/jool-tools-netfilter/description This package provides the userspace control programs for Jool. endef +define Package/jool-clat-proto + $(call Package/jool/Default) + TITLE:=Jool-based CLAT netifd proto helper + DEPENDS:=+ip-full +jool-tools-netfilter +kmod-veth +endef + +define Package/jool-clat-proto/description + $(call Package/jool/Default/description) + + This package provides a netifd proto helper for CLAT setups using Jool + SIIT in a dedicated network namespace. +endef + JOOL_AUTOLOAD:= \ jool \ jool_siit @@ -145,5 +158,11 @@ define Package/jool-tools-netfilter/install $(INSTALL_DATA) ./files/readme.md $(1)/etc/jool/readme.md endef +define Package/jool-clat-proto/install + $(INSTALL_DIR) $(1)/lib/netifd/proto + $(INSTALL_BIN) ./files/jool_clat.sh $(1)/lib/netifd/proto/jool_clat.sh +endef + $(eval $(call KernelPackage,jool-netfilter)) $(eval $(call BuildPackage,jool-tools-netfilter)) +$(eval $(call BuildPackage,jool-clat-proto)) diff --git a/net/jool/files/jool_clat.sh b/net/jool/files/jool_clat.sh new file mode 100644 index 0000000000000..3e80068df0842 --- /dev/null +++ b/net/jool/files/jool_clat.sh @@ -0,0 +1,286 @@ +#!/bin/sh +# +# netifd proto helper for 464XLAT CLAT using Jool SIIT in a network namespace. +JOOL_DEFAULT_VETH_HOST_V4="192.0.2.0" +JOOL_DEFAULT_VETH_NS_V4="192.0.2.1" + +[ -n "$INCLUDE_ONLY" ] || { + . /lib/functions.sh + . ../netifd-proto.sh + init_proto "$@" +} + +jool_clat_proto_log() { + local cfg="$1" + shift + + logger -t "jool-clat-proto[$cfg]" -- "$*" +} + +jool_clat_proto_fail() { + local cfg="$1" + local code="$2" + shift 2 + + [ "$#" -gt 0 ] && jool_clat_proto_log "$cfg" "$*" + proto_notify_error "$cfg" "$code" + proto_block_restart "$cfg" + proto_setup_failed "$cfg" + return 1 +} + +jool_clat_proto_wait_linklocal() { + local netns="$1" + local ifname="$2" + local addr tries=0 + + while [ "$tries" -lt 5 ]; do + if [ -n "$netns" ]; then + addr="$(ip netns exec "$netns" ip -6 -o addr show scope link dev "$ifname" | awk '$0 !~ / tentative( |$)/ { print $4; exit }' | cut -d/ -f1)" + else + addr="$(ip -6 -o addr show scope link dev "$ifname" | awk '$0 !~ / tentative( |$)/ { print $4; exit }' | cut -d/ -f1)" + fi + [ -n "$addr" ] && { + echo "$addr" + return 0 + } + + tries=$((tries + 1)) + sleep 1 + done + + return 1 +} + +jool_clat_proto_word_count() { + local count=0 + local word + + for word in $1; do + count=$((count + 1)) + done + + echo "$count" +} + +jool_clat_proto_run() { + local cfg="$1" + local message="$2" + shift 2 + + "$@" || { + jool_clat_proto_log "$cfg" "$message" + return 1 + } +} + +jool_clat_proto_cleanup() { + local cfg="$1" + local veth_host_ifname="$2" + local netns="$3" + + [ -n "$netns" ] && ip netns exec "$netns" jool_siit instance remove >/dev/null 2>&1 + [ -n "$netns" ] && ip netns del "$netns" >/dev/null 2>&1 + [ -n "$veth_host_ifname" ] && ip link del dev "$veth_host_ifname" >/dev/null 2>&1 +} + +jool_clat_proto_setup_instance() { + local cfg="$1" + local plat_prefix="$2" + local veth_host_v4="$3" + local veth_ns_v4="$4" + local veth_host_ifname="$5" + local veth_ns_ifname="$6" + local netns="$7" + local clat_v4="$8" + local clat_v6="$9" + local veth_host_v6 veth_ns_v6 veth_host_mac veth_ns_mac + local prefix4 prefix6 + + # Ref: https://blog.lingxh.com/post/464xlat/ + # https://www.jool.mx/en/464xlat.html + + # Set up netns and veth pair. + jool_clat_proto_run "$cfg" "failed to create namespace '$netns'" \ + ip netns add "$netns" || return 1 + jool_clat_proto_run "$cfg" "failed to create veth pair '$veth_host_ifname'/'$veth_ns_ifname'" \ + ip link add name "$veth_host_ifname" type veth peer name "$veth_ns_ifname" || return 1 + jool_clat_proto_run "$cfg" "failed to move '$veth_ns_ifname' into '$netns'" \ + ip link set dev "$veth_ns_ifname" netns "$netns" || return 1 + # Bring the host veth up now so we can learn its link-local address before + # handing the interface to netifd. + jool_clat_proto_run "$cfg" "failed to bring up '$veth_host_ifname'" \ + ip link set dev "$veth_host_ifname" up || return 1 + jool_clat_proto_run "$cfg" "failed to bring up loopback in '$netns'" \ + ip netns exec "$netns" ip link set dev lo up || return 1 + jool_clat_proto_run "$cfg" "failed to bring up '$veth_ns_ifname' in '$netns'" \ + ip netns exec "$netns" ip link set dev "$veth_ns_ifname" up || return 1 + + veth_host_v6="$(jool_clat_proto_wait_linklocal "" "$veth_host_ifname")" || { + jool_clat_proto_log "$cfg" "failed to discover link-local IPv6 on '$veth_host_ifname'" + return 1 + } + veth_ns_v6="$(jool_clat_proto_wait_linklocal "$netns" "$veth_ns_ifname")" || { + jool_clat_proto_log "$cfg" "failed to discover link-local IPv6 on '$veth_ns_ifname'" + return 1 + } + + veth_host_mac="$(cat "/sys/class/net/$veth_host_ifname/address")" || { + jool_clat_proto_log "$cfg" "failed to read MAC address for '$veth_host_ifname'" + return 1 + } + veth_ns_mac="$(ip netns exec "$netns" cat "/sys/class/net/$veth_ns_ifname/address")" || { + jool_clat_proto_log "$cfg" "failed to read MAC address for '$veth_ns_ifname' in '$netns'" + return 1 + } + + # Prepopulate the namespace-side neighbor entry to avoid relying on ARP + # and NDP, which may require extra firewall rules to work. + # The host-side neighbor entries are populated later via proto_add_ipv[46]_neighbor. + jool_clat_proto_run "$cfg" "failed to populate namespace IPv6 neighbor entry for '$veth_host_v6'" \ + ip netns exec "$netns" ip -6 neigh replace "$veth_host_v6" lladdr "$veth_host_mac" dev "$veth_ns_ifname" nud permanent || return 1 + jool_clat_proto_run "$cfg" "failed to populate namespace IPv4 neighbor entry for '$veth_host_v4'" \ + ip netns exec "$netns" ip -4 neigh replace "$veth_host_v4" lladdr "$veth_host_mac" dev "$veth_ns_ifname" nud permanent || return 1 + jool_clat_proto_run "$cfg" "failed to assign '$veth_ns_v4' peer '$veth_host_v4' on '$veth_ns_ifname'" \ + ip netns exec "$netns" ip addr add "$veth_ns_v4" peer "$veth_host_v4" dev "$veth_ns_ifname" || return 1 + # Route translated 4to6 traffic back to the host, where it is expected to be + # routed toward the PLAT. + jool_clat_proto_run "$cfg" "failed to install namespace default route" \ + ip netns exec "$netns" ip -6 route replace default via "$veth_host_v6" dev "$veth_ns_ifname" || return 1 + + # Route translated 6to4 return traffic back to the host. + for prefix4 in $clat_v4; do + [ "$prefix4" = "$veth_host_v4" ] && continue + jool_clat_proto_run "$cfg" "failed to install namespace route for '$prefix4'" \ + ip netns exec "$netns" ip route replace "$prefix4" via "$veth_host_v4" dev "$veth_ns_ifname" || return 1 + done + + jool_clat_proto_run "$cfg" "failed to enable namespace IPv4 forwarding" \ + ip netns exec "$netns" sysctl -q -w net.ipv4.conf.all.forwarding=1 || return 1 + jool_clat_proto_run "$cfg" "failed to enable namespace IPv6 forwarding" \ + ip netns exec "$netns" sysctl -q -w net.ipv6.conf.all.forwarding=1 || return 1 + + jool_clat_proto_run "$cfg" "failed to create jool_siit instance in '$netns'" \ + ip netns exec "$netns" jool_siit instance add --netfilter --pool6 "$plat_prefix" || return 1 + + # Prefix counts are validated earlier, so each IPv4 prefix maps to the + # next IPv6 prefix in order. + set -- $clat_v6 + for prefix4 in $clat_v4; do + prefix6="$1" + shift + jool_clat_proto_run "$cfg" "failed to map '$prefix4' to '$prefix6'" \ + ip netns exec "$netns" jool_siit eamt add --force "$prefix4" "$prefix6" || return 1 + done + + # Prevent Jool SIIT from capturing INPUT traffic toward the ns veth itself + # (e.g. ping for test). + jool_clat_proto_run "$cfg" "failed to add denylist" \ + ip netns exec "$netns" jool_siit denylist4 add "${veth_ns_v4}" || return 1 + + JOOL_CLAT_VETH_NS_V6="$veth_ns_v6" + JOOL_CLAT_VETH_NS_MAC="$veth_ns_mac" + return 0 +} + +proto_jool_clat_init_config() { + available=1 + no_device=1 + + proto_config_add_string "plat_prefix" + proto_config_add_array "clat_v4:list(string)" + proto_config_add_array "clat_v6:list(string)" + proto_config_add_string "veth_host_v4" + proto_config_add_string "veth_ns_v4" + proto_config_add_string "veth_host_ifname" + proto_config_add_string "veth_ns_ifname" + proto_config_add_string "netns" + proto_config_add_boolean "defaultroute" + proto_config_add_int "metric" +} + +proto_jool_clat_setup() { + local cfg="$1" + local plat_prefix veth_host_v4 veth_ns_v4 veth_host_ifname veth_ns_ifname netns defaultroute metric + local clat_v4 clat_v6 + local prefix_count4 prefix_count6 prefix6 + + json_get_vars plat_prefix veth_host_v4 veth_ns_v4 veth_host_ifname veth_ns_ifname netns defaultroute metric + json_get_values clat_v4 clat_v4 + json_get_values clat_v6 clat_v6 + + [ -n "$plat_prefix" ] || { + jool_clat_proto_fail "$cfg" MISSING_PLAT_PREFIX "missing required option plat_prefix" + return 1 + } + veth_host_v4="${veth_host_v4:-$JOOL_DEFAULT_VETH_HOST_V4}" + veth_ns_v4="${veth_ns_v4:-$JOOL_DEFAULT_VETH_NS_V4}" + + veth_host_ifname="${veth_host_ifname:-veth-$cfg}" + veth_ns_ifname="${veth_ns_ifname:-$veth_host_ifname-peer}" + netns="${netns:-ns-$cfg}" + defaultroute="${defaultroute:-1}" + clat_v4="${clat_v4:-$veth_host_v4}" + + prefix_count6="$(jool_clat_proto_word_count "$clat_v6")" + [ "$prefix_count6" -gt 0 ] || { + jool_clat_proto_fail "$cfg" MISSING_CLAT_V6 "at least one clat_v6 is required" + return 1 + } + + prefix_count4="$(jool_clat_proto_word_count "$clat_v4")" + [ "$prefix_count4" -eq "$prefix_count6" ] || { + jool_clat_proto_fail "$cfg" PREFIX_COUNT_MISMATCH "clat_v4 and clat_v6 must have the same number of entries" + return 1 + } + + if [ -e "/sys/class/net/$veth_host_ifname" ]; then + jool_clat_proto_fail "$cfg" HOST_IF_EXISTS "host interface '$veth_host_ifname' already exists" + return 1 + fi + if ip netns list | awk '{ print $1 }' | grep -Fxq "$netns"; then + jool_clat_proto_fail "$cfg" NETNS_EXISTS "network namespace '$netns' already exists" + return 1 + fi + + modprobe jool_siit >/dev/null 2>&1 + [ -x /usr/bin/jool_siit ] || { + jool_clat_proto_fail "$cfg" MISSING_BINARY "missing /usr/bin/jool_siit" + return 1 + } + + jool_clat_proto_setup_instance \ + "$cfg" "$plat_prefix" "$veth_host_v4" "$veth_ns_v4" "$veth_host_ifname" "$veth_ns_ifname" \ + "$netns" "$clat_v4" "$clat_v6" || { + jool_clat_proto_cleanup "$cfg" "$veth_host_ifname" "$netns" + jool_clat_proto_fail "$cfg" SETUP_FAILED "setup failed; see logread for details" + return 1 + } + + proto_init_update "$veth_host_ifname" 1 + proto_add_ipv4_address "$veth_host_v4" "" "" "$veth_ns_v4" + [ "$defaultroute" = 0 ] || proto_add_ipv4_route "0.0.0.0" 0 "$veth_ns_v4" "" "$metric" + proto_add_ipv4_neighbor "$veth_ns_v4" "$JOOL_CLAT_VETH_NS_MAC" + proto_add_ipv6_neighbor "$JOOL_CLAT_VETH_NS_V6" "$JOOL_CLAT_VETH_NS_MAC" + + for prefix6 in $clat_v6; do + proto_add_ipv6_route "${prefix6%/*}" "${prefix6#*/}" "$JOOL_CLAT_VETH_NS_V6" + done + + proto_send_update "$cfg" +} + +proto_jool_clat_teardown() { + local cfg="$1" + local veth_host_ifname netns + + json_get_vars veth_host_ifname netns + veth_host_ifname="${veth_host_ifname:-veth-$cfg}" + netns="${netns:-ns-$cfg}" + + jool_clat_proto_cleanup "$cfg" "$veth_host_ifname" "$netns" +} + +[ -n "$INCLUDE_ONLY" ] || { + add_protocol jool_clat +} diff --git a/net/jool/files/readme.md b/net/jool/files/readme.md index 0638d786e7dd5..3aea5d56fdec5 100644 --- a/net/jool/files/readme.md +++ b/net/jool/files/readme.md @@ -17,7 +17,7 @@ This package includes a start script that will: 3. Run `jool` with procd ### For now this means that - + - The services will be disabled by default in the uci config `(/etc/config/jool)` - The only uci configuration support available for the package is to enable or disable each instance or the entire deamon - There is no uci support and configuration will be saved at `/etc/jool/` @@ -30,6 +30,86 @@ The configuration files the startup script uses for each jool instance are: - jool(nat64): `/etc/jool/jool-nat64.conf.json` - jool(siit): `/etc/jool/jool-siit.conf.json` +### netifd proto (clat) + +The optional `jool-clat-proto` package ships a `jool_clat` proto helper for +CLAT setups using Jool SIIT inside a dedicated network namespace, with one end +of a veth pair exposed on the host. This allows translation of traffic sourced +from the router itself and more flexible routing, which are infeasible when +running Jool SIIT directly because it hooks PREROUTING and processes traffic +indiscriminately. + +Example `/etc/config/network` section: + +```uci +config interface 'clat0' + option proto 'jool_clat' + option plat_prefix '64:ff9b::/96' + option clat_v6 '2001:db8:100::2' + option defaultroute '1' # default + option metric '1470' +``` + +
+ Example of `/etc/config/firewall` section. + +```uci +config zone + option name 'clat' + list network 'clat0' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option masq '1' + +config forwarding + option src 'lan' + option dest 'clat' + +config forwarding + option src 'clat' + option dest 'wan' # where PLAT resides +``` +
+ +
+Example of `/etc/config/network` section with more options. + +```uci +config interface 'clat1' + option proto 'jool_clat' + option plat_prefix '64:ff9b::/96' + option veth_host_v4 '192.0.2.0' + option veth_ns_v4 '192.0.2.1' + # map explicit subnets exhaustively to avoid extra masquerade + list clat_v6 '2001:db8:100::/120' + list clat_v4 '192.168.1.0/24' + list clat_v6 '2001:db8:101::0/128' + list clat_v4 '192.0.2.0' + option defaultroute '0' +``` +
+ +Supported proto options: + +- `plat_prefix`: required PLAT (Jool pool6) prefix. +- `clat_v4` and `clat_v6`: arrays of IPv6/IPv4 address/prefix(es) mapped one-to-one in order, to be added as Jool's EAMT entries. + - `clat_v6`: required IPv6 address/prefix(es) used by the CLAT and mapped from `clat_v4`. + - `clat_v4`: optional IPv4 address/prefix(es) mapped to `clat_v6`. If omitted, it defaults to `veth_host_v4`; in that case, the CLAT interface only accepts traffic sourced from its own IPv4 address unless additional MASQUERADE is applied. +- `veth_host_v4`, `veth_ns_v4`: optional addresses for the host and namespace ends of the CLAT veth pair. If omitted, they default to `192.0.2.0` and `192.0.2.1`. +- `veth_host_ifname`, `veth_ns_ifname`, `netns`: optional names for the host veth, namespace veth, and network namespace. +- `defaultroute`: optional boolean, defaults to `1`. When enabled, the host side installs an IPv4 default route via the CLAT veth interface. +- `metric`: optional metric for the default IPv4 route installed on the host side. + +Notes: + +- Ensure the network namespace kernel feature is enabled. This appears to be the default on official builds. +- Enable IPv6 forwarding on the host (`net.ipv6.conf.all.forwarding=1`). This also appears to be the default on official builds. +- Configure OUTPUT and FORWARD firewall rules properly to allow traffic to pass through the CLAT interface. +- Apply MASQUERADE on the CLAT interface if you want to translate traffic sourced from LAN hosts or other local IPv4 addresses. +- To avoid extra MASQUERADE, specify `clat_v4` manually with all the IPv4 subnet(s) you want to map (usually the interface IP and LAN subnets) and provide the corresponding `clat_v6` entries yourself. +- With `mwan3` enabled or more complex policy routing rules, using MASQUERADE may be simpler. In that case, you may also want to add a custom policy rule, for example `uci add network rule && uci set network.@rule[-1].out='clat0' && uci set network.@rule[-1].lookup='main' && uci commit network && service network reload`, to make `ping -I` (`SO_BINDDEVICE`) testing easier. + ### OpenWrt tutorial For a more detailed tutorial refer to this [wiki page](https://openwrt.org/docs/guide-user/network/ipv6/nat64).