diff --git a/emhttp/plugins/dynamix/Eth0.page b/emhttp/plugins/dynamix/Eth0.page index 7b4858ea9..680f9fbd7 100644 --- a/emhttp/plugins/dynamix/Eth0.page +++ b/emhttp/plugins/dynamix/Eth0.page @@ -55,15 +55,13 @@ function index($key) { return filter_var($key, FILTER_SANITIZE_NUMBER_INT); } -function metric($eth, $prot, $index) { +function metric($eth) { $system = '/sys/class/net'; - $bridge = str_replace('eth','br', $eth); - $bond = str_replace('eth','bond', $eth); + $bridge = str_replace('eth', 'br', $eth); + $bond = str_replace('eth', 'bond', $eth); $port = file_exists("$system/$bridge") ? $bridge : (file_exists("$system/$bond") ? $bond : $eth); - $metric = exec("ip -$prot route show default dev $port 2>/dev/null | grep -Pom1 ' metric \K\d+'"); - if ($metric) return $metric + $index; - exec("ip -$prot route show default 2>/dev/null | grep -Po ' metric \K\d+'",$metrics); - return (count($metrics) ? max($metrics) : 0) + $index + 1; + $index = exec("cat $system/$port/ifindex 2>/dev/null"); + return 1000 + ($index ?: exec("cat $system/*/ifindex | sort -n | tail -1") + 1); } // remove non-existing ethernet ports @@ -476,9 +474,9 @@ function addVLAN(port) { } function removeVLAN(element) { - var id = $(element).prop('id').split('-'); + var form = $(element).closest('form'); $(element).remove(); - $('#view-'+id[1]).find('select').first().trigger('change'); + form.find('select').first().trigger('change'); } function showVLAN(port) { @@ -729,7 +727,7 @@ _(IPv4 address)_: _(IPv4 default gateway)_: : " class="narrow" pattern="" title="_(IPv4 address A.B.C.D)_"> - " class="slim"> ** + " class="slim"> ** :eth_ipv4_default_gateway_help: @@ -752,7 +750,7 @@ _(IPv6 address)_: _(IPv6 default gateway)_: : " pattern="" title="_(IPv6 address nnnn:xxxx::yyyy)_"> - " class="slim"> ** + " class="slim"> ** :eth_ipv6_default_gateway_help: @@ -830,7 +828,7 @@ _(IPv4 address)_:
_(IPv4 default gateway)_: : " class="narrow" pattern="" title="_(IPv4 address A.B.C.D)_"> - ',4,$i)?>" class="slim"> ** + " class="slim"> ** :eth_ipv4_default_gateway_help: @@ -859,7 +857,7 @@ _(IPv6 address)_:
_(IPv6 default gateway)_: : " pattern="" title="_(IPv6 address nnnn:xxxx::yyyy)_"> - ',6,$i)?>" class="slim"> ** + " class="slim"> ** :eth_ipv6_default_gateway_help: @@ -921,7 +919,7 @@ _(IPv4 address)_:
_(IPv4 default gateway)_: : - ** + **
@@ -942,7 +940,7 @@ _(IPv6 address)_:
_(IPv6 default gateway)_: : - ** + **
diff --git a/emhttp/plugins/dynamix/EthX.page b/emhttp/plugins/dynamix/EthX.page index be1713f3b..27d369ebe 100644 --- a/emhttp/plugins/dynamix/EthX.page +++ b/emhttp/plugins/dynamix/EthX.page @@ -186,7 +186,7 @@ _(IPv4 address)_:
_(IPv4 default gateway)_: : " class="narrow" pattern="" title="_(IPv4 address A.B.C.D)_"> - " class="slim"> ** + " class="slim"> ** :eth_ipv4_default_gateway_help: @@ -213,7 +213,7 @@ _(IPv6 address)_:
_(IPv6 default gateway)_: : " pattern="" title="_(IPv6 address nnnn:xxxx::yyyy)_"> - " class="slim"> ** + " class="slim"> ** :eth_ipv6_default_gateway_help: @@ -293,7 +293,7 @@ _(IPv4 address)_:
_(IPv4 default gateway)_: : " class="narrow" pattern="" title="_(IPv4 address A.B.C.D)_"> - ',4,$i)?>" class="slim"> ** + " class="slim"> ** :eth_ipv4_default_gateway_help: @@ -320,7 +320,7 @@ _(IPv6 address)_:
_(IPv6 default gateway)_: : " pattern="" title="_(IPv6 address nnnn:xxxx::yyyy)_"> - ',6,$i)?>" class="slim"> ** + " class="slim"> ** :eth_ipv6_default_gateway_help: @@ -383,7 +383,7 @@ _(IPv4 address)_:
_(IPv4 default gateway)_: : - ** + **
@@ -404,7 +404,7 @@ _(IPv6 address)_:
_(IPv6 default gateway)_: : - ** + **
diff --git a/emhttp/plugins/dynamix/include/RoutingTable.php b/emhttp/plugins/dynamix/include/RoutingTable.php index 2bfa73efb..1741cbc99 100644 --- a/emhttp/plugins/dynamix/include/RoutingTable.php +++ b/emhttp/plugins/dynamix/include/RoutingTable.php @@ -26,8 +26,8 @@ case 'Add Route': if ($gateway && $route) exec("/etc/rc.d/rc.inet1 ".escapeshellarg("{$gateway}_{$route}_{$metric}_add")); break; default: - exec("ip -4 route show table all|grep -Pv '^(127\\.0\\.0\\.0)|table local|unreachable'",$ipv4); - exec("ip -6 route show table all|grep -Pv '^([am:]|(f[ef][0-9][0-9])::)|table local|unreachable'",$ipv6); + exec("ip -4 route show table all | grep -Pv '^(127\\.0\\.0\\.0)|table local|unreachable|linkdown|broadcast'",$ipv4); + exec("ip -6 route show table all | grep -Pv '^([am:]|(f[ef][0-9][0-9])::)|table local|unreachable|linkdown'",$ipv6); foreach ($ipv4 as $info) { $cell = explode(' ',$info); $route = $cell[0]; diff --git a/emhttp/plugins/dynamix/scripts/reload_services b/emhttp/plugins/dynamix/scripts/reload_services index a1fe713cb..babe9c7c1 100755 --- a/emhttp/plugins/dynamix/scripts/reload_services +++ b/emhttp/plugins/dynamix/scripts/reload_services @@ -8,9 +8,6 @@ queue(){ atq | grep -Pom1 '^\d+' } -# delayed execution -sleep ${1:-1} - JOB=$(queue) if [[ -n $JOB ]]; then atrm $JOB 2>/dev/null diff --git a/emhttp/plugins/dynamix/scripts/update_services b/emhttp/plugins/dynamix/scripts/update_services index a89950733..4fbebbb2a 100755 --- a/emhttp/plugins/dynamix/scripts/update_services +++ b/emhttp/plugins/dynamix/scripts/update_services @@ -15,6 +15,6 @@ else log "no queued job present" fi -echo "/usr/local/emhttp/webGui/scripts/reload_services ${1:-1}" | at -M now 2>/dev/null +echo "sleep ${1:-1}; /usr/local/emhttp/webGui/scripts/reload_services" | at -M now 2>/dev/null log "queue new job $(queue), wait for ${1:-1}s" exit 0 diff --git a/etc/rc.d/rc.docker b/etc/rc.d/rc.docker index 6a9719d35..8d9b5221f 100755 --- a/etc/rc.d/rc.docker +++ b/etc/rc.d/rc.docker @@ -10,7 +10,7 @@ # VMs, bare metal, OpenStack clusters, public clouds and more. # # LimeTech - modified for Unraid OS -# Bergware - modified for Unraid OS, January 2025 +# Bergware - modified for Unraid OS, May 2025 DAEMON="Docker daemon" UNSHARE="/usr/bin/unshare" @@ -35,6 +35,22 @@ TMP=/var/tmp/network.tmp # run & log functions . /etc/rc.d/rc.runlog +# return interface index +index(){ + cat $SYSTEM/$1/ifindex 2>/dev/null +} + +# return active interface +active(){ + if [[ -e $SYSTEM/${1/eth/br} ]]; then + echo ${1/eth/br} + elif [[ -e $SYSTEM/${1/eth/bond} ]]; then + echo ${1/eth/bond} + else + echo $1 + fi +} + # wait for interface to go up carrier(){ local n e @@ -49,20 +65,13 @@ carrier(){ # initialize docker settings docker_read_options(){ # determine active port name - [[ -e $SYSTEM/bond0 ]] && PORT=bond0 || PORT=eth0 - [[ -e $SYSTEM/br0 ]] && PORT=br0 + PORT=$(active eth0) [[ ! $(carrier $PORT) && $(carrier wlan0 1) ]] && PORT=wlan0 # Set defaults used by the docker daemon if [[ -f $DOCKER_CFG ]]; then for NIC in $NICS; do - if [[ ${NIC:0:3} == eth ]]; then - if [[ -e $SYSTEM/${NIC/eth/br} ]]; then - NIC=${NIC/eth/br} - elif [[ -e $SYSTEM/${NIC/eth/bond} ]]; then - NIC=${NIC/eth/bond} - fi - fi + [[ ${NIC:0:3} == eth ]] && NIC=$(active $NIC) CFG=($(grep -Pom2 "_SUBNET_|_${NIC^^}(_[0-9]+)?=" $DOCKER_CFG)) if [[ ${CFG[0]} == _SUBNET_ && -z ${CFG[1]} ]]; then # interface has changed, update configuration @@ -103,7 +112,7 @@ docker_read_options(){ [[ -n $MTU && $MTU -ne 1500 ]] && DOCKER_OPTS="--mtu=$MTU $DOCKER_OPTS" # Enable IPv6 for docker bridge network - if [[ -n $(ip -6 route show default dev $PORT) ]]; then + if [[ -n $(ip -6 route show to default dev $PORT) ]]; then DOCKER0='fd17::/64' DOCKER_OPTS="--ipv6 --fixed-cidr-v6=$DOCKER0 $DOCKER_OPTS" IPV6_FORWARD=${IPV6_FORWARD:=accept} @@ -165,6 +174,8 @@ driver(){ ATTACH='macvlan' MODE='bridge' fi + # wlan0 has forced ipvlan + [[ $1 == wlan && $2 == forced ]] && ATTACH=ipvlan } # Custom networks @@ -172,6 +183,11 @@ network(){ docker network ls --filter driver="$1" --format='{{.Name}}' 2>/dev/null | grep -P "^[a-z]+$2(\$|\.)" | tr '\n' ' ' } +# Does the ipv4 address exist? +ipv4_exist(){ + ip -4 -br addr show to $2 dev $1 | awk '{print $3;exit}' +} + # Is container running? container_running(){ local CONTAINER @@ -213,9 +229,9 @@ container_add_route(){ local NET=${CT[1]#*[} local LAN=${NET%:*} if [[ $PID -gt 0 && "eth0 br0 bond0 wlan0" =~ $LAN ]]; then - local THISIP=$(ip -4 -br addr show dev $LAN scope global | awk '{print $3;exit}') + local THISIP=$(ip -4 -br addr show scope global primary dev $LAN | awk '{print $3;exit}') for CFG in /etc/wireguard/wg*.cfg ; do - local NETWORK=$(ip -4 show route dev $LAN $THISIP | awk '{print $1;exit}') + local NETWORK=$(ip -4 route show to $THISIP dev $LAN | awk '{print $1;exit}') [[ -n $NETWORK ]] && nsenter -n -t $PID ip -4 route add $NETWORK via ${THISIP%/*} dev $LAN 2>/dev/null done fi @@ -246,8 +262,7 @@ docker_network_start(){ if ! docker_running; then return 1; fi # get container settings for custom networks to reconnect later declare -A NETRESTORE CTRESTORE - CONTAINERS=$(docker container ls -a --format='{{.Names}}' | tr '\n' ' ') - for CONTAINER in $CONTAINERS; do + for CONTAINER in $(docker container ls -a --format='{{.Names}}'); do # the file case (due to fat32) might be different so use find to match XMLFILE=$(find /boot/config/plugins/dockerMan/templates-user -maxdepth 1 -iname my-${CONTAINER}.xml) if [[ -n $XMLFILE ]]; then @@ -255,13 +270,7 @@ docker_network_start(){ MAIN= # update custom network reference (if changed) for NIC in $NICS; do - if [[ ${NIC:0:3} == eth ]]; then - if [[ -e $SYSTEM/${NIC/eth/br} ]]; then - NIC=${NIC/eth/br} - elif [[ -e $SYSTEM/${NIC/eth/bond} ]]; then - NIC=${NIC/eth/bond} - fi - fi + [[ ${NIC:0:3} == eth ]] && NIC=$(active $NIC) X=${NIC//[^0-9]/} REF=$(grep -Pom1 "\K(br|bond|eth|wlan)$X" $XMLFILE) [[ $X == 0 && ! $(carrier $NIC 1) ]] && continue @@ -307,24 +316,18 @@ docker_network_start(){ done # detach custom networks for NIC in $NICS; do - if [[ ${NIC:0:3} == eth ]]; then - if [[ -e $SYSTEM/${NIC/eth/br} ]]; then - NIC=${NIC/eth/br} - elif [[ -e $SYSTEM/${NIC/eth/bond} ]]; then - NIC=${NIC/eth/bond} - fi - fi + [[ ${NIC:0:3} == eth ]] && NIC=$(active $NIC) X=${NIC//[^0-9]/} driver ${NIC//[0-9]/} for NETWORK in $(network $DETACH $X); do - [[ $STOCK =~ ${NETWORK%%[0-9]*} || $DOCKER_USER_NETWORKS != preserve ]] && docker network rm $NETWORK >/dev/null + [[ $STOCK =~ ${NETWORK%%[0-9]*} || $DOCKER_USER_NETWORKS != preserve ]] && docker network rm $NETWORK &>/dev/null done # get existing custom networks for NETWORK in $(network $ATTACH $X); do if [[ $STOCK =~ ${NETWORK%%[0-9]*} ]]; then - [[ $EXCLUDE =~ "$NETWORK " || ! $ACTIVE =~ "$NETWORK " ]] && docker network rm $NETWORK >/dev/null + [[ $EXCLUDE =~ "$NETWORK " || ! $ACTIVE =~ "$NETWORK " ]] && docker network rm $NETWORK &>/dev/null else - [[ $DOCKER_USER_NETWORKS != preserve ]] && docker network rm $NETWORK >/dev/null + [[ $DOCKER_USER_NETWORKS != preserve ]] && docker network rm $NETWORK &>/dev/null fi done NETWORKS=$(network $ATTACH $X) @@ -336,27 +339,27 @@ docker_network_start(){ AUTO=${NETWORK/./_} AUTO=DOCKER_AUTO_${AUTO^^} if [[ ${!AUTO} == no ]]; then - [[ $NETWORKS =~ "$NETWORK " ]] && docker network rm $NETWORK >/dev/null + [[ $NETWORKS =~ "$NETWORK " ]] && docker network rm $NETWORK &>/dev/null continue fi # add auto defined networks SUBNET=; GATEWAY=; SERVER=; RANGE=; - [[ -z ${!AUTO} || ${!AUTO} =~ "4" ]] && IPV4=$(ip -4 -br addr show $NETWORK scope global | awk '{print $3;exit}') || IPV4= + [[ -z ${!AUTO} || ${!AUTO} =~ "4" ]] && IPV4=$(ip -4 -br addr show scope global primary dev $NETWORK | awk '{print $3;exit}') || IPV4= if [[ -n $IPV4 ]]; then - SUBNET=$(ip -4 route show $IPV4 dev $NETWORK | awk '{print $1;exit}') + SUBNET=$(ip -4 route show to $IPV4 dev $NETWORK | awk '{print $1;exit}') SERVER=${IPV4%/*} DHCP=${NETWORK/./_} DHCP=DOCKER_DHCP_${DHCP^^} RANGE=${!DHCP} - GATEWAY=$(ip -4 route show default dev $NETWORK | awk '{print $3;exit}') + GATEWAY=$(ip -4 route show to default dev $NETWORK | awk '{print $3;exit}') fi SUBNET6=; GATEWAY6=; SERVER6=; - [[ -z ${!AUTO} || ${!AUTO} =~ "6" ]] && IPV6=$(ip -6 -br addr show $NETWORK scope global -temporary -deprecated | awk '{print $3;exit}') || IPV6= + # get IPv6 address - ignore any /128 networks + [[ -z ${!AUTO} || ${!AUTO} =~ "6" ]] && IPV6=$(ip -6 -br addr show scope global primary -deprecated dev $NETWORK | awk -v RS='[[:space:]]+' '(NR>2){print}' | grep -Pvm1 '^.+/128|^$') || IPV6= if [[ -n $IPV6 ]]; then - # get IPV6 subnet, preset to /64 if single host address is given - [[ ${IPV6#*/} == 128 ]] && SUBNET6=$(echo $IPV6 | sed -r 's/^([^:]+):([^:]+):([^:]+):([^:]+).*$/\1:\2:\3:\4::\/64/') || SUBNET6=$(ip -6 route show $IPV6 dev $NETWORK | awk '{print $1;exit}') + SUBNET6=$(ip -6 route show to $IPV6 dev $NETWORK | awk '{print $1;exit}') SERVER6=${IPV6%/*} - GATEWAY6=$(ip -6 route show default dev $NETWORK | awk '{print $3;exit}') + GATEWAY6=$(ip -6 route show to default dev $NETWORK | awk '{print $3;exit}') # replace link local address for first address in subnet [[ ${GATEWAY6:0:4} == fe80 ]] && GATEWAY6=${SUBNET6%/*}1 fi @@ -378,38 +381,7 @@ docker_network_start(){ GATEWAY6=${!GATEWAY6} SERVER6=; fi - # custom network already existing and changed? - if [[ $NETWORKS =~ "$NETWORK " ]]; then - UPDATE=; - SUBNETS=($(docker network inspect --format='{{range .IPAM.Config}}{{.Subnet}} {{end}}' $NETWORK 2>/dev/null)) - RANGES=($(docker network inspect --format='{{range .IPAM.Config}}{{.IPRange}} {{end}}' $NETWORK 2>/dev/null)) - GATEWAYS=($(docker network inspect --format='{{range .IPAM.Config}}{{.Gateway}} {{end}}' $NETWORK 2>/dev/null)) - SERVERS=($(docker network inspect --format='{{range .IPAM.Config}}{{range $IPAddr := .AuxiliaryAddresses}}{{$IPAddr}}{{end}} {{end}}' $NETWORK 2>/dev/null)) - # distribute ipv4 and ipv6 assignments - [[ ${SUBNETS[0]} =~ '.' ]] && SUBNET0=${SUBNETS[0]} || SUBNET1=${SUBNETS[0]} - [[ -n ${SUBNETS[1]} && ${SUBNETS[1]} =~ '.' ]] && SUBNET0=${SUBNETS[1]} || SUBNET1=${SUBNETS[1]} - [[ ${RANGES[0]} =~ '.' ]] && RANGE0=${RANGES[0]} || RANGE1=${RANGES[0]} - [[ -n ${RANGES[1]} && ${RANGES[1]} =~ '.' ]] && RANGE0=${RANGES[1]} || RANGE1=${RANGES[1]} - [[ ${GATEWAYS[0]} =~ '.' ]] && GATEWAY0=${GATEWAYS[0]} || GATEWAY1=${GATEWAYS[0]} - [[ -n ${GATEWAYS[1]} && ${GATEWAYS[1]} =~ '.' ]] && GATEWAY0=${GATEWAYS[1]} || GATEWAY1=${GATEWAYS[1]} - [[ ${SERVERS[0]} =~ '.' ]] && SERVER0=${SERVERS[0]} || SERVER1=${SERVERS[0]} - [[ -n ${SERVERS[1]} && ${SERVERS[1]} =~ '.' ]] && SERVER0=${SERVERS[1]} || SERVER1=${SERVERS[1]} - # check for changes - [[ $SUBNET != $SUBNET0 || $SUBNET6 != $SUBNET1 ]] && UPDATE=1 - [[ $RANGE != $RANGE0 ]] && UPDATE=1 - [[ (-n $GATEWAY && $GATEWAY != $GATEWAY0) || (-n $GATEWAY6 && $GATEWAY6 != $GATEWAY1) ]] && UPDATE=1 - [[ (-n $SERVER && $SERVER != $SERVER0) || (-n $SERVER6 && $SERVER6 != $SERVER1) ]] && UPDATE=1 - if [[ -z $UPDATE ]]; then - # no changes, ignore - SUBNET=; SUBNET6=; - else - # changed, remove first - docker network rm $NETWORK >/dev/null - fi - fi # set parameters for custom network creation - N4=$SUBNET; R4=$RANGE; - N6=$SUBNET6; [[ -n $SUBNET && -n $GATEWAY ]] && GATEWAY="--gateway=$GATEWAY" || GATEWAY=; [[ -n $SUBNET && -n $SERVER ]] && SERVER="--aux-address=server=$SERVER" || SERVER=; [[ -n $SUBNET && -n $RANGE ]] && RANGE="--ip-range=$RANGE" || RANGE=; @@ -419,15 +391,15 @@ docker_network_start(){ [[ -n $SUBNET6 ]] && SUBNET6="--ipv6 --subnet=$SUBNET6" if [[ -n $SUBNET || -n $SUBNET6 ]]; then TYPE=${NETWORK//[0-9.]/} - driver $TYPE - if [[ $TYPE == br ]]; then + driver $TYPE forced + if [[ $TYPE == br || $TYPE == wlan ]]; then VHOST=$NETWORK - elif [[ $TYPE == wlan ]]; then - VHOST=$NETWORK - ATTACH=ipvlan else - [[ $DOCKER_ALLOW_ACCESS == yes && -n $IPV4 ]] && VHOST=vhost${NETWORK//[^0-9.]/} || VHOST=$NETWORK + [[ -n $IPV4 && $DOCKER_ALLOW_ACCESS == yes ]] && VHOST=vhost${NETWORK//[^0-9.]/} || VHOST=$NETWORK fi + # delete and recreate unconditionally + log "Processing... $NETWORK" + docker network rm $NETWORK &>/dev/null docker network create -d $ATTACH $SUBNET $GATEWAY $SERVER $RANGE $SUBNET6 $GATEWAY6 $SERVER6 -o parent=$VHOST $NETWORK | xargs docker network inspect -f "created network $ATTACH {{.Name}} with subnets: {{range .IPAM.Config}}{{.Subnet}}; {{end}}" 2>/dev/null | log # connect containers to this new network for CONNECT in ${NETRESTORE[$NETWORK]}; do @@ -448,57 +420,50 @@ docker_network_start(){ # hack to let containers talk to host if [[ $TYPE == br ]]; then LINK=shim-$NETWORK - GW=($(ip -4 route show default dev $NETWORK | awk '{print $3,$5;exit}')) if [[ $DOCKER_ALLOW_ACCESS == yes && -n $IPV4 ]]; then - # create shim interface and copy parent IPv4 address to shim interface + IPV4="$IPV4 metric $((1000 - 1 + $(index $NETWORK)))" + # create shim interface [[ -e $SYSTEM/$LINK ]] || run ip link add link $NETWORK name $LINK type $ATTACH mode $MODE - run ip addr flush dev $LINK scope global - run ip -4 addr add $IPV4 dev $LINK metric 0 # disable IPv6 on shim interface echo 1 >$CONF6/$LINK/disable_ipv6 + run ip -6 addr flush dev $LINK + # copy parent IPv4 address to shim interface + run ip addr add $IPV4 dev $LINK run ip link set $LINK up - if [[ -n $GW ]]; then - if [[ -z ${GW[1]} ]]; then - METRIC=1 - METRICS=$(ip -4 route show default | grep -Po 'metric \K\d+') - while [[ " $METRICS " =~ " $METRIC " ]]; do ((METRIC++)); done - # update existing route to avoid conflict with shim route - run ip -4 route del default via $GW dev $NETWORK - run ip -4 route add default via $GW dev $NETWORK metric $METRIC - fi - run ip -4 route add default via $GW dev $LINK metric 0 - fi log "created network $LINK for host access" elif [[ -e $SYSTEM/$LINK ]]; then # remove shim interface - [[ -n $GW ]] && ip -4 route del default via $GW dev $LINK run ip addr flush dev $LINK run ip link set $LINK down run ip link del $LINK fi - elif [[ $TYPE != wlan ]]; then - if [[ $DOCKER_ALLOW_ACCESS == yes && -n $IPV4 ]]; then - run ip addr flush dev $VHOST scope global - # copy IPv4 address to vhost interface - run ip -4 addr add $IPV4 dev $VHOST metric 0 - log "prepared network $VHOST for host access" + else + if [[ $TYPE == wlan ]]; then + VHOST=shim-$NETWORK + INDEX=3000 else VHOST=vhost${NETWORK//[^0-9.]/} - if [[ -e $SYSTEM/$VHOST ]]; then - # remove IP addresses of vhost - run ip addr flush dev $VHOST scope global - # remove routing of vhost - run ip -4 route flush dev $VHOST - run ip -6 route flush dev $VHOST - fi + INDEX=1000 + fi + INDEX=$(($INDEX - 1 + $(index $NETWORK))) + if [[ -n $IPV4 && $DOCKER_ALLOW_ACCESS == yes ]]; then + # disable IPv6 on vhost interface + echo 1 >$CONF6/$VHOST/disable_ipv6 + run ip -6 addr flush dev $VHOST + # copy parent IPv4 address to vhost interface + [[ -z $(ipv4_exist $VHOST ${IPV4%/*}) ]] && run ip addr add $IPV4 metric $INDEX dev $VHOST + log "created network $VHOST for host access" + elif [[ -n $IPV4 && -e $SYSTEM/$VHOST && -n $(ipv4_exist $VHOST ${IPV4%/*}) ]]; then + # remove parent IPv4 address from vhost interface + run ip addr del $IPV4 metric $INDEX dev $VHOST fi fi fi done # create IPv6 forward accept rule if [[ $IPV6_FORWARD == accept ]]; then - log "creating forward accept rule for IPv6 network" ip6tables -P FORWARD ACCEPT + log "created forward accept rule for IPv6 network" fi log "Network started." } @@ -506,33 +471,31 @@ docker_network_start(){ docker_network_stop(){ log "Stopping network..." if ! docker_running; then return 1; fi + # Read docker configuration file + [[ -f $DOCKER_CFG ]] && . $DOCKER_CFG for NIC in $NICS; do - if [[ ${NIC:0:3} == eth ]]; then - if [[ -e $SYSTEM/${NIC/eth/br} ]]; then - NIC=${NIC/eth/br} - elif [[ -e $SYSTEM/${NIC/eth/bond} ]]; then - NIC=${NIC/eth/bond} - fi - fi - driver ${NIC//[0-9]/} + [[ ${NIC:0:3} == eth ]] && NIC=$(active $NIC) + driver ${NIC//[0-9]/} forced for NETWORK in $(network $ATTACH ${NIC//[^0-9]/}); do - [[ $STOCK =~ ${NETWORK%%[0-9]*} || $DOCKER_USER_NETWORKS != preserve ]] && docker network rm $NETWORK >/dev/null + [[ $STOCK =~ ${NETWORK%%[0-9]*} || $DOCKER_USER_NETWORKS != preserve ]] && docker network rm $NETWORK &>/dev/null TYPE=${NETWORK//[0-9.]/} if [[ $TYPE == br ]]; then LINK=shim-$NETWORK if [[ -e $SYSTEM/$LINK ]]; then - GW=$(ip -4 route show default dev $LINK | awk '{print $3;exit}') - [[ -n $GW ]] && run ip -4 route del default via $GW dev $LINK run ip addr flush dev $LINK run ip link set $LINK down run ip link del $LINK fi - else + elif [[ $TYPE != wlan ]]; then VHOST=vhost${NETWORK//[^0-9.]/} [[ -e $SYSTEM/$VHOST ]] && run ip addr flush dev $VHOST fi done done + if [[ -e $SYSTEM/shim-wlan0 ]]; then + IPV4=$(ip -4 -br addr show scope global primary dev shim-wlan0 | awk '{print $3,$4,$5;exit}') + [[ -n $IPV4 ]] && run ip addr del $IPV4 dev shim-wlan0 + fi log "Network stopped." } @@ -549,7 +512,7 @@ docker_container_start(){ if [[ $OUT =~ "Error:" ]]; then log "$CONTAINER: $OUT" & else - run container_add_route $CONTAINER + container_add_route $CONTAINER log "$CONTAINER: started successfully!" & if [[ $WAIT -gt 0 ]]; then log "$CONTAINER: wait $WAIT seconds" & diff --git a/etc/rc.d/rc.inet1 b/etc/rc.d/rc.inet1 index 6670cbbfc..835b8dd3e 100755 --- a/etc/rc.d/rc.inet1 +++ b/etc/rc.d/rc.inet1 @@ -7,7 +7,7 @@ # @(#)/etc/rc.d/rc.inet1 10.2 Sun Jul 24 12:45:56 PDT 2005 (pjv) # LimeTech - modified for Unraid OS -# Bergware - modified for Unraid OS, February 2025 +# Bergware - modified for Unraid OS, May 2025 # Adapted by Bergware for use in Unraid OS - April 2016 # - improved interface configuration @@ -81,6 +81,9 @@ # - added metric value to interface IP assignment # - fixed DNS entries get removed when configuring interface other then eth0 +# Adapted by Bergware for use in Unraid OS - May 2025 +# - improved metric value to interface IP assignment + ########### # LOGGING # ########### @@ -122,22 +125,18 @@ done # LOOPBACK FUNCTIONS # ###################### -# function to bring up loopback interface +# bring up loopback interface lo_up(){ if [[ -e $SYSTEM/lo ]]; then - if ! ip -4 addr show lo | grep -qw 'inet'; then - run ip -4 addr add 127.0.0.1/8 dev lo - fi - if ! ip -6 addr show lo | grep -qw 'inet6'; then - run ip -6 addr add ::1/128 dev lo - fi + [[ -z $(ip -4 -br addr show lo | awk '{print $3;exit}') ]] && run ip -4 addr add 127.0.0.1/8 dev lo + [[ -z $(ip -6 -br addr show lo | awk '{print $3;exit}') ]] && run ip -6 addr add ::1/128 dev lo run ip link set lo up else [[ $DEBUG_ETH_UP == yes ]] && log "interface lo not present, can't bring up" fi } -# function to take down loopback interface +# take down loopback interface lo_down(){ if [[ -e $SYSTEM/lo ]]; then run ip link set lo down @@ -150,12 +149,17 @@ lo_down(){ # INTERFACE FUNCTIONS # ####################### -# function to get link mtu size +# return interface index +index(){ + cat $SYSTEM/$1/ifindex 2>/dev/null +} + +# get link mtu size get_mtu(){ ip link show $1 | grep -Po 'mtu \K\d+' } -# function to set/reset link mtu size +# set/reset link mtu size set_mtu(){ if [[ -n ${MTU[$i]} ]]; then # set MTU to specified value @@ -166,8 +170,8 @@ set_mtu(){ fi } -# function to wait for carrier of interface -carrier_up(){ +# wait for carrier of interface +carrier(){ local n for n in {1..10}; do [[ $(cat $SYSTEM/$1/carrier 2>/dev/null) == 1 ]] && return 0 || sleep 1 @@ -175,7 +179,7 @@ carrier_up(){ return 1 } -# function to create bond interface +# create bond interface bond_up(){ [[ -d /proc/net/bonding ]] || modprobe bonding mode=${BONDING_MODE[$i]} miimon=${BONDING_MIIMON[$i]} run ip link add name ${BONDNAME[$i]} type bond mode ${BONDING_MODE[$i]} miimon ${BONDING_MIIMON[$i]} @@ -194,7 +198,7 @@ bond_up(){ [[ -n $PRIMARY ]] && run ip link set name ${BONDNAME[$i]} type bond primary $PRIMARY } -# function to delete bond interface +# delete bond interface bond_down(){ if [[ -e $SYSTEM/${BONDNAME[$i]} ]]; then # loop thru attached interfaces in bond @@ -208,7 +212,7 @@ bond_down(){ fi } -# function to create bridge interface +# create bridge interface br_up(){ for ((j=0;j<${VLANS[$i]:-1};j++)); do [[ $j -eq 0 ]] && BRIDGE=${BRNAME[$i]} || BRIDGE=${BRNAME[$i]}.${VLANID[$i,$j]} @@ -235,7 +239,7 @@ br_up(){ done } -# function to delete bridge interface +# delete bridge interface br_down(){ for ((j=0;j<${VLANS[$i]:-1};j++)); do # loop thru main bridge and bridge VLAN interfaces @@ -253,7 +257,7 @@ br_down(){ done } -# function to create VLAN interfaces +# create VLAN interfaces vlan_up(){ for PORT in ${BRNICS[$i]:-${IFNAME[$i]}}; do for ((j=1;j<${VLANS[$i]};j++)); do @@ -265,7 +269,7 @@ vlan_up(){ done } -# function to delete VLAN interfaces +# delete VLAN interfaces vlan_down(){ for PORT in ${BRNICS[$i]:-${IFNAME[$i]}}; do for VLAN in $(ls --indicator-style=none $SYSTEM | grep -Po "$PORT\.\d+"); do @@ -275,7 +279,7 @@ vlan_down(){ done } -# function to create macvtap interfaces +# create macvtap interfaces macvtap_up(){ PARENT=${IFNAME[$i]} [[ -n ${BONDNICS[$i]} ]] && PARENT=${BONDNAME[$i]} @@ -283,16 +287,18 @@ macvtap_up(){ MAC=$(echo $(hostname)-$VTAP | md5sum | sed -r 's/^(..)(..)(..)(..)(..).*$/02:\1:\2:\3:\4:\5/') run ip link add link $PARENT name $VTAP address $MAC type macvtap mode bridge set_mtu $VTAP + echo 1 >$CONF6/$VTAP/disable_ipv6 run ip link set $VTAP up for ((j=1;j<${VLANS[$i]:-0};j++)); do VLAN=${VLANID[$i,$j]} run ip link add link $PARENT.$VLAN name $VTAP.$VLAN address $MAC type macvtap mode bridge set_mtu $VTAP.$VLAN + echo 1 >$CONF6/$VTAP.$VLAN/disable_ipv6 run ip link set $VTAP.$VLAN up done } -# function to delete macvtap interfaces +# delete macvtap interfaces macvtap_down(){ PARENT=${IFNAME[$i]} [[ -n ${BONDNICS[$i]} ]] && PARENT=${BONDNAME[$i]} @@ -308,26 +314,26 @@ macvtap_down(){ run ip link del $VTAP } -# function to enable/disable ipv6 protocol per interface +# enable/disable ipv6 protocol per interface ipv6_up(){ [[ -d $CONF6/${IFACE/$1/$2} ]] && echo $4 >$CONF6/${IFACE/$1/$2}/disable_ipv6 [[ -d $CONF6/${IFACE/$1/$3} ]] && echo $4 >$CONF6/${IFACE/$1/$3}/disable_ipv6 } -# function to enable/disable ipv6 assignment per interface +# enable/disable ipv6 assignment per interface ipv6_ra(){ echo $2 >$CONF6/$1/accept_ra echo $2 >$CONF6/$1/accept_ra_defrtr echo $3 >$CONF6/$1/autoconf } -# function to enable/disable ipv6 assignment per interface +# enable/disable ipv6 assignment per interface ipv6_conf(){ [[ -d $CONF6/${IFACE/$1/$2} ]] && ipv6_ra ${IFACE/$1/$2} $4 $5 [[ -d $CONF6/${IFACE/$1/$3} ]] && ipv6_ra ${IFACE/$1/$3} $4 $5 } -# function to enable/disable ipv6 assignment per interface +# enable/disable ipv6 assignment per interface ipv6_addr(){ [[ -d $CONF6/$IFACE ]] && ipv6_ra $IFACE $1 $2 [[ -d $CONF6/$VHOST ]] && ipv6_ra $VHOST $1 $2 @@ -342,7 +348,7 @@ ipv6_addr(){ sleep 1 } -# function to assign IP address +# assign IP address ipaddr_up(){ if [[ -z $RENEW ]]; then # disable IPv6 per interface when IPv4 only @@ -371,7 +377,7 @@ ipaddr_up(){ [[ $IP == ipv4 ]] && DHCP_OPTIONS="$DHCP_OPTIONS -4" [[ $IP == ipv6 ]] && DHCP_OPTIONS="$DHCP_OPTIONS -6" [[ $IP != ipv4 && -n $PRIV6 && -d $CONF6/$IFACE ]] && echo $PRIV6 >$CONF6/$IFACE/use_tempaddr - if carrier_up $IFACE; then + if carrier $IFACE; then # interface is UP log "interface $IFACE is UP, polling up to 60 sec for DHCP $IP server" if ! run timeout 60 dhcpcd -w $DHCP_OPTIONS $IFACE; then @@ -385,21 +391,23 @@ ipaddr_up(){ fi elif [[ $DHCP == no ]]; then # bring up interface using static IP address - if carrier_up $IFACE; then STATE="UP"; else STATE="DOWN"; fi + if carrier $IFACE; then STATE="UP"; else STATE="DOWN"; fi log "interface $IFACE is $STATE, setting static $IP address" ipv6_addr 0 1 + INDEX=$(index $IFACE) + INDEX=$((1000 + ${INDEX:-$(($(index * | sort -n | tail -1) + 1))})) if [[ $IP != ipv6 ]]; then [[ $j -eq 0 ]] && ADDR=${IPADDR[$i]} || ADDR=${IPADDR[$i,$j]} if [[ -n $ADDR ]]; then [[ $j -eq 0 ]] && MASK=${NETMASK[$i]} || MASK=${NETMASK[$i,$j]} - [[ -n $MASK ]] && run ip -4 addr add $(unzero $ADDR)/$MASK dev $IFACE metric ${DHCP_METRIC:-1} + [[ -n $MASK ]] && run ip -4 addr add $(unzero $ADDR)/$MASK metric $INDEX dev $IFACE fi fi if [[ $IP != ipv4 ]]; then [[ $j -eq 0 ]] && ADDR6=${IPADDR6[$i]} || ADDR6=${IPADDR6[$i,$j]} if [[ -n $ADDR6 ]]; then [[ $j -eq 0 ]] && MASK6=${NETMASK6[$i]} || MASK6=${NETMASK6[$i,$j]} - [[ -n $MASK6 ]] && run ip -6 addr add $(unzero6 $ADDR6)/$MASK6 dev $IFACE metric ${DHCP_METRIC:-1} + [[ -n $MASK6 ]] && run ip -6 addr add $(unzero6 $ADDR6)/$MASK6 metric $INDEX dev $IFACE [[ -n $PRIV6 && -d $CONF6/$IFACE ]] && echo 0 >$CONF6/$IFACE/use_tempaddr fi fi @@ -410,7 +418,7 @@ ipaddr_up(){ fi } -# function to release IP addresses and routes +# release IP addresses and routes ipaddr_conf(){ if [[ -e $SYSTEM/${IFACE/$1/$2} ]]; then run ip -$4 addr flush dev ${IFACE/$1/$2} @@ -422,7 +430,7 @@ ipaddr_conf(){ fi } -# function to release IP addresses and routes +# release IP addresses and routes ipaddr_flush(){ run ip -$1 addr flush dev $IFACE run ip -$1 route flush dev $IFACE @@ -436,7 +444,7 @@ ipaddr_flush(){ fi } -# function to release IP addresses and routes +# release IP addresses and routes ipaddr_down(){ if [[ $DHCP == yes ]]; then DHCP_OPTIONS="-q -k" @@ -453,7 +461,7 @@ ipaddr_down(){ fi } -# function to bring up network interface +# bring up network interface if_up(){ # set index of INTERFACE in array i=0 @@ -555,7 +563,7 @@ if_up(){ done } -# function to take down network interface +# take down network interface if_down(){ # set index of INTERFACE in array i=0 @@ -612,7 +620,7 @@ if_down(){ # GATEWAY FUNCTIONS # ##################### -# function to add default gateway per interface +# add default gateway per interface gateway_up(){ for GW in ${GATEWAY[@]}; do [[ -z $GW ]] && continue @@ -623,7 +631,7 @@ gateway_up(){ IP=${PROTOCOL[$x]:-ipv4} AD=${METRIC[$x]} [[ -n $AD ]] && AD="metric $AD" - EXIST=$(ip -4 route show default via $(unzero $GW) dev $DEV | grep "$AD ") + EXIST=$(ip -4 route show to default via $(unzero $GW) dev $DEV | grep "$AD ") [[ $IP != ipv6 && -z $EXIST ]] && run ip -4 route add default via $(unzero $GW) dev $DEV $AD done for GW6 in ${GATEWAY6[@]}; do @@ -635,12 +643,12 @@ gateway_up(){ IP=${PROTOCOL[$x]:-ipv4} AD6=${METRIC6[$x]} [[ -n $AD6 ]] && AD6="metric $AD6" - EXIST=$(ip -6 route show default via $(unzero6 $GW6) dev $DEV | grep "$AD6 ") + EXIST=$(ip -6 route show to default via $(unzero6 $GW6) dev $DEV | grep "$AD6 ") [[ $IP != ipv4 && -z $EXIST ]] && run ip -6 route add default via $(unzero6 $GW6) dev $DEV $AD6 done } -# function to delete default gateway per interface +# delete default gateway per interface gateway_down(){ for GW in ${GATEWAY[@]}; do [[ -z $GW ]] && continue @@ -649,7 +657,7 @@ gateway_down(){ i=(${x/,/ }) [[ -z ${i[1]} ]] && DEV=${IFNAME[$i]} || DEV=${IFNAME[$i]}.${VLANID[$x]} IP=${PROTOCOL[$x]:-ipv4} - EXIST=$(ip -4 route show default dev $DEV) + EXIST=$(ip -4 route show to default dev $DEV) [[ $IP != ipv6 && -n $EXIST ]] && run ip -4 route flush default dev $DEV done for GW6 in ${GATEWAY6[@]}; do @@ -659,12 +667,12 @@ gateway_down(){ i=(${x/,/ }) [[ -z ${i[1]} ]] && DEV=${IFNAME[$i]} || DEV=${IFNAME[$i]}.${VLANID[$x]} IP=${PROTOCOL[$x]:-ipv4} - EXIST=$(ip -6 route show default dev $DEV) + EXIST=$(ip -6 route show to default dev $DEV) [[ $IP != ipv4 && -n $EXIST ]] && run ip -6 route flush default dev $DEV done } -# function to start network +# start network start(){ lo_up for INTERFACE in ${IFNAME[@]}; do @@ -673,7 +681,7 @@ start(){ gateway_up } -# function to stop network +# stop network stop(){ gateway_down for INTERFACE in ${IFNAME[@]}; do @@ -682,7 +690,7 @@ stop(){ lo_down } -# function to show network status +# show network status status(){ echo "INTERFACE STATE INFORMATION" echo "========================================================================" @@ -693,38 +701,38 @@ status(){ # STATIC ROUTE FUNCTIONS # ########################## -# function to add static route +# add static route route_up(){ [[ -n $3 ]] && METRIC="metric $3" || METRIC= if [[ $2 == default ]]; then # determine IP protocol & optional device [[ -n ${1##*:*} ]] && IP=-4 || IP=-6 [[ -z ${1##*-*} ]] && DEV="dev ${1#*-}" || DEV= - EXIST=$(ip $IP route show default via ${1%-*} $DEV | grep "$METRIC ") + EXIST=$(ip $IP route show to default via ${1%-*} $DEV | grep "$METRIC ") [[ -z $EXIST ]] && run ip $IP route add default via ${1%-*} $DEV $METRIC elif [[ -n $2 ]]; then # determine IP protocol & gateway syntax [[ -n ${2##*:*} ]] && IP=-4 || IP=-6 [[ -e $SYSTEM/$1 ]] && GW="dev $1" || GW="via $1" - EXIST=$(ip $IP route show $2 $GW | grep "$METRIC ") + EXIST=$(ip $IP route show to $2 $GW | grep "$METRIC ") [[ -z $EXIST ]] && run ip $IP route add $2 $GW $METRIC fi } -# function to delete static route +# delete static route route_down(){ [[ -n $3 ]] && METRIC="metric $3" || METRIC= if [[ $2 == default ]]; then # determine IP protocol & optional device [[ -n ${1##*:*} ]] && IP=-4 || IP=-6 [[ -z ${1##*-*} ]] && DEV="dev ${1#*-}" || DEV= - EXIST=$(ip $IP route show default via ${1%-*} $DEV) + EXIST=$(ip $IP route show to default via ${1%-*} $DEV) [[ -n $EXIST ]] && run ip $IP route del default via ${1%-*} $DEV $METRIC elif [[ -n $2 ]]; then # determine IP protocol & gateway syntax [[ -n ${2##*:*} ]] && IP=-4 || IP=-6 [[ -e $SYSTEM/$1 ]] && GW="dev $1" || GW="via $1" - EXIST=$(ip $IP route show $2 $GW) + EXIST=$(ip $IP route show to $2 $GW) [[ -n $EXIST ]] && run ip $IP route del $2 $GW $METRIC fi } diff --git a/etc/rc.d/rc.library.source b/etc/rc.d/rc.library.source index 99861be3b..fb5f5f9d0 100644 --- a/etc/rc.d/rc.library.source +++ b/etc/rc.d/rc.library.source @@ -5,7 +5,7 @@ # Library used by nfsd, ntpd, rpc, samba, nginx, sshd, avahidaemon, show_interfaces # # Bergware - created for Unraid OS, December 2023 -# Bergware - updated January 2025 +# Bergware - updated May 2025 WIREGUARD="/etc/wireguard" NETWORK_INI="/var/local/emhttp/network.ini" @@ -56,8 +56,8 @@ good(){ show(){ case $# in - 1) ip -br addr show scope global -temporary -deprecated to $1 2>/dev/null | awk '{gsub("@.+","",$1);print $1;exit}' ;; - 2) ip -br addr show scope global -temporary -deprecated $1 $2 2>/dev/null | awk '{$1=$2="";print;exit}' | sed -r 's/ metric [0-9]+//g' ;; + 1) ip -br addr show scope global primary -deprecated to $1 2>/dev/null | awk '{gsub("@.+","",$1);print $1;exit}' ;; + 2) ip -br addr show scope global primary -deprecated $1 $2 2>/dev/null | awk '{$1=$2="";print;exit}' | sed -r 's/ metric [0-9]+//g' ;; esac } @@ -160,7 +160,7 @@ check(){ fi [[ $(ipv $ADDR) == 4 ]] && IPV4=yes || IPV6=yes done - done <<< $(ip -br addr show scope global -temporary -deprecated | awk '$1~"^(br|bond|eth|wlan|wg)[0-9]+(.[0-9]+)?" && $3!="" {gsub("@.+","",$1);$2="";print}' | sed -r 's/ metric [0-9]+//g' | sort) + done <<< $(ip -br addr show scope global primary -deprecated | awk '$1~"^(br|bond|eth|wlan|wg)[0-9]+(.[0-9]+)?" && $3!="" {gsub("@.+","",$1);$2="";print}' | sed -r 's/ metric [0-9]+//g' | sort) # add loopback interface if [[ "smb nfs" =~ "$CALLER" ]]; then [[ $IPV4 == yes ]] && BIND+=(127.0.0.1) diff --git a/etc/rc.d/rc.local b/etc/rc.d/rc.local index af02d607c..b43f02b5c 100755 --- a/etc/rc.d/rc.local +++ b/etc/rc.d/rc.local @@ -80,9 +80,14 @@ fi /usr/local/emhttp/webGui/scripts/notify smtp-init /usr/local/emhttp/webGui/scripts/notify cron-init +# start interface state monitoring +if [[ -x /usr/local/sbin/monitor_interface ]]; then + /usr/local/sbin/monitor_interface &>/dev/null +fi + # start nchan monitoring -> stop all running nchan processes when no subscribers are connected if [[ -x /usr/local/sbin/monitor_nchan ]]; then - /usr/local/sbin/monitor_nchan + /usr/local/sbin/monitor_nchan &>/dev/null fi # First boot following Unraid Server OS update: delete plugin file @@ -184,7 +189,7 @@ else if [[ -f "$PRIORITY_PLUGIN_PATH" ]]; then /usr/local/sbin/plugin install "$PRIORITY_PLUGIN_PATH" | log fi - done + done # Install remaining plugins shopt -s nullglob for PLUGIN in $CONFIG/plugins/*.plg; do diff --git a/etc/rc.d/rc.nginx b/etc/rc.d/rc.nginx index 0f903a569..c9298385e 100755 --- a/etc/rc.d/rc.nginx +++ b/etc/rc.d/rc.nginx @@ -6,7 +6,7 @@ # Written for Slackware Linux by Cherife Li . # LimeTech - modified for Unraid OS -# Bergware - modified for Unraid OS, October 2023 +# Bergware - modified for Unraid OS, May 2025 # reference: # LANNAME 'tower' @@ -537,12 +537,12 @@ build_ssl(){ # fetch LAN IP address (read management interface eth0) [[ -e $SYSTEM/bond0 ]] && DEV=bond0 || DEV=eth0 [[ -e $SYSTEM/br0 ]] && DEV=br0 - LANIP=$(ip -4 -br addr show $DEV scope global | sed -r 's/\/[0-9]+//g' | awk '{print $3;exit}') - LANIP6=$(ip -6 -br addr show $DEV scope global -temporary -deprecated | sed -r 's/\/[0-9]+//g' | awk '{print $3;exit}') + LANIP=$(ip -4 -br addr show scope global primary dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') + LANIP6=$(ip -6 -br addr show scope global primary -deprecated dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') # try wireless connection if no IP address on interface eth0 - [[ -z $LANIP && -e $SYSTEM/wlan0 ]] && LANIP=$(ip -4 -br addr show wlan0 scope global | sed -r 's/\/[0-9]+//g' | awk '{print $3;exit}') - [[ -z $LANIP6 && -e $SYSTEM/wlan0 ]] && LANIP6=$(ip -6 -br addr show wlan0 scope global -temporary -deprecated | sed -r 's/\/[0-9]+//g' | awk '{print $3;exit}') + [[ -z $LANIP && -e $SYSTEM/wlan0 ]] && LANIP=$(ip -4 -br addr show scope global primary dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') + [[ -z $LANIP6 && -e $SYSTEM/wlan0 ]] && LANIP6=$(ip -6 -br addr show scope global primary -deprecated dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') # regenerate self-signed cert if local TLD changes */ SELFCERTPATH=$SSL/certs/${LANNAME}_unraid_bundle.pem @@ -716,7 +716,7 @@ nginx_start(){ # side-load unraid-api unraid_api_control start # resume nchan publishers - /usr/local/sbin/monitor_nchan start + /usr/local/sbin/monitor_nchan start rm -f /tmp/publishPaused if nginx_running; then REPLY="Started"; else REPLY="Failed"; fi diff --git a/etc/rc.d/rc.wireless b/etc/rc.d/rc.wireless index c2f419784..8e16249ce 100755 --- a/etc/rc.d/rc.wireless +++ b/etc/rc.d/rc.wireless @@ -10,8 +10,10 @@ DAEMON="WiFi network" CALLER="wifi" INI="/var/local/emhttp/wireless.ini" CFG="/boot/config/wireless.cfg" +DOCKER="/boot/config/docker.cfg" OPENSSL="/usr/local/emhttp/webGui/scripts/open_ssl" STARTWIFI="/usr/local/emhttp/webGui/scripts/wireless" +SERVICES="/usr/local/emhttp/webGui/scripts/update_services" WPA="/etc/wpa_supplicant.conf" # system network references @@ -28,6 +30,16 @@ CONF6="/proc/sys/net/ipv6/conf" [[ -r $INI ]] && . $INI PORT=${PORT:-wlan0} +# return variable value from file +var(){ + [[ -r "$2" ]] && grep -Pom1 "^$1=\"\K[^\"]+" "$2" +} + +# return interface index +index(){ + cat $SYSTEM/$1/ifindex 2>/dev/null +} + # translate security to informational text trans(){ case "$1" in @@ -126,13 +138,21 @@ ipaddr_up(){ if carrier $PORT; then STATE="UP"; else STATE="DOWN"; fi log "interface $PORT is $STATE, setting static $IP address" ipv6_addr $PORT 0 1 + INDEX=$(index $PORT) + INDEX=$((3000 + ${INDEX:-$(($(index * | sort -n | tail -1) + 1))})) if [[ $IP == ipv4 ]]; then - [[ -n $IP4 && -n $MASK4 ]] && run ip -4 addr add $(unzero $IP4)/$MASK4 dev $PORT metric 3004 - [[ -n $GATEWAY4 ]] && run ip -4 route add default via $GATEWAY4 dev $PORT metric 3004 + if [[ -n $IP4 && -n $MASK4 ]]; then + run ip -4 addr add $(unzero $IP4)/$MASK4 metric $INDEX dev $PORT + # re-add IPv4 address of parent (if docker is running) + if [[ $(var DOCKER_ALLOW_ACCESS $DOCKER) == yes && -S /var/run/docker.sock ]]; then + ip addr add $(unzero $IP4)/$MASK4 metric $(($INDEX - 1)) dev shim-$PORT + fi + fi + [[ -n $GATEWAY4 ]] && run ip -4 route add default via $GATEWAY4 metric $INDEX dev $PORT fi if [[ $IP == ipv6 ]]; then - [[ -n $IP6 && -n $MASK6 ]] && run ip -6 addr add $(unzero6 $IP6)/$MASK6 dev $PORT metric 3004 - [[ -n $GATEWAY6 ]] && run ip -6 route add default via $GATEWAY6 dev $PORT metric 3004 + [[ -n $IP6 && -n $MASK6 ]] && run ip -6 addr add $(unzero6 $IP6)/$MASK6 metric $INDEX dev $PORT + [[ -n $GATEWAY6 ]] && run ip -6 route add default via $GATEWAY6 metric $INDEX dev $PORT fi fi if [[ $DNS == yes ]]; then @@ -148,17 +168,13 @@ ipaddr_up(){ ipaddr_down(){ if [[ $DHCP == yes ]]; then # release DHCP assigned address and default route - OPTIONS="-q -k" + OPTIONS="-q -k -$1" [[ $DNS == yes ]] && OPTIONS="$OPTIONS -C resolv.conf" - [[ $IP == ipv4 ]] && OPTIONS="$OPTIONS -4" - [[ $IP == ipv6 ]] && OPTIONS="$OPTIONS -6" run dhcpcd $OPTIONS $PORT elif [[ $DHCP == no ]]; then # release static assigned address and default route - [[ $IP == ipv4 ]] && run ip -4 addr flush dev $PORT - [[ $IP == ipv4 ]] && run ip -4 route flush default dev $PORT - [[ $IP == ipv6 ]] && run ip -6 addr flush dev $PORT - [[ $IP == ipv6 ]] && run ip -6 route flush default dev $PORT + run ip -$1 addr flush dev $PORT + run ip -$1 route flush default dev $PORT fi } @@ -253,19 +269,25 @@ wifi_stop(){ log "$DAEMON... No Wifi present." return fi - IP=ipv4 DHCP=$DHCP4 DNS=$DNS4 - ipaddr_down + SRV4=$DNS + SRV6= + ipaddr_down 4 if [[ -n $DHCP6 ]]; then - IP=ipv6 DHCP=$DHCP6 DNS=$DNS6 - ipaddr_down + SRV6=$DNS + ipaddr_down 6 fi + IPV4=$(ip -4 -br addr show scope global primary dev shim-$PORT | awk '{print $3,$4,$5;exit}') + [[ -n $IPV4 ]] && run ip addr del $IPV4 dev shim-$PORT + run ip addr flush dev $PORT run pkill wpa_supplicant run iw dev $PORT disconnect run rm -f $INI + # restart services when static assignments + [[ $SRV4 == no && (-z $SRV6 || $SRV6 == no) ]] && $SERVICES 5 if ! wifi_running; then REPLY="Stopped"; else REPLY="Failed"; fi log "$DAEMON... $REPLY." } @@ -309,6 +331,8 @@ wifi_join(){ IP=ipv4 DHCP=$DHCP4 DNS=$DNS4 + SRV4=$DNS + SRV6= ipaddr_up # IPv6 address assignment (if enabled) if [[ -n $DHCP6 ]]; then @@ -316,10 +340,13 @@ wifi_join(){ IP=ipv6 DHCP=$DHCP6 DNS=$DNS6 + SRV6=$DNS ipaddr_up else echo 1 >$CONF6/$PORT/disable_ipv6 fi + # restart services when static assignments + [[ $SRV4 == no && (-z $SRV6 || $SRV6 == no) ]] && $SERVICES 5 if wifi_running; then if [[ -z $CC ]]; then CC=($(iw reg get | grep -Po '^country \K..')) diff --git a/sbin/create_network_ini b/sbin/create_network_ini index afe6c39e1..c2f9e55e5 100755 --- a/sbin/create_network_ini +++ b/sbin/create_network_ini @@ -13,23 +13,42 @@ [[ (-z $reason && -z $1) || (-n $reason && ! "BOUND6 IPV4LL EXPIRE" =~ $reason) ]] && exit 0 -INI=/var/local/emhttp/network.ini.new -CFG=/boot/config/network.cfg -SYS=/sys/class/net +INI="/var/local/emhttp/network.ini.new" +STA="/var/local/emhttp/statics.ini.new" +CFG="/boot/config/network.cfg" +DOCKER="/boot/config/docker.cfg" +SYSTEM="/sys/class/net" declare -A VLANID USE_DHCP IPADDR NETMASK GATEWAY METRIC USE_DHCP6 IPADDR6 NETMASK6 GATEWAY6 PRIVACY6 METRIC6 DESCRIPTION PROTOCOL # run & log functions . /etc/rc.d/rc.runlog -mask(){ - [[ -z $1 ]] && return - # convert prefix to netmask - set -- $((5-($1/8))) 255 255 255 255 $(((255<<(8-($1%8)))&255)) 0 0 0 - [[ $1 -gt 1 ]] && shift $1 || shift - echo $1.$2.$3.$4 +# return variable value from file +var(){ + [[ -r "$2" ]] && grep -Pom1 "^$1=\"\K[^\"]+" "$2" } +# return interface index +index(){ + cat $SYSTEM/$1/ifindex 2>/dev/null +} + +# convert netmask to prefix +mask2cidr(){ + [[ -z $1 ]] && return + local MASK=$(eval eval echo "'\$((('{"${1//./,}"}'>>'{7..0}')%2))'") + eval echo '$(('"${MASK// /+}"'))' +} + +# convert prefix to netmask +cidr2mask(){ + [[ -z $1 ]] && return + local MASK=$(eval echo '$(((1<<32)-1<<32-$1>>'{3..0}'*8&255))') + echo "${MASK// /.}" +} + +# return dns nameserver entry dns() { [[ $1 == 4 ]] && ADDR='(\d{1,3}\.){3}\d+' || ADDR='([0-9a-fA-F]{1,4}::?){1,7}[0-9a-fA-F]*' grep -Po "^nameserver \K$ADDR" /etc/resolv.conf @@ -48,8 +67,10 @@ else BONDING=yes BRIDGING=yes fi + # prepare empty file echo -n >$INI +echo -n >$STA # loop thru all defined interfaces (=1 in case of legacy) for ((i=0; i<${SYSNICS:-1}; i++)); do @@ -57,7 +78,7 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do ETH=${IFACE/#bond/eth} ETH=${ETH/#br/eth} # don't store when non-existing - [[ -e $SYS/$ETH ]] || continue + [[ -e $SYSTEM/$ETH ]] || continue echo "[$ETH]" >>$INI if [[ $i -eq 0 ]]; then # process legacy settings @@ -125,12 +146,13 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do echo "PROTOCOL:0=\"${PROTOCOL[$i]}\"" >>$INI echo "USE_DHCP:0=\"${USE_DHCP[$i]}\"" >>$INI echo "USE_GW4:0=\"${USE_GW4[$i]}\"" >>$INI + DATA= if [[ ${USE_DHCP[$i]} == yes ]]; then # get dhcp assigned ipv4 address & mask - NET=($(ip -4 -br addr show $IFACE | awk '{sub("/"," ",$3);print $3;exit}')) - GW=$(ip -4 route show default dev $IFACE | awk '{print $3;exit}') + NET=($(ip -4 -br addr show scope global primary dev $IFACE | awk '{sub("/"," ",$3);print $3;exit}')) + GW=$(ip -4 route show to default dev $IFACE | awk '{print $3;exit}') echo "IPADDR:0=\"${NET[0]}\"" >>$INI - echo "NETMASK:0=\"$(mask ${NET[1]})\"" >>$INI + echo "NETMASK:0=\"$(cidr2mask ${NET[1]})\"" >>$INI echo "GATEWAY:0=\"$GW\"" >>$INI echo "METRIC:0=\"${METRIC[$i]}\"" >>$INI else @@ -139,13 +161,15 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do echo "NETMASK:0=\"${NETMASK[$i]}\"" >>$INI echo "GATEWAY:0=\"${GATEWAY[$i]}\"" >>$INI echo "METRIC:0=\"${METRIC[$i]}\"" >>$INI + # store static ipv4 assignment + [[ -n ${IPADDR[$i]} ]] && DATA="$IFACE ${IPADDR[$i]}/$(mask2cidr ${NETMASK[$i]}) ${METRIC[$i]:-0}" fi echo "USE_DHCP6:0=\"${USE_DHCP6[$i]}\"" >>$INI echo "USE_GW6:0=\"${USE_GW6[$i]}\"" >>$INI if [[ ${USE_DHCP6[$i]} == yes ]]; then # get auto assigned ipv6 address & prefix - NET6=($(ip -6 -br addr show $IFACE scope global -temporary -deprecated | awk '{sub("/"," ",$3);print $3;exit}')) - GW6=$(ip -6 route show default dev $IFACE | awk '{print $3;exit}') + NET6=($(ip -6 -br addr show scope global primary -deprecated dev $IFACE | awk '{sub("/"," ",$3);print $3;exit}')) + GW6=$(ip -6 route show to default dev $IFACE | awk '{print $3;exit}') echo "IPADDR6:0=\"${NET6[0]}\"" >>$INI echo "NETMASK6:0=\"${NET6[1]}\"" >>$INI echo "GATEWAY6:0=\"$GW6\"" >>$INI @@ -158,7 +182,10 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do echo "GATEWAY6:0=\"${GATEWAY6[$i]}\"" >>$INI echo "METRIC6:0=\"${METRIC6[$i]}\"" >>$INI echo "PRIVACY6:0=\"\"" >>$INI + # store static ipv4 assignment + [[ -n ${IPADDR6[$i]} ]] && DATA="${DATA:-$IFACE} ${IPADDR6[$i]}/${NETMASK6[$i]} ${METRIC6[$i]:-0}" fi + [[ -n $DATA ]] && echo "$DATA" >>$STA echo "USE_MTU=\"${USE_MTU[$i]}\"" >>$INI echo "MTU=\"${MTU[$i]}\"" >>$INI if [[ -n ${VLANS[$i]} ]]; then @@ -170,13 +197,14 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do echo "PROTOCOL:$j=\"${PROTOCOL[$i,$j]}\"" >>$INI echo "USE_DHCP:$j=\"${USE_DHCP[$i,$j]}\"" >>$INI echo "USE_GW4:$j=\"${USE_GW4[$i,$j]}\"" >>$INI + DATA= if [[ ${USE_DHCP[$i,$j]} == yes ]]; then DEV=$IFACE.${VLANID[$i,$j]} - # get dhcp assigned ipv4 address & mask - NET=($(ip -4 -br addr show $DEV | awk '{sub("/"," ",$3);print $3;exit}')) - GW=$(ip -4 route show default dev $DEV | awk '{print $3;exit}') + # get dhcp assigned ipv4 address & cidr2mask + NET=($(ip -4 -br addr show scope global primary dev $DEV | awk '{sub("/"," ",$3);print $3;exit}')) + GW=$(ip -4 route show to default dev $DEV | awk '{print $3;exit}') echo "IPADDR:$j=\"${NET[0]}\"" >>$INI - echo "NETMASK:$j=\"$(mask ${NET[1]})\"" >>$INI + echo "NETMASK:$j=\"$(cidr2mask ${NET[1]})\"" >>$INI echo "GATEWAY:$j=\"$GW\"" >>$INI echo "METRIC:$j=\"${METRIC[$i,$j]}\"" >>$INI else @@ -185,14 +213,16 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do echo "NETMASK:$j=\"${NETMASK[$i,$j]}\"" >>$INI echo "GATEWAY:$j=\"${GATEWAY[$i,$j]}\"" >>$INI echo "METRIC:$j=\"${METRIC[$i,$j]}\"" >>$INI + # store static ipv6 assignment + [[ -n ${IPADDR[$i,$j]} ]] && DATA="$DEV ${IPADDR[$i,$j]}/$(mask2cidr ${NETMASK[$i,$j]}) ${METRIC[$i,$j]:-0}" fi echo "USE_DHCP6:$j=\"${USE_DHCP6[$i,$j]}\"" >>$INI echo "USE_GW6:$j=\"${USE_GW6[$i,$j]}\"" >>$INI if [[ ${USE_DHCP6[$i,$j]} == yes ]]; then DEV=$IFACE.${VLANID[$i,$j]} # get auto assigned ipv6 address & prefix - NET6=($(ip -6 -br addr show $DEV scope global -temporary -deprecated | awk '{sub("/"," ",$3);print $3;exit}')) - GW6=$(ip -6 route show default dev $DEV | awk '{print $3;exit}') + NET6=($(ip -6 -br addr show scope global primary -deprecated dev $DEV | awk '{sub("/"," ",$3);print $3;exit}')) + GW6=$(ip -6 route show to default dev $DEV | awk '{print $3;exit}') echo "IPADDR6:$j=\"${NET6[0]}\"" >>$INI echo "NETMASK6:$j=\"${NET6[1]}\"" >>$INI echo "GATEWAY6:$j=\"$GW6\"" >>$INI @@ -205,7 +235,11 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do echo "GATEWAY6:$j=\"${GATEWAY6[$i,$j]}\"" >>$INI echo "METRIC6:$j=\"${METRIC6[$i,$j]}\"" >>$INI echo "PRIVACY6:$j=\"\"" >>$INI + # store static ipv6 assignment + [[ -n ${IPADDR6[$i,$j]} ]] && DATA="${DATA:-$DEV} ${IPADDR6[$i,$j]}/${NETMASK6[$i,$j]} ${METRIC6[$i,$j]:-0}" fi + # static IP assignments handled by rc.monitor + [[ -n $DATA ]] && echo "$DATA" >>$STA done else # interface without VLANs @@ -214,6 +248,25 @@ for ((i=0; i<${SYSNICS:-1}; i++)); do done # atomically update file mv $INI ${INI%.*} +mv $STA ${STA%.*} + +# add or remove IPv4 assignment from attached interface when Docker custom network access is allowed +[[ ${interface:0:2} == br || $interface == wlan0 ]] && LINK=shim-$interface || LINK=vhost${interface//[^0-9.]/} +if [[ -e $SYSTEM/$LINK && $(var DOCKER_ALLOW_ACCESS $DOCKER) == yes ]]; then + IPV4=$(ip -4 -br addr show scope global primary dev $interface | awk '{print $3;exit}') + [[ $interface == wlan0 ]] && INDEX=3000 || INDEX=1000 + INDEX=$(($INDEX - 1 + $(index $interface))) + case $reason in + 'BOUND' | 'BOUND6') + # re-add IPv4 address of parent (if docker is running) + [[ -S /var/run/docker.sock ]] && ip addr add $IPV4 metric $INDEX dev $LINK + ;; + 'EXPIRE') + # remove IPv4 address of parent + ip addr del $IPV4 metric $INDEX dev $LINK + ;; + esac +fi log "interface=${interface:-$1}, reason=$reason, protocol=$protocol" # delayed execution @@ -224,19 +277,19 @@ if [[ -z $interface || "eth0 br0 bond0 wlan0" =~ $interface ]]; then . /etc/unraid-version echo -e "Unraid Server OS version: $version" >/etc/issue # find management interface - [[ -e $SYS/bond0 ]] && dev=bond0 || dev=eth0 - [[ -e $SYS/br0 ]] && dev=br0 - IPv4=$(ip -4 -br addr show $dev scope global | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') - IPv6=$(ip -6 -br addr show $dev scope global -temporary -deprecated | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') + [[ -e $SYSTEM/bond0 ]] && DEV=bond0 || DEV=eth0 + [[ -e $SYSTEM/br0 ]] && DEV=br0 + IPV4=$(ip -4 -br addr show scope global primary dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') + IPV6=$(ip -6 -br addr show scope global primary -deprecated dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') # show current IP assignment - [[ -n $IPv4 ]] && echo " IPv4 address: $IPv4" >>/etc/issue || echo " IPv4 address: not set" >>/etc/issue - [[ -n $IPv6 ]] && echo " IPv6 address: $IPv6" >>/etc/issue || echo " IPv6 address: not set" >>/etc/issue - if [[ -e $SYS/wlan0 ]]; then + [[ -n $IPV4 ]] && echo " IPv4 address: $IPV4" >>/etc/issue || echo " IPv4 address: not set" >>/etc/issue + [[ -n $IPV6 ]] && echo " IPv6 address: $IPV6" >>/etc/issue || echo " IPv6 address: not set" >>/etc/issue + if [[ -e $SYSTEM/wlan0 ]]; then echo "Wireless network:" >>/etc/issue - IPv4=$(ip -4 -br addr show wlan0 scope global | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') - IPv6=$(ip -6 -br addr show wlan0 scope global -temporary -deprecated | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') - [[ -n $IPv4 ]] && echo " IPv4 address: $IPv4" >>/etc/issue || echo " IPv4 address: not set" >>/etc/issue - [[ -n $IPv6 ]] && echo " IPv6 address: $IPv6" >>/etc/issue || echo " IPv6 address: not set" >>/etc/issue + IPV4=$(ip -4 -br addr show scope global primary dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') + IPV6=$(ip -6 -br addr show scope global primary -deprecated dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//') + [[ -n $IPV4 ]] && echo " IPv4 address: $IPV4" >>/etc/issue || echo " IPv4 address: not set" >>/etc/issue + [[ -n $IPV6 ]] && echo " IPv6 address: $IPV6" >>/etc/issue || echo " IPv6 address: not set" >>/etc/issue fi echo >>/etc/issue fi diff --git a/sbin/monitor_interface b/sbin/monitor_interface new file mode 100755 index 000000000..041f02856 --- /dev/null +++ b/sbin/monitor_interface @@ -0,0 +1,70 @@ +#!/bin/bash +# +# script: monitor_interface +# +# Monitors a given list of interfaces and add or remove static IP addresses to these interfaces. +# The list of interfaces is provided in the file '/var/local/emhttp/statics.ini' +# This file is maintained by the script 'create_network_ini' which keep track of all IP assignments. +# +# By removing static IP addresses from inactive interfaces, these interfaces do not longer interfere with wireless. +# In other words the wired connection can be pulled without consequences. +# +# Bergware - modified for Unraid OS, May 2025 + +FILE=/var/local/emhttp/statics.ini +SYSTEM=/sys/class/net + +state(){ + cat $SYSTEM/$1/operstate 2>/dev/null +} + +md5(){ + [[ -r $FILE ]] && md5sum $FILE | awk '{print $1;exit}' +} + +switch(){ + local n + [[ -z $1 ]] && return 1 + # state change should stay stable for at least 5 seconds + for n in {1..5}; do + [[ $(state $1) == $2 ]] && return 1 || sleep 1 + done + return 0 +} + +init(){ + PORT=(); IPV4=(); METRIC4=(); IPV6=(); METRIC6=(); STATE=(); + if [[ -r $FILE ]]; then + # initialize values from file, maintained by 'create_network_ini' + while IFS=$'\n' read -r ROW; do + ROW=($ROW) + PORT+=(${ROW[0]}) + IPV4+=(${ROW[1]:--}) + METRIC4+=(${ROW[2]:--}) + IPV6+=(${ROW[3]:--}) + METRIC6+=(${ROW[4]:--}) + STATE+=($(state ${ROW[0]})) + done <$FILE + fi + MD5=$(md5) +} + +while :; do + # monitor file content changes + [[ $MD5 != $(md5) ]] && init + for i in ${!PORT[@]}; do + # did interface state change? + if switch ${PORT[$i]} ${STATE[$i]}; then + STATE[$i]=$(state ${PORT[$i]}) + if [[ ${STATE[$i]} == up ]]; then + [[ ${IPV4[$i]} != '-' ]] && ip addr add ${IPV4[$i]} metric ${METRIC4[$i]} dev ${PORT[$i]} + [[ ${IPV6[$i]} != '-' ]] && ip addr add ${IPV6[$i]} metric ${METRIC6[$i]} dev ${PORT[$i]} + elif [[ ${STATE[$i]} == down ]]; then + ip addr flush scope global dev ${PORT[$i]} + fi + fi + done + # check every 3 seconds + sleep 3 +done & +disown %%