diff --git a/.gitignore b/.gitignore index f56c925342..7e09c0d4e9 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ emhttp/plugins/pnpm-lock.yaml # api source code unraid-api/ +unraid/ # Node.js version .node-version @@ -88,6 +89,9 @@ lib/node_modules bin/ sbin/unraid-api +# Provided by Unraid API +emhttp/redirect.htm + # Unraid API readme/changelog emhttp/plugins/dynamix.unraid.net diff --git a/emhttp/auth-request.php b/emhttp/auth-request.php index b1c5ebea2d..dc21578877 100644 --- a/emhttp/auth-request.php +++ b/emhttp/auth-request.php @@ -14,8 +14,52 @@ session_write_close(); } -// Include JS caching functions -require_once '/usr/local/emhttp/webGui/include/JSCache.php'; +function isPathInDocroot(string $realPath, string $docroot): bool { + return $realPath === $docroot || str_starts_with($realPath, $docroot . '/'); +} + +function getCanonicalRequestUri(string $docroot): string { + $requestUri = getRequestUriPath(); + + $realRequestPath = realpath($docroot . '/' . ltrim($requestUri, '/')); + if (!is_string($realRequestPath) || !isPathInDocroot($realRequestPath, $docroot)) { + return ''; + } + + $canonicalRequestUri = substr($realRequestPath, strlen($docroot)); + return $canonicalRequestUri === '' ? '/' : $canonicalRequestUri; +} + +function isWebComponentsRequest(string $requestUri): bool { + $webComponentsDirectory = '/plugins/dynamix.my.servers/unraid-components'; + return $requestUri === $webComponentsDirectory || str_starts_with($requestUri, $webComponentsDirectory . '/'); +} + +function getRequestUriPath(): string { + $requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + return is_string($requestUri) ? $requestUri : '/'; +} + +function getAllowedExternalPublicAssetTargets(): array { + return [ + '/webGui/images/case-model.png' => '/boot/config/plugins/dynamix/case-model.png', + ]; +} + +function isAllowedPublicAssetRequest(string $requestUri, string $docroot, array $arrWhitelist): bool { + if (!in_array($requestUri, $arrWhitelist, true)) { + return false; + } + + $realRequestPath = realpath($docroot . '/' . ltrim($requestUri, '/')); + if (is_string($realRequestPath) && isPathInDocroot($realRequestPath, $docroot)) { + return true; + } + + $allowedExternalTargets = getAllowedExternalPublicAssetTargets(); + return isset($allowedExternalTargets[$requestUri]) && + $realRequestPath === $allowedExternalTargets[$requestUri]; +} // Base whitelist of files $arrWhitelist = [ @@ -54,12 +98,22 @@ '/manifest.json' ]; -// Whitelist ALL files from the unraid-components directory -$webComponentsDirectory = '/plugins/dynamix.my.servers/unraid-components/'; -$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/'; +// Use canonical filesystem path checks against the trusted docroot. +$docroot = '/usr/local/emhttp'; +$requestUri = getRequestUriPath(); +$canonicalRequestUri = getCanonicalRequestUri($docroot); + +// Allow explicit public assets with strict target checks. +if (isAllowedPublicAssetRequest($requestUri, $docroot, $arrWhitelist)) { + http_response_code(200); + exit; +} -// Check if the request is for any file in the unraid-components directory -if (str_starts_with($requestUri, $webComponentsDirectory) || in_array($requestUri, $arrWhitelist)) { +// Allow canonical requests under unraid-components. +if ( + $canonicalRequestUri !== '' && + isWebComponentsRequest($canonicalRequestUri) +) { // authorized http_response_code(200); } else { diff --git a/emhttp/languages/en_US/helptext.txt b/emhttp/languages/en_US/helptext.txt index 08a4d856c9..e66f84d57f 100644 --- a/emhttp/languages/en_US/helptext.txt +++ b/emhttp/languages/en_US/helptext.txt @@ -2367,6 +2367,12 @@ Generally speaking, it is recommended to leave this setting to its default value IMPORTANT NOTE: If adjusting port mappings, do not modify the settings for the Container port as only the Host port can be adjusted. :end +:docker_fixed_mac_help: +Assigns the container's MAC address on the selected Docker network endpoint. Use a valid unicast MAC address; the first octet must be even, e.g. 02:42:9a:0d:7e:c0. + +This avoids using the legacy container-level --mac-address option in Extra Parameters. +:end + :docker_container_network_help: This allows your container to utilize the network configuration of another container. Select the appropriate container from the list.
This setup can be particularly beneficial if you wish to route your container's traffic through a VPN. :end diff --git a/emhttp/plugins/dynamix.docker.manager/DockerContainers.page b/emhttp/plugins/dynamix.docker.manager/DockerContainers.page index 8cf6e687ca..053c6d9585 100755 --- a/emhttp/plugins/dynamix.docker.manager/DockerContainers.page +++ b/emhttp/plugins/dynamix.docker.manager/DockerContainers.page @@ -37,7 +37,7 @@ $cpus = cpu_list(); _(Application)_ _(Version)_ _(Network)_ - _(Container IP)_ + _(Container IP)_ / _(MAC)_ _(Container Port)_ _(LAN IP:Port)_ _(Volume Mappings)_ (_(App to Host)_) @@ -46,7 +46,7 @@ $cpus = cpu_list(); _(Uptime)_ - + diff --git a/emhttp/plugins/dynamix.docker.manager/DockerSettings.page b/emhttp/plugins/dynamix.docker.manager/DockerSettings.page index d6f7893d26..5329d04309 100644 --- a/emhttp/plugins/dynamix.docker.manager/DockerSettings.page +++ b/emhttp/plugins/dynamix.docker.manager/DockerSettings.page @@ -115,6 +115,17 @@ function base_net($route) { return substr(explode('/',$route)[0],0,-2); } +// Derive the IPv4 gateway rc.docker would use when none is configured: +// the subnet's first usable host (e.g. 192.168.10.0/24 -> 192.168.10.1). +// Calls the same scripts/derive_gateway helper rc.docker uses, so the logic +// is not duplicated. Returns '' if not derivable. +function derive_gw($route) { + global $docroot; + $route = trim((string)$route); + if ($route === '') return ''; + return exec($docroot."/plugins/dynamix.docker.manager/scripts/derive_gateway ".escapeshellarg($route)); +} + $bgcolor = $themeHelper->isLightTheme() ? '#f2f2f2' : '#1c1c1c'; // $themeHelper set in DefaultPageLayout.php //Check if docker.cfg does exist @@ -315,6 +326,7 @@ _(Preserve user defined networks)_: $net = normalize($network); $docker_auto = "DOCKER_AUTO_$net"; $docker_dhcp = "DOCKER_DHCP_$net"; +$docker_gw = "DOCKER_GATEWAY_$net"; ?> @@ -369,8 +381,9 @@ _(IPv4 custom network on interface)_ (_(optional)_): : '._('Subnet').': ', $route)?> - - : + + : + > @@ -464,6 +477,7 @@ _(IPv4 custom network on interface)_ (_(optional)_): $net = normalize($network); $docker_auto = "DOCKER_AUTO_$net"; $docker_dhcp6 = "DOCKER_DHCP6_$net"; +$docker_gw6 = "DOCKER_GATEWAY6_$net"; ?> @@ -485,7 +499,10 @@ _(IPv6 custom network on interface)_ (_(optional)_): **_(Subnet)_:** '._('Subnet').': ', $route)?> - **_(Gateway)_:** + + : + > + @@ -591,7 +608,7 @@ $docker_dhcp = "DOCKER_DHCP_$net"; _(IPv4 custom network on interface)_ : : **_(Subnet)_:** '._('Subnet').': ', $route)?> - **_(Gateway)_:** + **_(Gateway)_:** **_(DHCP pool)_:**   ( _(hosts)_) @@ -784,7 +801,9 @@ function prepareDocker(form) { $(mask).prop('disabled',true); }); $(form).find('input[name^="DOCKER_GATEWAY_"]').each(function(){ - var edit = '#'+$(this).attr('name').replace('GATEWAY','CUSTOM')+'_edit'; + var custom = '#'+$(this).attr('name').replace('GATEWAY','CUSTOM')+'_edit'; + var auto = '#'+$(this).attr('name').replace('GATEWAY','DHCP')+'_edit'; + var edit = $(custom).length ? custom : auto; if (!$(edit).prop('checked')) $(this).val('').prop('disabled',false); }); $(form).find('input[name^="DOCKER_RANGE_"]').each(function(){ @@ -809,7 +828,9 @@ function prepareDocker(form) { $(mask6).prop('disabled',true); }); $(form).find('input[name^="DOCKER_GATEWAY6_"]').each(function(){ - var edit6 = '#'+$(this).attr('name').replace('GATEWAY','CUSTOM')+'_edit'; + var custom6 = '#'+$(this).attr('name').replace('GATEWAY','CUSTOM')+'_edit'; + var auto6 = '#'+$(this).attr('name').replace('GATEWAY','DHCP')+'_edit'; + var edit6 = $(custom6).length ? custom6 : auto6; if (!$(edit6).prop('checked')) $(this).val('').prop('disabled',false); }); $(form).find('input[name^="DOCKER_RANGE6_"]').each(function(){ @@ -842,6 +863,7 @@ function changeEdit(id, ip) { auto.val(auto.val().replace(ip,'')); if (auto.val() == '') auto.val('no'); } + $(id1+'gw').prop('disabled',!checked); changeDHCP(id, ip, $('#'+id.replace('edit','dhcp')).prop('checked')); } diff --git a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php index 794faadfbe..2966cf214a 100755 --- a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php +++ b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php @@ -202,10 +202,9 @@ function cpu_pinning() { if (preg_match('/^container:(.*)/', $Network)) { $Net_Container = str_replace("container:", "", $Network); } else { - preg_match("/--(net|network)=container:[^\s]+/", $ExtraParams, $NetworkParam); - if (!empty($NetworkParam[0])) { - $Net_Container = explode(':', $NetworkParam[0])[1]; - $Net_Container = str_replace(['"', "'"], '', $Net_Container); + preg_match("/--(?:net|network)(?:=|\s+)(['\"]?)container:([^'\"\s]+)\\1/", $ExtraParams, $NetworkParam); + if (!empty($NetworkParam[2])) { + $Net_Container = $NetworkParam[2]; } } // check if the container still exists from which the network should be used, if it doesn't exist any more recreate container with network none and don't start it @@ -213,7 +212,7 @@ function cpu_pinning() { $Net_Container_ID = $DockerClient->getContainerID($Net_Container); if (empty($Net_Container_ID)) { $cmd = str_replace('/docker run -d ', '/docker create ', $cmd); - $cmd = preg_replace("/--(net|network)=(['\"]?)container:[^'\"]+\\2/", "--network=none ", $cmd); + $cmd = preg_replace("/--(?:net|network)(?:=|\s+)(['\"]?)container:[^'\"\s]+\\1/", "--network=none ", $cmd); } } // force kill container if still running after time-out @@ -736,6 +735,8 @@ function removeConfig(num) { function prepareConfig(form) { var types = [], values = [], targets = [], vcpu = []; + var myMAC = $(form).find('input[name="contMyMAC"]').val().trim().replaceAll('-', ':').toLowerCase(); + $(form).find('input[name="contMyMAC"]').val(myMAC); if ($('select[name="contNetwork"]').val()=='host') { $(form).find('input[name="confType[]"]').each(function(){types.push($(this).val());}); $(form).find('input[name="confValue[]"]').each(function(){values.push($(this));}); @@ -744,6 +745,7 @@ function prepareConfig(form) { } $(form).find('input[id^="box"]').each(function(){if ($(this).prop('checked')) vcpu.push($('#'+$(this).prop('id').replace('box','cpu')).text());}); form.contCPUset.value = vcpu.join(','); + return true; } function makeName(type) { @@ -893,7 +895,7 @@ function prepareCategory() { ?>
-
+ @@ -1111,6 +1113,14 @@ function prepareCategory() {
+
+_(Fixed MAC address)_ (_(optional)_): +: + +:docker_fixed_mac_help: + +
+
_(Container Network)_: : + + +
diff --git a/emhttp/plugins/dynamix/DashStats.page b/emhttp/plugins/dynamix/DashStats.page index 5522666aed..1bb1677358 100755 --- a/emhttp/plugins/dynamix/DashStats.page +++ b/emhttp/plugins/dynamix/DashStats.page @@ -151,34 +151,45 @@ $total = exec("awk '/^MemTotal/{print $2*1024}' /proc/meminfo"); unset($ports); exec("ls --indicator-style=none /sys/class/net|grep -Po '^(bond|eth|wlan)\d+$'",$ports); $ports[] = 'lo'; -$sizes = ['MB','GB','TB']; +$memory_units = [ + // dmidecode reports memory capacities using binary math; treat both SI and IEC labels as base-1024. + // This keeps parsing consistent across dmidecode versions that may print GB/TB or GiB/TiB. + 'kb' => 1/1024, 'kib' => 1/1024, + 'mb' => 1, 'mib' => 1, + 'gb' => 1024, 'gib' => 1024, + 'tb' => 1048576,'tib' => 1048576, + 'pb' => 1073741824, 'pib' => 1073741824 +]; +$parse_memory_to_mib = function($value) use ($memory_units) { + if (!preg_match('/([0-9.]+)\s*([A-Za-z]+)/',$value ?? '',$match)) return 0; + $size = (float)$match[1]; + $unit = strtolower($match[2]); + return isset($memory_units[$unit]) ? (int)round($size*$memory_units[$unit]) : 0; +}; $memory_type = $ecc = ''; $memory_installed = $memory_maximum = 0; $memory_devices = dmidecode('Memory Device','17'); foreach ($memory_devices as $device) { - if (!is_numeric($device['Size'][0])) continue; - [$size, $unit] = my_explode(' ',$device['Size']??''); - $base = array_search($unit,$sizes); - if ($base!==false) $memory_installed += $size*pow(1024,$base); + $memory_installed += $parse_memory_to_mib($device['Size'] ?? ''); if (!$memory_type && isset($device['Type']) && $device['Type']!='Unknown') $memory_type = $device['Type']; } $memory_array = dmidecode('Physical Memory Array','16'); foreach ($memory_array as $device) { - [$size, $unit] = my_explode(' ',$device['Maximum Capacity']??''); - $base = array_search($unit,$sizes); - if ($base>=1) $memory_maximum += $size*pow(1024,$base); + $memory_maximum += $parse_memory_to_mib($device['Maximum Capacity'] ?? ''); if (!$ecc && isset($device['Error Correction Type']) && $device['Error Correction Type']!='None') $ecc = "{$device['Error Correction Type']} "; } if ($memory_installed >= 1048576) { $memory_installed = round($memory_installed/1048576); $memory_maximum = round($memory_maximum/1048576); - $unit = 'TiB'; + $memory_unit = 'TiB'; } else { if ($memory_installed >= 1024) { $memory_installed = round($memory_installed/1024); $memory_maximum = round($memory_maximum/1024); - $unit = 'GiB';} -else $unit = 'MiB'; } + $memory_unit = 'GiB';} +else $memory_unit = 'MiB'; } + +$unit = $memory_unit; // get system resources size exec("df --output=size /boot /var/log /var/lib/docker 2>/dev/null|awk '(NR>1){print $1*1024}'",$df); @@ -433,7 +444,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout - _(Memory)_: + _(Memory)_: @@ -464,7 +475,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
_(Usable size)_:
- _(Maximum size)_: + _(Maximum size)_:
_(Legend)_ diff --git a/emhttp/plugins/dynamix/MoverSettings.page b/emhttp/plugins/dynamix/MoverSettings.page index e6576111a4..e67f46f949 100755 --- a/emhttp/plugins/dynamix/MoverSettings.page +++ b/emhttp/plugins/dynamix/MoverSettings.page @@ -18,13 +18,15 @@ Tag="calendar-check-o" $mode = ['Disabled','Hourly','Daily','Weekly','Monthly']; $days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; $setup = true; +$buttontext = _('Move now'); if (!$pool_devices) { - echo "

"._('No Cache device present')."!

"; - $setup = false; + echo "

"._('No Cache device present only empty function will run')."!

"; + $setup = true; + $buttontext = _('Empty now'); } elseif ($var['shareUser']=='-') { echo "

"._('User shares not enabled')."!

"; $setup = false; -} +} if (empty($var['shareMoverSchedule'])) { $cron = explode(' ', "* * * * *"); $move = 0; @@ -32,7 +34,7 @@ if (empty($var['shareMoverSchedule'])) { $cron = explode(' ', $var['shareMoverSchedule']); $move = $cron[2]!='*' ? 4 : ($cron[4]!='*' ? 3 : (substr($cron[1],0,1)!='*' ? 2 : 1)); } -$showMoverButton = $setup && $pool_devices; +$showMoverButton = $setup; $moverRunning = file_exists('/var/run/mover.pid'); ?> - - - diff --git a/emhttp/update.php b/emhttp/update.php index b1b91fee6b..4046ca4a89 100644 --- a/emhttp/update.php +++ b/emhttp/update.php @@ -114,7 +114,7 @@ function write_log($string) { if (strpos($command, $docroot) !== 0) $command = "$docroot/$command"; $command = realpath($command); - if ($command === false) + if ($command === false || strpos($command, rtrim(realpath($docroot), '/') . '/') !== 0) syslog(LOG_INFO, "Invalid #command: {$_POST['#command']}"); else { $command = escapeshellcmd($command); diff --git a/etc/rc.d/rc.M b/etc/rc.d/rc.M index 714bdf993f..04bbd07409 100755 --- a/etc/rc.d/rc.M +++ b/etc/rc.d/rc.M @@ -246,7 +246,8 @@ fi # Start avahi: if [[ -x /etc/rc.d/rc.avahidaemon ]]; then /etc/rc.d/rc.avahidaemon start - /etc/rc.d/rc.avahidnsconfd start + # disable by default, users can start manually if needed + # /etc/rc.d/rc.avahidnsconfd start fi # Start Samba (a file/print server for Windows machines). diff --git a/etc/rc.d/rc.docker b/etc/rc.d/rc.docker index e9ac334d98..cbfbeac560 100755 --- a/etc/rc.d/rc.docker +++ b/etc/rc.d/rc.docker @@ -192,6 +192,51 @@ network(){ docker network ls --filter driver="$1" --format='{{.Name}}' 2>/dev/null | grep -P "^[a-z]+$2(\$|\.)" | tr '\n' ' ' } +configured_gateway(){ + local NETWORK=$1 + local KEY=$2 + local CFG=${NETWORK_CFG:-/boot/config/network.cfg} + [[ -s $CFG ]] || return + + ( + declare -A VLANID USE_DHCP IPADDR NETMASK GATEWAY METRIC USE_DHCP6 IPADDR6 NETMASK6 GATEWAY6 METRIC6 PRIVACY6 DESCRIPTION PROTOCOL + local BASE=${NETWORK%%.*} + local VLAN= + local IFACE ETH VALUE + local CANDIDATE + local -a CANDIDATES + local i j + + [[ $NETWORK == *.* ]] && VLAN=${NETWORK#*.} + . <(fromdos <"$CFG") + + for ((i=0; i<${SYSNICS:-1}; i++)); do + IFACE=${IFNAME[$i]:-eth$i} + ETH=${IFACE/#br/eth} + ETH=${ETH/#bond/eth} + CANDIDATES=("$IFACE" "$ETH" "${BRNAME[$i]}" "${BONDNAME[$i]}") + if [[ $i -eq 0 ]]; then + [[ ${BRIDGING:-} == yes ]] && CANDIDATES+=("br0") + [[ ${BONDING:-} == yes ]] && CANDIDATES+=("bond0") + fi + for CANDIDATE in "${CANDIDATES[@]}"; do + [[ -n $CANDIDATE && $CANDIDATE == "$BASE" ]] || continue + if [[ -z $VLAN ]]; then + [[ $KEY == GATEWAY6 ]] && VALUE=${GATEWAY6[$i]} || VALUE=${GATEWAY[$i]} + [[ -n $VALUE ]] && printf '%s\n' "$VALUE" + exit + fi + for ((j=1; j<${VLANS[$i]:-0}; j++)); do + [[ ${VLANID[$i,$j]} == "$VLAN" ]] || continue + [[ $KEY == GATEWAY6 ]] && VALUE=${GATEWAY6[$i,$j]} || VALUE=${GATEWAY[$i,$j]} + [[ -n $VALUE ]] && printf '%s\n' "$VALUE" + exit + done + done + done + ) +} + # Is container running? container_running(){ local CONTAINER @@ -227,6 +272,97 @@ read_dom(){ read -d \< ENTITY CONTENT } +netrestore_add(){ + local NETWORK=$1 + local CONTAINER=$2 + local IPS=$3 + local MAC=$4 + local ENTRY="${CONTAINER}|${IPS}|${MAC}" + if [[ -n ${NETRESTORE[$NETWORK]} ]]; then + NETRESTORE[$NETWORK]+=$'\n' + fi + NETRESTORE[$NETWORK]+=$ENTRY +} + +netrestore_connect(){ + local NETWORK=$1 + local CONTAINER=$2 + local MY_TT=$3 + local MY_MAC=$4 + local MY_IP= + local MY_IPV4= + local MY_IPV6= + local IP= + local IPAM_JSON= + local ENDPOINT_JSON= + local CONNECT_JSON= + local CODE= + local BODY= + local ENDPOINT_ID= + local ENDPOINT_MAC= + local OUT= + + container_exist "$CONTAINER" || return 0 + docker network inspect "$NETWORK" >/dev/null 2>&1 || return 0 + + if [[ -n ${REBUILD_CONTAINERS[$CONTAINER]} && ${PRIMARY_NETWORK[$CONTAINER]} == $NETWORK ]]; then + log "rebuild container $CONTAINER" + if OUT=$(/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container "$CONTAINER" 2>&1); then + unset REBUILD_CONTAINERS[$CONTAINER] + return 0 + fi + log "failed to rebuild container $CONTAINER: $OUT" + return 1 + fi + + for IP in ${MY_TT//;/ }; do + [[ -n $IP ]] || continue + if [[ $IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + MY_IPV4=$IP + MY_IP="$MY_IP --ip $IP" + elif [[ $IP =~ : ]]; then + MY_IPV6=$IP + MY_IP="$MY_IP --ip6 $IP" + else + log "skipping invalid stored IP for $CONTAINER on network $NETWORK: $IP" + fi + done + + ENDPOINT_ID=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.EndpointID}}{{end}}" "$CONTAINER" 2>/dev/null) + if [[ -n $ENDPOINT_ID ]]; then + [[ -n $MY_MAC ]] || return 0 + ENDPOINT_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.MacAddress}}{{end}}" "$CONTAINER" 2>/dev/null) + [[ ${ENDPOINT_MAC,,} == ${MY_MAC,,} ]] && return 0 + log "reconnecting $CONTAINER to network $NETWORK to restore MAC $MY_MAC" + if ! OUT=$(docker network disconnect -f "$NETWORK" "$CONTAINER" 2>&1); then + log "failed to disconnect $CONTAINER from network $NETWORK: $OUT" + return 1 + fi + fi + + if [[ -n $MY_MAC ]]; then + [[ -n $MY_IPV4 ]] && IPAM_JSON="\"IPv4Address\":\"$MY_IPV4\"" + [[ -n $MY_IPV6 ]] && IPAM_JSON="${IPAM_JSON:+$IPAM_JSON,}\"IPv6Address\":\"$MY_IPV6\"" + ENDPOINT_JSON="\"MacAddress\":\"$MY_MAC\"" + [[ -n $IPAM_JSON ]] && ENDPOINT_JSON="\"IPAMConfig\":{$IPAM_JSON},$ENDPOINT_JSON" + CONNECT_JSON="{\"Container\":\"$CONTAINER\",\"EndpointConfig\":{$ENDPOINT_JSON}}" + OUT=$(curl --unix-socket /var/run/docker.sock -sS -w $'\n%{http_code}' -X POST -H "Content-Type: application/json" --data "$CONNECT_JSON" "http://localhost/networks/$NETWORK/connect" 2>&1) + CODE=${OUT##*$'\n'} + BODY=${OUT%$'\n'$CODE} + if [[ $CODE != 2* ]]; then + log "failed to connect $CONTAINER to network $NETWORK: $BODY" + return 1 + fi + return 0 + fi + + log "connecting $CONTAINER to network $NETWORK" + if ! OUT=$(docker network connect $MY_IP $NETWORK $CONTAINER 2>&1); then + log "failed to connect $CONTAINER to network $NETWORK: $OUT" + return 1 + fi +} + container_add_route(){ local CT=($(docker inspect --format='{{.State.Pid}} {{.NetworkSettings.Networks}}' $1)) local PID=${CT[0]} @@ -267,7 +403,7 @@ docker_network_start(){ done <<< $(ls --indicator-style=none $SYSTEM | grep -P '^(bond|eth|wlan)[0-9]+') if ! docker_running; then return 1; fi # get container settings for custom networks to reconnect later - declare -A NETRESTORE CTRESTORE + declare -A NETRESTORE PRIMARY_NETWORK REBUILD_CONTAINERS USED_SUBNETS4 USED_SUBNETS6 RESTORED_NETWORKS 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) @@ -279,33 +415,41 @@ docker_network_start(){ [[ ${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 + if [[ $X == 0 ]] && ! carrier $NIC 1; then + continue + fi [[ $X == 0 && $NIC != wlan0 ]] && MAIN=$NIC [[ $NIC == wlan0 && -n $MAIN ]] && continue if [[ -n $REF && $REF != $NIC ]]; then sed -ri "s/(br|bond|eth|wlan)$X(\.[0-9]+)?<\/Network>/$NIC\2<\/Network>/" $XMLFILE - # flag container for later rebuild REBUILD=1 fi done - MY_NETWORK= MY_IP= + MY_NETWORK= MY_IP= MY_MAC= XML_MAC= TEMPLATE_MAC= CUSTOM_PRIMARY= while read_dom; do [[ $ENTITY == Network ]] && MY_NETWORK=$CONTENT [[ $ENTITY == MyIP ]] && MY_IP=${CONTENT// /,} && MY_IP=$(echo "$MY_IP" | tr -s "," ";") + [[ $ENTITY == MyMAC ]] && XML_MAC=${CONTENT// /} done <$XMLFILE # only restore valid networks if [[ -n $MY_NETWORK ]]; then - NETRESTORE[$MY_NETWORK]="$CONTAINER,$MY_IP ${NETRESTORE[$MY_NETWORK]}" - # save container name for later rebuild - CTRESTORE[$MY_NETWORK]=$REBUILD + [[ $MY_NETWORK =~ ^(br|bond|eth|wlan)[0-9]+(\.[0-9]+)?$ ]] && CUSTOM_PRIMARY=1 + TEMPLATE_MAC=$(sed -nE 's@.*.*--mac-address(=|[[:space:]]+)([^ <]+).*@\2@p' "$XMLFILE" | head -n1) + if [[ -n $XML_MAC ]]; then + MY_MAC=$XML_MAC + else + MY_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$MY_NETWORK\"}}{{.MacAddress}}{{end}}" $CONTAINER 2>/dev/null) + [[ -n $MY_MAC ]] || MY_MAC=$TEMPLATE_MAC + fi + netrestore_add "$MY_NETWORK" "$CONTAINER" "$MY_IP" "$MY_MAC" + PRIMARY_NETWORK[$CONTAINER]=$MY_NETWORK + [[ -n $REBUILD || (-z $XML_MAC && -n $TEMPLATE_MAC && -n $CUSTOM_PRIMARY) ]] && REBUILD_CONTAINERS[$CONTAINER]=1 fi fi # restore user defined networks - USER_NETWORKS=$(docker inspect --format='{{range $key,$value:=.NetworkSettings.Networks}}{{$key}};{{if $value.IPAMConfig}}{{if $value.IPAMConfig.IPv4Address}}{{$value.IPAMConfig.IPv4Address}}{{end}}{{if $value.IPAMConfig.IPv6Address}},{{$value.IPAMConfig.IPv6Address}}{{end}}{{end}} {{end}}' $CONTAINER) - for ROW in $USER_NETWORKS; do - ROW=(${ROW/;/ }) - USER_NETWORK=${ROW[0]} - USER_IP=${ROW[1]/,/;} + USER_NETWORKS=$(docker inspect --format='{{range $key,$value:=.NetworkSettings.Networks}}{{printf "%s;%s;" $key $value.MacAddress}}{{if $value.IPAMConfig}}{{if $value.IPAMConfig.IPv4Address}}{{$value.IPAMConfig.IPv4Address}}{{end}}{{if $value.IPAMConfig.IPv6Address}},{{$value.IPAMConfig.IPv6Address}}{{end}}{{end}}{{println}}{{end}}' $CONTAINER) + while IFS=';' read -r USER_NETWORK USER_MAC USER_IP; do + USER_IP=${USER_IP//,/;} if [[ -n $USER_NETWORK && $USER_NETWORK != $MY_NETWORK ]]; then LABEL=${USER_NETWORK//[0-9.]/} IF_NO_PARTS=${USER_NETWORK#"$LABEL"} @@ -316,9 +460,9 @@ docker_network_start(){ USER_NETWORK=${USER_NETWORK/$LABEL/${PORT:0:-1}} fi log "container $CONTAINER has an additional network that will be restored: $USER_NETWORK" - NETRESTORE[$USER_NETWORK]="$CONTAINER,$USER_IP ${NETRESTORE[$USER_NETWORK]}" + netrestore_add "$USER_NETWORK" "$CONTAINER" "$USER_IP" "$USER_MAC" fi - done + done <<< "$USER_NETWORKS" done # detach custom networks for NIC in $NICS; do @@ -354,6 +498,9 @@ docker_network_start(){ if [[ -n $IPV4 ]]; then SUBNET=$(ip -4 route show dev $NETWORK | sort | awk -v ORS=" " '$1 !~ /^default/ {print $1}' | sed 's/ $//') GATEWAY=$(ip -4 route show to default dev $NETWORK | awk '{print $3;exit}') + [[ -n $GATEWAY ]] || GATEWAY=$(configured_gateway "$NETWORK" GATEWAY) + [[ -n $GATEWAY ]] || { DEVICE=${NETWORK/./_}; DEVICE=${DEVICE^^}; DGW=DOCKER_GATEWAY_$DEVICE; GATEWAY=${!DGW}; } + [[ -n $GATEWAY ]] || GATEWAY=$(/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/derive_gateway "$SUBNET") SERVER=${IPV4%/*} DHCP=${NETWORK/./_} DHCP=DOCKER_DHCP_${DHCP^^} @@ -365,6 +512,8 @@ docker_network_start(){ if [[ -n $IPV6 ]]; then SUBNET6=$(ip -6 route show dev $NETWORK | sort | awk -v ORS=" " '$1 !~ /^(default|fe80)/ {print $1}' | sed 's/ $//') GATEWAY6=$(ip -6 route show to default dev $NETWORK | awk '{print $3;exit}') + [[ -n $GATEWAY6 ]] || GATEWAY6=$(configured_gateway "$NETWORK" GATEWAY6) + [[ -n $GATEWAY6 ]] || { DEVICE=${NETWORK/./_}; DEVICE=${DEVICE^^}; DGW=DOCKER_GATEWAY6_$DEVICE; GATEWAY6=${!DGW}; } fi else # add user defined networks @@ -384,6 +533,24 @@ docker_network_start(){ GATEWAY6=DOCKER_GATEWAY6_$DEVICE GATEWAY6=${!GATEWAY6} fi + SKIP_NETWORK= + for CANDIDATE in $SUBNET; do + if [[ -n ${USED_SUBNETS4[$CANDIDATE]} ]]; then + log "skipping network $NETWORK: IPv4 subnet $CANDIDATE is already used by ${USED_SUBNETS4[$CANDIDATE]}" + SKIP_NETWORK=1 + break + fi + done + if [[ -z $SKIP_NETWORK ]]; then + for CANDIDATE in $SUBNET6; do + if [[ -n ${USED_SUBNETS6[$CANDIDATE]} ]]; then + log "skipping network $NETWORK: IPv6 subnet $CANDIDATE is already used by ${USED_SUBNETS6[$CANDIDATE]}" + SKIP_NETWORK=1 + break + fi + done + fi + [[ -n $SKIP_NETWORK ]] && continue # set parameters for custom network creation [[ -n $SUBNET ]] && SET4=1 || SET4=0 [[ -n $SUBNET6 ]] && SET6=1 || SET6=0 @@ -415,22 +582,18 @@ docker_network_start(){ log "Processing... $NETWORK" docker network rm $NETWORK &>/dev/null docker network create -d $ATTACH $SUBNET $GATEWAY $SERVER $RANGE $SUBNET6 $GATEWAY6 -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 - CONTAINER=${CONNECT%,*} - MY_TT=${CONNECT#*,} - MY_IP= - for IP in ${MY_TT//;/ }; do - [[ $IP =~ ':' ]] && MY_IP="$MY_IP --ip6 $IP" || MY_IP="$MY_IP --ip $IP" - done - log "connecting $CONTAINER to network $NETWORK" - docker network connect $MY_IP $NETWORK $CONTAINER >/dev/null - if [[ -n ${CTRESTORE[$NETWORK]} ]]; then - # rebuild the container to use changed network - log "rebuild container $CONTAINER" - /usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container $CONTAINER - fi + for CANDIDATE in ${SUBNET//--subnet=/ }; do + [[ -n $CANDIDATE ]] && USED_SUBNETS4[$CANDIDATE]=$NETWORK + done + for CANDIDATE in ${SUBNET6//--ipv6 / }; do + [[ $CANDIDATE == --subnet=* ]] && USED_SUBNETS6[${CANDIDATE#--subnet=}]=$NETWORK done + # connect containers to this new network + while IFS='|' read -r CONTAINER MY_TT MY_MAC; do + [[ -n $CONTAINER ]] || continue + netrestore_connect "$NETWORK" "$CONTAINER" "$MY_TT" "$MY_MAC" + done <<< "${NETRESTORE[$NETWORK]}" + RESTORED_NETWORKS[$NETWORK]=1 # hack to let containers talk to host if [[ $TYPE == br ]]; then SHIM=shim-$NETWORK @@ -484,6 +647,13 @@ docker_network_start(){ fi fi done + for NETWORK in "${!NETRESTORE[@]}"; do + [[ -n ${RESTORED_NETWORKS[$NETWORK]} ]] && continue + while IFS='|' read -r CONTAINER MY_TT MY_MAC; do + [[ -n $CONTAINER ]] || continue + netrestore_connect "$NETWORK" "$CONTAINER" "$MY_TT" "$MY_MAC" + done <<< "${NETRESTORE[$NETWORK]}" + done # # create IPv6 forward accept rule # if [[ $IPV6_FORWARD == accept ]]; then # ip6tables -P FORWARD ACCEPT diff --git a/etc/rc.d/rc.library.source b/etc/rc.d/rc.library.source index fb5f5f9d03..29b19049ec 100644 --- a/etc/rc.d/rc.library.source +++ b/etc/rc.d/rc.library.source @@ -57,7 +57,7 @@ good(){ show(){ case $# in 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' ;; + 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;s/ peer [0-9a-fA-F:.]+\/[0-9]+//g;s/^ +//g' ;; esac } diff --git a/etc/rc.d/rc.sshd b/etc/rc.d/rc.sshd index aa82e5620f..8ac53a2bc9 100755 --- a/etc/rc.d/rc.sshd +++ b/etc/rc.d/rc.sshd @@ -126,6 +126,9 @@ sshd_update(){ if sshd_running && check && [[ "$(this ListenAddress)" != "${BIND[@]}" ]]; then log "Updating $DAEMON..." sshd_reload + elif ! sshd_running && [[ $USE_SSH == yes ]]; then + log "Recovering $DAEMON..." + sshd_start fi } diff --git a/share/docker/tailscale_container_hook b/share/docker/tailscale_container_hook index 817e22eb10..4ed51963ed 100755 --- a/share/docker/tailscale_container_hook +++ b/share/docker/tailscale_container_hook @@ -298,6 +298,13 @@ while true; do sleep 2 done +# Clear persisted Serve/Funnel state before applying the current template mode. +# Without this, switching the template from Funnel/Serve to No leaves the old +# config active in the existing Tailscale state directory after restart. +echo "Resetting Tailscale Serve/Funnel configuration" +tailscale funnel reset >/dev/null 2>&1 || true +tailscale serve reset >/dev/null 2>&1 || true + if [ ! -z "${TAILSCALE_SERVE_PORT}" ] && [ "$(tailscale status --json | jq -r '.CurrentTailnet.MagicDNSEnabled')" != "false" ] && [ -z "$(tailscale status --json | jq -r '.Self.Capabilities[] | select(. == "https")')" ]; then echo "ERROR: Enable MagicDNS and HTTPS on your Tailscale account to use Tailscale Serve/Funnel." echo "See: https://tailscale.com/kb/1153/enabling-https"