git: 644990f193cc - main - New version of jng (9.0)
Devin Teske <[email protected]>
| Newsgroups | gmane.os.freebsd.devel.cvs.src |
|---|---|
| Message-ID | <6a84aae6.3f7ed.446c48ae__28341.0927209601$1787079417$gmane$org@gitrepo.freebsd.org> |
The branch main has been updated by dteske: URL: https://cgit.FreeBSD.org/src/commit/?id=644990f193cc284db241125e567519cafa1c7ccd commit 644990f193cc284db241125e567519cafa1c7ccd Author: Devin Teske <[email protected]> AuthorDate: 2026-08-18 18:54:36 +0000 Commit: Devin Teske <[email protected]> CommitDate: 2026-08-18 18:54:36 +0000 New version of jng (9.0) Changes for jng 2.0 -> 9.0 include: + Use ng_bridge(4) uplink hooks on ng_ether(4) lower so the host mapping table stays small (first hook is uplink; unknown unicast goes only to uplink) + Add `jng pin [-h] {-a | NAME ...}' to plant eiface MACs with NGM_BRIDGE_MOVE_HOST and raise maxStaleness so they do not expire + Remove experimental NG_TYPE=iface / ng_tcpmss(4); ng_iface(4) cannot work with ng_bridge(4) + Add -v + SPDX-License-Identifier: BSD-2-Clause; bump copyright to 2026 See D58902 for the ng_bridge(4) data-path MOVE_HOST fix. MFC after: 1 week Reviewed by: kfv, jlduran Differential Revision: https://reviews.freebsd.org/D58903 --- share/examples/jails/jng | 347 +++++++++++++++++++++++++++++++---------------- 1 file changed, 232 insertions(+), 115 deletions(-) diff --git a/share/examples/jails/jng b/share/examples/jails/jng index 03f5c318199c..82f1dd7b5e94 100755 --- a/share/examples/jails/jng +++ b/share/examples/jails/jng @@ -1,33 +1,13 @@ #!/bin/sh -#- -# Copyright (c) 2016-2024 Devin Teske <[email protected]> -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2016-2026 Devin Teske <[email protected]> # ############################################################ IDENT(1) # # $Title: netgraph(4) management script for vnet jails $ -# $Version: 2.0 $ +# $Version: 9.0 $ # ############################################################ INFORMATION # @@ -84,6 +64,7 @@ # ### BEGIN EXCERPT ### # # jail_enable="YES" +# #jail_confwarn="NO" # Optional: disable warning to migrate to jail.conf(5) # jail_list="xxx" # # # @@ -127,30 +108,32 @@ # jail_{name}_devfs_ruleset="11" (rc.conf(5)). # # NB: While this tool can't create every type of desirable topology, it should -# handle most setups, minus some which considered exotic or purpose-built. +# handle most setups, minus some considered exotic or purpose-built. +# +# Uplink on ng_ether(4) `lower' (jng 7+) keeps the WAN MAC table small: +# ng_bridge(4) does not learn on uplink hooks. The first connected hook being +# uplink also selects restrictive unknown-unicast: frames for an unknown dest +# go only to uplink, not to jail links. Inbound unicast to a jail therefore +# requires that jail's MAC to live in the forwarding database (FDB) on the +# jail's link. jng 8 pins each eiface MAC with ngctl movehost and sets +# maxStaleness so host->staleness cannot catch it. `jng pin NAME' replants +# after an accidental move. ng_bridge must not MOVE_HOST from learnMac=0 hooks +# or promiscuous TX echo can steal a pinned MAC onto uplink; without that +# kernel fix, re-run `jng pin'. # ############################################################ CONFIGURATION # -# Netgraph node type. Can be `iface' or `eiface' and refers to whether -# ng_iface(4) or ng_eiface(4) is used with ng_bridge(4). The advantages of -# choosing iface over eiface is that with iface you can utilize ng_tcpmss(4) -# to limit the TCP MSS for operating in environments that clamp down on ICMP. -# -# NB: iface/tcpmss support is EXPERIMENTAL -# -NG_TYPE=eiface # Can be iface or eiface - -# -# Clamp TCP Maximum Segment Size to reasonably below standard MTU -# NB: Fixes TCP hangup issue in environments where ICMP is restricted -# NB: Be liberal about MSS (RFC 879, section 7) -# NB: Unused unless NG_TYPE=iface +# host->staleness is uint16_t; conf.maxStaleness is uint32_t. +# ng_bridge_timeout expires when ++staleness >= maxStaleness. +# A threshold above 65535 is unreachable (the counter wraps). # -NG_TCPMSS_CONFIG='{ inHook="bridge" outHook="'$NG_TYPE'" maxMSS=1280 }' +NG_BRIDGE_MAX_STALENESS=4294967295 ############################################################ GLOBALS +VERSION='$Version: 9.0 $' + pgm="${0##*/}" # Program basename # @@ -168,22 +151,35 @@ STATS_FMT=text # -j for JSON quietly(){ "$@" > /dev/null 2>&1; } +die() +{ + local fmt="$1" + if [ "$fmt" ]; then + shift 1 # fmt + printf "%s: $fmt\n" "$pgm" "$@" >&2 + fi + exit $FAILURE +} + usage() { local fmt="$1" + local optfmt="\t%-5s %s\n" local action usage descr exec >&2 if [ "$fmt" ]; then shift 1 # fmt printf "%s: $fmt\n" "$pgm" "$@" fi - echo "Usage: $pgm [-h] action [arguments]" - echo "Options:" - printf "\t-h Print usage statement and exit.\n" - echo "Actions:" + printf "Usage: %s [-hv] action [arguments]\n" "$pgm" + printf "Options:\n" + printf "$optfmt" "-h" "Print this usage statement and exit." + printf "$optfmt" "-v" "Print version information and exit." + printf "Actions:\n" for action in \ bridge \ graph \ + pin \ show \ show1 \ shutdown \ @@ -194,7 +190,7 @@ usage() eval descr=\"\$jng_${action}_descr\" printf "\t%s\n\t\t%s\n" "$usage" "$descr" done - exit $FAILURE + die } action_usage() @@ -206,10 +202,10 @@ action_usage() printf "%s: %s: $fmt\n" "$pgm" "$action" "$@" >&2 fi eval usage=\"\$jng_${action}_usage\" - echo "Usage: $pgm $usage" >&2 + printf "Usage: %s %s\n" "$pgm" "$usage" >&2 eval descr=\"\$jng_${action}_descr\" - printf "\t%s\n" "$descr" - exit $FAILURE + printf "\t%s\n" "$descr" >&2 + die } derive_mac() @@ -235,7 +231,7 @@ derive_mac() # # Calculate MAC address derived from given iface. # - # The formula I'm using is ``NP:SS:SS:II:II:II'' where: + # The formula used is ``NP:SS:SS:II:II:II'' where: # + N denotes 4 bits used as a counter to support branching # each parent interface up to 15 times under the same jail # name (see S below). @@ -246,7 +242,7 @@ derive_mac() # + I denotes bits that are inherited from parent interface. # # The S bits are a CRC-16 checksum of NAME, allowing the jail - # to change link numbers in ng_bridge(4) without affecting the + # to change link numbers in ng_bridge(4) without effecting the # MAC address. Meanwhile, if... # + the jail NAME changes (e.g., it was duplicated and given # a new name with no other changes) @@ -259,7 +255,7 @@ derive_mac() # ??:??:??:II:II:II __new_devid=${__iface_devid#??:??:??} # => :II:II:II # => :SS:SS:II:II:II - __num=$( set -- `echo -n "$__name" | sum` && echo $1 ) + __num=$( set -- $( echo -n "$__name" | sum ) && echo $1 ) __new_devid=$( printf :%02x:%02x \ $(( $__num >> 8 & 255 )) $(( $__num & 255 )) )$__new_devid # => P:SS:SS:II:II:II @@ -293,10 +289,100 @@ derive_mac() mustberoot_to_continue() { - if [ "$( id -u )" -ne 0 ]; then - echo "Must run as root!" >&2 - exit $FAILURE + [ "$( id -u )" -eq 0 ] || die "Must run as root!" +} + +jng_bridge_has_uplink() +{ + ngctl show "$1:" 2> /dev/null | awk ' + $1 ~ /^uplink/ { found = 1; exit } + END { exit !found } + ' # END-QUOTE +} + +jng_bridge_persist_hosts() +{ + local node="$1" + local debug=0 loop=60 stable=1 config + + eval $( ngctl msg "$node:" getconfig 2> /dev/null | awk ' + { + if (match($0, /debugLevel=[0-9]+/)) + printf "debug=%s ", + substr($0, RSTART + 11, RLENGTH - 11) + if (match($0, /loopTimeout=[0-9]+/)) + printf "loop=%s ", + substr($0, RSTART + 12, RLENGTH - 12) + if (match($0, /minStableAge=[0-9]+/)) + printf "stable=%s ", + substr($0, RSTART + 13, RLENGTH - 13) + } + ' ) + config="debugLevel=$debug" + config="$config loopTimeout=$loop" + config="$config maxStaleness=$NG_BRIDGE_MAX_STALENESS" + config="$config minStableAge=$stable" + quietly ngctl msg "$node:" setconfig "{ $config }" +} + +jng_pin_mac() +{ + local node="$1" mac="$2" hook="$3" + + [ "$node" -a "$mac" -a "$hook" ] || return $FAILURE + quietly ngctl msg "$node:" movehost "{ addr=$mac hook=\"$hook\" }" +} + +jng_jiface_mac() +{ + local __jiface="$1" __jail="$2" __var_to_set="$3" + local __mac + + __mac=$( ifconfig "$__jiface" ether 2> /dev/null | + awk '/ether/ { print $2; exit }' ) + if [ ! "$__mac" -a "$__jail" ]; then + # + # After vnet.interface takes the eiface, it is gone + # from the host ifconfig; netgraph node remains. + # + __mac=$( jexec "$__jail" ifconfig "$__jiface" ether \ + 2> /dev/null | awk '/ether/ { print $2; exit }' ) fi + eval $__var_to_set=\"\$__mac\" + [ "$__mac" ] +} + +jng_pin_jiface() +{ + local jiface="$1" jail="$2" + local mac peer peerhook pbridge phook + + jng_jiface_mac "$jiface" "$jail" mac || return $FAILURE + + # ether <peer> bridge <id> <peerhook> + set -- $( ngctl show "$jiface:" 2> /dev/null | awk ' + $3 == "bridge" { print $2, $5; exit } + ' ) + peer="$1" peerhook="$2" + [ "$peer" -a "$peerhook" ] || return $FAILURE + + jng_pin_mac "$peer" "$mac" "$peerhook" || return + if jng_bridge_has_uplink "$peer"; then + jng_bridge_persist_hosts "$peer" || : persist optional + return + fi + + # + # Secondary bridge: also pin on the parent that holds uplink + # (restrictive unknown-unicast lives there). + # + set -- $( ngctl show "$peer:" 2> /dev/null | awk ' + $3 == "bridge" { print $2, $5; exit } + ' ) + pbridge="$1" phook="$2" + [ "$pbridge" -a "$phook" ] || return $SUCCESS + jng_pin_mac "$pbridge" "$mac" "$phook" || return + jng_bridge_persist_hosts "$pbridge" || : persist optional } jng_bridge_usage="bridge [-h] [-b BRIDGE_NAME] NAME [!|=]iface0 [[!|=]iface1 ...]" @@ -326,7 +412,7 @@ jng_bridge() mustberoot_to_continue local iface parent jiface jiface_devid - local new clone_mac no_derive num quad i=0 + local new clone_mac no_derive num quad mtu i=0 for iface in $*; do clone_mac= @@ -351,77 +437,49 @@ jng_bridge() ngctl msg $iface: setautosrc 0 || return # Make sure the interface has been bridged + # NB: You must connect uplinkX before linkX + # NB: see ng_bridge(4) for policy on first connected hook if ! quietly ngctl info ${iface}bridge:; then - ngctl mkpeer $iface: bridge lower link0 || return - ngctl connect $iface: $iface:lower upper link1 || + ngctl mkpeer $iface: bridge lower uplink1 || return + ngctl connect $iface: $iface:lower upper link0 || return ngctl name $iface:lower ${iface}bridge || return + jng_bridge_persist_hosts ${iface}bridge || + : persist optional fi - mtu=$(ifconfig ${iface} | sed -n '1s/^.*mtu //p;') || return + mtu=$( ifconfig $iface | sed -n '1s/^.*mtu //p' ) || return # Optionally create a secondary bridge + # NB: This time, you want to only connect linkX (no uplinkX) if [ "$bridge" != "bridge" ] && ! quietly ngctl info "$iface$bridge:" then - num=2 + num=1 while quietly ngctl msg ${iface}bridge: getstats $num do num=$(( $num + 1 )) done - ngctl mkpeer $iface:lower bridge link$num link1 || + ngctl mkpeer $iface:lower bridge link$num link0 || return ngctl name ${iface}bridge:link$num "$iface$bridge" || return fi # Create a new interface to the bridge - num=2 + num=1 while quietly ngctl msg "$iface$bridge:" getstats $num; do num=$(( $num + 1 )) done - local hook peerhook - case "$NG_TYPE" in - eiface) - # Hook the eiface directly to the bridge - hook=link$num peerhook=ether - ngctl mkpeer "$iface$bridge:" \ - $NG_TYPE $hook $peerhook || return - ;; - iface) - # Hook tcpmss<->iface to bridge - hook=link$num peerhook=bridge - ngctl mkpeer "$iface$bridge:" \ - tcpmss $hook $peerhook || return - hook=iface peerhook=inet - ngctl mkpeer "$iface$bridge:link$num" \ - $NG_TYPE $hook $peerhook || return - ;; - *) return $FAILURE - esac + ngctl mkpeer "$iface$bridge:" eiface link$num ether || return # Rename the new interface while [ ${#jiface} -gt 15 ]; do # OS limitation jiface=${jiface%?} done - case "$NG_TYPE" in - eiface) - new=$( ngctl show -n "$iface$bridge:link$num" ) || - return - new=$( set -- $new; echo $2 ) - ngctl name "$iface$bridge:link$num" $jiface || return - ;; - iface) - ngctl name "$iface$bridge:link$num" $jiface-mss || - return - new=$( ngctl show -n "$jiface-mss:$hook" ) || return - new=$( set -- $new; echo $2 ) - ngctl name $jiface-mss:$hook $jiface || return - ngctl msg $jiface: broadcast || return - ngctl msg $jiface-mss: config "$NG_TCPMSS_CONFIG" || - return - ;; - esac + new=$( ngctl show -n "$iface$bridge:link$num" ) || return + new=$( set -- $new; echo $2 ) + ngctl name "$iface$bridge:link$num" $jiface || return ifconfig $new name $jiface || return ifconfig $jiface mtu $mtu || return ifconfig $jiface up || return @@ -439,13 +497,66 @@ jng_bridge() fi [ "$jiface_devid" ] && quietly ifconfig $jiface ether $jiface_devid + jng_pin_jiface "$jiface" "$name" || : pin optional i=$(( $i + 1 )) done # for iface } +jng_pin_usage="pin [-h] {-a | NAME ...}" +jng_pin_descr="Pin eiface MACs into ng_bridge forwarding database (FDB)" +jng_pin() +{ + local OPTIND=1 OPTARG flag + local show_all= err=$SUCCESS + local name iface jiface + + while getopts ah flag; do + case "$flag" in + a) show_all=1 ;; + *) action_usage pin # NOTREACHED + esac + done + shift $(( $OPTIND - 1 )) + if [ "$show_all" ]; then + [ $# -eq 0 ] || + action_usage pin "too many arguments" # NOTREACHED + for iface in $( ifconfig -l ); do + quietly ngctl info ${iface}bridge: || continue + jng_bridge_persist_hosts ${iface}bridge || + : persist optional + done + set -- $( jls -q name 2> /dev/null ) + [ $# -gt 0 ] || + action_usage pin "no jails" # NOTREACHED + else + [ $# -gt 0 ] || + action_usage pin "too few arguments" # NOTREACHED + fi + + mustberoot_to_continue + + for name in "$@"; do + [ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] || + action_usage pin "invalid name: %s" "$name" + # NOTREACHED + for jiface in $( jexec "$name" ifconfig -l 2> /dev/null ) + do + case "$jiface" in + ng[0-9]*) + jng_pin_jiface "$jiface" "$name" || { + echo "$pgm: pin $jiface: failed" >&2 + err=$FAILURE + } + ;; + esac + done + done + return $err +} + jng_graph_usage="graph [-fh] [-T type] [-o output]" -jng_graph_descr="Generate network graph (default output is \`jng.svg')" +jng_graph_descr="Generate network graph (default output is 'jng.svg')" jng_graph() { local OPTIND=1 OPTARG flag @@ -465,7 +576,7 @@ jng_graph() mustberoot_to_continue if [ -e "$output" -a ! "$force" ]; then - echo "$output: Already exists (use \`-f' to overwrite)" >&2 + echo "$output: Already exists (use '-f' to overwrite)" >&2 return $FAILURE fi if [ ! "$output_type" ]; then @@ -481,7 +592,7 @@ jng_graph() } jng_show_usage="show [-h]" -jng_show_descr="List possible NAME values for \`show NAME'" +jng_show_descr="List possible NAME values for 'show NAME'" jng_show1_usage="show [-h] NAME ..." jng_show1_descr="Lists ng0_NAME [ng1_NAME ...]" jng_show2_usage="show [NAME ...]" @@ -508,11 +619,9 @@ jng_show() fi for name in "$@"; do ngctl ls | awk -v name="$name" ' - BEGIN { N = length(name) + 1 } - !match(ng = $2, /^ng[[:digit:]]+_/) { next } - { _name = substr(ng, S = RSTART + RLENGTH) } - _name != name && substr(_name, 1, N) != name "-" { next } - (type = $4) ~ /^(e?iface|tcpmss)$/, $0 = ng + match($2, /^ng[[:digit:]]+_/) && + substr($2, RSTART + RLENGTH) == name && + $4 == "eiface", $0 = $2 ' | sort done } @@ -588,13 +697,13 @@ jng_stats() if ifconfig -l | xargs -n1 2> /dev/null | fgrep -qw "$name" then [ "$STATS_FMT" != "text" ] || - echo "${name}bridge:link0 [lower]" - ngctl msg ${name}bridge: getstats 0 | + echo "${name}bridge:uplink1 [lower]" + ngctl msg ${name}bridge: getstats -1 | fmt_stats -n "${name}.lower" -t "$now" [ "$STATS_FMT" != "text" ] || - echo "${name}bridge:link1 [upper]" - ngctl msg ${name}bridge: getstats 1 | + echo "${name}bridge:link0 [upper]" + ngctl msg ${name}bridge: getstats 0 | fmt_stats -n "${name}.upper" -t "$now" fi local jiface @@ -672,17 +781,25 @@ action="$1" # # Validate action argument # +case "$action" in +-h) usage ;; # NOTREACHED +-v) VERSION="${VERSION#*: }" + echo "${VERSION% $}" + exit $SUCCESS ;; +-*) usage "unknown option: %s" "$action" ;; # NOTREACHED +*[!a-zA-Z0-9_-]*) usage 'invalid action "%s"' "$action" ;; # NOTREACHED +esac if [ "$BASH_VERSION" ]; then - type="$( type -t "jng_$action" )" || usage # NOTREACHED + type="$( type -t "jng_$action" )" else - type="$( type "jng_$action" 2> /dev/null )" || usage # NOTREACHED -fi + type="$( type "jng_$action" 2> /dev/null )" +fi || usage 'unknown action "%s"' "$action" # NOTREACHED case "$type" in *function) shift 1 # action eval "jng_$action" \"\$@\" ;; -*) usage # NOTREACHED +*) usage 'unknown action "%s"' "$action" # NOTREACHED esac ################################################################################