diff --git a/emhttp/languages/en_US/helptext.txt b/emhttp/languages/en_US/helptext.txt index c1a7ba4e5..8321014c2 100644 --- a/emhttp/languages/en_US/helptext.txt +++ b/emhttp/languages/en_US/helptext.txt @@ -2314,6 +2314,136 @@ 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_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 + +:docker_tailscale_help: +Enable Tailscale to add this container as a machine on your Tailnet. +:end + +:docker_tailscale_hostname_help: +Provide the hostname for this container. It does not need to match the container name, but it must be unique on your Tailnet. Note that an HTTPS certificate will be generated for this hostname, which means it will be placed in a public ledger, so use a name that you don't mind being public. +For more information see enabling https. +:end + +:docker_tailscale_be_exitnode_help: +Enable this if other machines on your Tailnet should route their Internet traffic through this container, this is most useful for containers that connect to commercial VPN services. +Be sure to authorize this Exit Node in your Tailscale Machines Admin Panel. +For more details, see the Tailscale documentation on Exit Nodes. +:end + +:docker_tailscale_exitnode_ip_help: +Optionally route this container's outgoing Internet traffic through an Exit Node on your Tailnet. Choose the Exit Node or input its Tailscale IP address. +For more details, see Exit Nodes. +:end + +:docker_tailscale_lanaccess_help: +Only applies when this container is using an Exit Node. Enable this to allow the container to access the local network. + +WARNING: Even with this feature enabled, systems on your LAN may not be able to access the container unless they have Tailscale installed. +:end + +:docker_tailscale_userspace_networking_help: +When enabled, this container will operate in a restricted environment. Tailscale DNS will not work, and the container will not be able to initiate connections to other Tailscale machines. However, other machines on your Tailnet will still be able to communicate with this container. + +When disabled, this container will have full access to your Tailnet. Tailscale DNS will work, and the container can fully communicate with other machines on the Tailnet. +However, systems on your LAN may not be able to access the container unless they have Tailscale installed. +:end + +:docker_tailscale_ssh_help: +Tailscale SSH is similar to the Docker "Console" option in the Unraid webgui, except you connect with an SSH client and authenticate via Tailscale. +For more details, see the Tailscale SSH documentation.. +:end + +:docker_tailscale_serve_mode_help: +Enabling Serve will automatically reverse proxy the primary web service from this container and make it available on your Tailnet using https with a valid certificate! + +Note that when accessing the Tailscale WebUI url, no additional authentication layer is added beyond restricting it to your Tailnet - the container is still responsible for managing usernames/passwords that are allowed to access it. Depending on your configuration, direct access to the container may still be possible as well. + +For more details, see the Tailscale Serve documentation. + +If the documentation recommends additional settings for a more complex use case, enable "Tailscale Show Advanced Settings". Support for these advanced settings is not available beyond confirming the commands are passed to Tailscale correctly. + +Funnel is similar to Serve, except that the web service is made available on the open Internet. Use with care as the service will likely be attacked. As with Serve, the container itself is responsible for handling any authentication. + +We recommend reading the Tailscale Funnel documentation. before enabling this feature. + +Note: Enabling Serve or Funnel publishes the Tailscale hostname to a public ledger. +For more details, see the Tailscale Documentation: Enabling HTTPS. +:end + +:docker_tailscale_serve_port_help: +This field should specify the port for the primary web service this container offers. Note: it should specify the port in the container, not a port that was remapped on the host. + +The system attempted to determine the correct port automatically. If it used the wrong value then there is likely an issue with the "Web UI" field for this container, visible by switching from "Basic View" to "Advanced View" in the upper right corner of this page. + +In most cases this port is all you will need to specify in order to Serve the website in this container, although additional options are available below for more complex containers. + +This value is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- http://localhost:`**``**``
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_show_advanced_help: +Here there be dragons! +:end + +:docker_tailscale_serve_local_path_help: +When not specified, this value defaults to an empty string. It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- http://localhost:`**``**
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_protocol_help: +When not specified, this value defaults to "https". It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg --`**``**`= http://localhost:`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_protocol_port_help: +When not specified, this value defaults to "=443". It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg --`**``**` http://localhost:`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_path_help: +When not specified, this value defaults to an empty string. It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg --`**``** `http://localhost:`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_webui_help: +If Serve is enabled this will be an https url with a proper domain name that is accessible over your Tailnet, no port needed! + +If Funnel is enabled the same url will be available on the Internet. + +If they are disabled then the url will be generated from the container's main "Web UI" field, but modified to use the Tailscale IP. If the wrong port is specified here then switch from "Basic View" to "Advanced View" and review the "Web UI" field for this container. +:end + +:docker_tailscale_advertise_routes_help: +If desired, specify any routes that should be passed to the **`--advertise-routes=`** parameter when running **`tailscale up`**. +For more details see the Subnet routers documentation. +:end + +:docker_tailscale_daemon_extra_params_help: +Specify any extra parameters to pass when starting **`tailscaled`**. +For more details see the tailscaled documentation. +:end + +:docker_tailscale_extra_param_help: +Specify any extra parameters to pass when running **`tailscale up`**. +For more details see the Tailscale CLI documentation. +:end + +:docker_tailscale_statedir_help: +If state directory detection fails on startup, you can specify a persistent directory in the container to override automatic detection. +:end + +:docker_tailscale_troubleshooting_packages_help: +Enable this to install `ping`, `nslookup`, and `curl` into the container to help troubleshoot networking issues. Once the issues are resolved we recommend disabling this to reduce the size of the container. +:end + :docker_privileged_help: For containers that require the use of host-device access directly or need full exposure to host capabilities, this option will need to be selected. For more information, see this link: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities diff --git a/emhttp/plugins/dynamix.docker.manager/images/tailscale.png b/emhttp/plugins/dynamix.docker.manager/images/tailscale.png new file mode 100755 index 000000000..fd4a4fced Binary files /dev/null and b/emhttp/plugins/dynamix.docker.manager/images/tailscale.png differ diff --git a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php index 7a58f43e9..b2d8da1c3 100644 --- a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php +++ b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php @@ -141,11 +141,24 @@ if (isset($_POST['contName'])) { @unlink("$userTmplDir/my-$existing.xml"); } } + // Extract real Entrypoint and Cmd from container for Tailscale + if (isset($_POST['contTailscale']) && $_POST['contTailscale'] == 'on') { + // Create preliminary base container but don't run it + exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'"); + // Get Entrypoint and Cmd from docker inspect + $containerInfo = $DockerClient->getContainerDetails($Name); + $ts_env = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : ''; + $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : ''; + // Insert Entrypoint and Cmd to docker command + $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd); + // Remove preliminary container + exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'"); + } if ($startContainer) $cmd = str_replace('/docker create ', '/docker run -d ', $cmd); execCommand($cmd); if ($startContainer) addRoute($Name); // add route for remote WireGuard access - echo '

'; + echo '

'; goto END; } @@ -169,6 +182,7 @@ if (isset($_GET['updateContainer'])){ $xml = file_get_contents($tmpl); [$cmd, $Name, $Repository] = xmlToCommand($tmpl); $Registry = getXmlVal($xml, "Registry"); + $TS_Enabled = getXmlVal($xml, "TailscaleEnabled"); $oldImageID = $DockerClient->getImageID($Repository); // pull image if ($echo && !pullImage($Name, $Repository)) continue; @@ -184,6 +198,19 @@ if (isset($_GET['updateContainer'])){ } // force kill container if still running after time-out if (empty($_GET['communityApplications'])) removeContainer($Name, $echo); + // Extract real Entrypoint and Cmd from container for Tailscale + if ($TS_Enabled == 'true') { + // Create preliminary base container but don't run it + exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'"); + // Get Entrypoint and Cmd from docker inspect + $containerInfo = $DockerClient->getContainerDetails($Name); + $ts_env = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : ''; + $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : ''; + // Insert Entrypoint and Cmd to docker command + $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd); + // Remove preliminary container + exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'"); + } execCommand($cmd, $echo); if ($startContainer) addRoute($Name); // add route for remote WireGuard access $DockerClient->flushCaches(); @@ -272,6 +299,153 @@ $authoring = $authoringMode ? 'advanced' : 'noshow'; $disableEdit = $authoringMode ? 'false' : 'true'; $showAdditionalInfo = ''; $bgcolor = strstr('white,azure',$display['theme']) ? '#f2f2f2' : '#1c1c1c'; + +# Search for existing TAILSCALE_ entries in the Docker template +$TS_existing_vars = false; +foreach ($xml["Config"] as $config) { + if (isset($config["Target"]) && strpos($config["Target"], "TAILSCALE_") === 0) { + $TS_existing_vars = true; + break; + } +} + +# Look for Exit Nodes if Tailscale plugin is installed +$ts_exit_nodes = []; +$ts_en_check = false; +if (file_exists('/usr/local/sbin/tailscale') && exec('pgrep --ns $$ -f "/usr/local/sbin/tailscaled"')) { + exec('tailscale exit-node list', $ts_exit_node_list, $retval); + if ($retval === 0) { + foreach ($ts_exit_node_list as $line) { + if (!empty(trim($line))) { + if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) { + $parts = preg_split('/\s+/', $matches[2]); + $ts_exit_nodes[] = [ + 'ip' => $matches[1], + 'hostname' => $parts[0], + 'country' => $parts[1], + 'city' => $parts[2], + 'status' => $parts[3] + ]; + $ts_en_check = true; + } + } + } + } +} + +# Try to detect port from WebUI and set webui_url +$TSwebuiport = ''; +$webui_url = ''; +if (empty($xml['TailscalePort'])) { + if (!empty($xml['WebUI'])) { + $webui_url = parse_url($xml['WebUI']); + preg_match('/:(\d+)\]/', $webui_url['host'], $matches); + $TSwebuiport = $matches[1]; + } +} + +$TS_raw = []; +$TS_container_raw = []; +$TS_HostNameWarning = ""; +$TS_HTTPSDisabledWarning = ""; +$TS_ExitNodeNeedsApproval = false; +$TS_MachinesLink = "https://login.tailscale.com/admin/machines/"; +$TS_DirectMachineLink = $TS_MachinesLink; +$TS_HostNameActual = ""; +$TS_not_approved = ""; +// Get Tailscale information and create arrays/variables +exec("docker exec -i ".$xml['Name']." /bin/sh -c \"tailscale status --peers=false --json\"", $TS_raw); +$TS_no_peers = json_decode(implode('', $TS_raw),true); +$TS_container = json_decode(implode('', $TS_raw),true); +$TS_container = $TS_container['Self']; +if (!empty($TS_no_peers) && !empty($TS_container)) { + // define the direct link to this machine on the Tailscale website + if (!empty($TS_container['TailscaleIPs']) && !empty($TS_container['TailscaleIPs'][0])) { + $TS_DirectMachineLink = $TS_MachinesLink.$TS_container['TailscaleIPs'][0]; + } + // warn if MagicDNS or HTTPS is disabled + if (empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || !$TS_no_peers['CurrentTailnet']['MagicDNSEnabled'] || empty($TS_no_peers['CertDomains']) || empty($TS_no_peers['CertDomains'][0])) { + $TS_HTTPSDisabledWarning = "Enable HTTPS on your Tailscale account to use Tailscale Serve/Funnel."; + } + // In $TS_container, 'HostName' is what the user requested, need to parse 'DNSName' to find the actual HostName in use + $TS_DNSName = _var($TS_container,'DNSName',''); + $TS_HostNameActual = substr($TS_DNSName, 0, strpos($TS_DNSName, '.')); + // compare the actual HostName in use to the one in the XML file + if (strcasecmp($TS_HostNameActual, _var($xml, 'TailscaleHostname')) !== 0 && !empty($TS_DNSName)) { + // they are different, show a warning + $TS_HostNameWarning = "Warning: the actual Tailscale hostname is '".$TS_HostNameActual."'"; + } + // If this is an Exit Node, show warning if it still needs approval + if (_var($xml,'TailscaleIsExitNode') == 'true' && _var($TS_container, 'ExitNodeOption') === false) { + $TS_ExitNodeNeedsApproval = true; + } + //Check for key expiry + if(!empty($TS_container['KeyExpiry'])) { + $TS_expiry = new DateTime($TS_container['KeyExpiry']); + $current_Date = new DateTime(); + $TS_expiry_diff = $current_Date->diff($TS_expiry); + } + // Check for non approved routes + if(!empty($xml['TailscaleRoutes'])) { + $TS_advertise_routes = str_replace(' ', '', $xml['TailscaleRoutes']); + if (empty($TS_container['PrimaryRoutes'])) { + $TS_container['PrimaryRoutes'] = []; + } + $routes = explode(',', $TS_advertise_routes); + foreach ($routes as $route) { + if (!in_array($route, $TS_container['PrimaryRoutes'])) { + $TS_not_approved .= " " . $route; + } + } + } + // Check for exit nodes if ts_en_check was not already done + if (!$ts_en_check) { + exec("docker exec -i ".$xml['Name']." /bin/sh -c \"tailscale exit-node list\"", $ts_exit_node_list, $retval); + if ($retval === 0) { + foreach ($ts_exit_node_list as $line) { + if (!empty(trim($line))) { + if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) { + $parts = preg_split('/\s+/', $matches[2]); + $ts_exit_nodes[] = [ + 'ip' => $matches[1], + 'hostname' => $parts[0], + 'country' => $parts[1], + 'city' => $parts[2], + 'status' => $parts[3] + ]; + } + } + } + } + } + // Construct WebUI URL on container template page + // Check if webui_url, Tailscale WebUI and MagicDNS are not empty and make sure that MagicDNS is enabled + if (!empty($webui_url) && !empty($xml['TailscaleWebUI']) && (!empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || $TS_no_peers['CurrentTailnet']['MagicDNSEnabled'])) { + // Check if serve or funnel are enabled by checking for [hostname] and replace string with TS_DNSName + if (!empty($xml['TailscaleWebUI']) && strpos($xml['TailscaleWebUI'], '[hostname]') !== false && isset($TS_DNSName)) { + $TS_webui_url = str_replace("[hostname][magicdns]", rtrim($TS_DNSName, '.'), $xml['TailscaleWebUI']); + // Check if serve is disabled, construct url with port, path and query if present and replace [noserve] with url + } elseif (strpos($xml['TailscaleWebUI'], '[noserve]') !== false && isset($TS_container['TailscaleIPs'])) { + $ipv4 = ''; + foreach ($TS_container['TailscaleIPs'] as $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $ipv4 = $ip; + break; + } + } + if (!empty($ipv4)) { + $webui_url = isset($xml['WebUI']) ? parse_url($xml['WebUI']) : ''; + $webui_port = (preg_match('/\[PORT:(\d+)\]/', $xml['WebUI'], $matches)) ? ':' . $matches[1] : ''; + $webui_path = $webui_url['path'] ?? ''; + $webui_query = isset($webui_url['query']) ? '?' . $webui_url['query'] : ''; + $TS_webui_url = 'http://' . $ipv4 . $webui_port . $webui_path . $webui_query; + } + // Check if TailscaleWebUI in the xml is custom and display instead + } elseif (strpos($xml['TailscaleWebUI'], '[hostname]') === false && strpos($xml['TailscaleWebUI'], '[noserve]') === false) { + $TS_webui_url = $xml['TailscaleWebUI']; + } + } +} ?> "> "> @@ -675,6 +849,16 @@ $(function() { }); }); + + +
@@ -715,7 +899,7 @@ _(Template)_:
_(Name)_: -: +: :docker_client_name_help: @@ -903,9 +1087,263 @@ _(Container Network)_: } } ?> -:docker_container_network_help: + +:docker_container_network_help: +
+ +

+ + +
+_(WARNING)_: +: _(Existing TAILSCALE variables found, please remove any existing modifications in the Template for Tailscale before using this function!)_ +
+ + + +
+_(First deployment)_: +:

_(After deploying the container, open the log and follow the link to register the container to your Tailnet!)_

+
+ + +
+_(Recommendation)_: +:

_(For the best experience with Tailscale, install "Tailscale (Plugin)" from)_ Community Applications.

+
+ + + + +
+_(Use Tailscale)_: +: onchange="showTailscale(this)"> + +:docker_tailscale_help: + +
+ +
+_(NOTE)_: +: _(This option will install Tailscale and dependencies into the container.)_ +
+ + +
+Warning: +: Exit Node not yet approved. Navigate to the Tailscale website and approve it. +
+ + + +
+_(Warning)_: +invert):?> +: Tailscale Key expired! Renew/Disable key expiry for ''. + +: Tailscale Key will expire in days?> days! Disable Key Expiry for ''. + + +
+ + + +
+_(Warning)_: +: The following route(s) are not approved: +
+ + +
+_(Tailscale Hostname)_: +: placeholder="_(Hostname for the container)_"> + +:docker_tailscale_hostname_help: + +
+ +
+_(Be a Tailscale Exit Node)_: +: + + +:docker_tailscale_be_exitnode_help: + +
+ +
+_(Use a Tailscale Exit Node)_: + +: placeholder="_(IP/Hostname from Exit Node)_" onchange="processExitNodeoptions(this)"> + +: + + + + +:docker_tailscale_exitnode_ip_help: + +
+ +
+_(Tailscale Allow LAN Access)_: +: + + +:docker_tailscale_lanaccess_help: + +
+ +
+_(Tailscale Userspace Networking)_: +: + + +:docker_tailscale_userspace_networking_help: + +
+ +
+_(Enable Tailscale SSH)_: +: + +:docker_tailscale_ssh_help: + +
+ +
+_(Tailscale Serve)_: +: +' . $TS_webui_url . ''; ?> + +:docker_tailscale_serve_mode_help: + +
+ +
+_(Tailscale Serve Port)_: +: + +:docker_tailscale_serve_port_help: + +
+ +
+_(Tailscale Show Advanced Settings)_: +: + +:docker_tailscale_show_advanced_help: + +
+ +
+_(Tailscale Serve Local Path)_: +: placeholder="_(Leave empty if unsure)_"> + +:docker_tailscale_serve_local_path_help: + +
+ +
+_(Tailscale Serve Protocol)_: +: placeholder="_(Leave empty if unsure, defaults to https)_"> + +:docker_tailscale_serve_protocol_help: + +
+ +
+_(Tailscale Serve Protocol Port)_: +: placeholder="_(Leave empty if unsure, defaults to =443)_"> + +:docker_tailscale_serve_protocol_port_help: + +
+ +
+_(Tailscale Serve Path)_: +: placeholder="_(Leave empty if unsure)_"> + +:docker_tailscale_serve_path_help: + +
+ +
+_(Tailscale WebUI)_: +: +> + +:docker_tailscale_serve_webui_help: + +
+ +
+_(Tailscale Advertise Routes)_: +: placeholder="_(Leave empty if unsure)_"> + +:docker_tailscale_advertise_routes_help: + +
+ +
+_(Tailscale Daemon Parameters)_: +: placeholder="_(Leave empty if unsure)_"> + +:docker_tailscale_daemon_extra_params_help: + +
+ +
+_(Tailscale Extra Parameters)_: +: placeholder="_(Leave empty if unsure)_"> + +:docker_tailscale_extra_param_help: + +
+ +
+_(Tailscale State Directory)_: +: placeholder="_(Leave empty if unsure)_"> + +:docker_tailscale_statedir_help: + +
+ +
+_(Tailscale Install Troubleshooting Packages)_: +: > + +:docker_tailscale_troubleshooting_packages_help: + +
+ +
+
+
+ _(Console shell command)_: :