From a4d49761033969cf2288b831bdcc5bbdefc2f420 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:54:10 +0100 Subject: [PATCH 01/11] Update to use docker network checking against newt networking --- docker/client.go | 99 +++++++++++++++++++++++++++++++++++++++++++----- main.go | 57 ++++++++++++++++++---------- 2 files changed, 128 insertions(+), 28 deletions(-) diff --git a/docker/client.go b/docker/client.go index 98936fe..762ce28 100644 --- a/docker/client.go +++ b/docker/client.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "os" + "strconv" "strings" "time" @@ -67,8 +69,44 @@ func CheckSocket(socketPath string) bool { return true } +// IsWithinNewtNetwork checks if a provided target is within the newt network +func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { + containers, err := ListContainers(socketPath, containerNameAsHostname) + if err != nil { + return false, fmt.Errorf("failed to list Docker containers: %s", err) + } + + // If we can find the passed hostname/ip in the networks or as the container name, it is valid and can add it + for _, c := range containers { + for _, network := range c.Networks { + //If the container name matches, check the ports being mapped too + if containerNameAsHostname { + if c.Name == targetAddress { + for _, port := range c.Ports { + if port.PublicPort == targetPort || port.PrivatePort == targetPort { + return true, nil + } + } + } + } else { + //If the ip address matches, check the ports being mapped too + if network.IPAddress == targetAddress { + for _, port := range c.Ports { + if port.PublicPort == targetPort || port.PrivatePort == targetPort { + return true, nil + } + } + } + } + } + } + + combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort) + return false, fmt.Errorf("target address not within newt network: %s", combinedTargetAddress) +} + // ListContainers lists all Docker containers with their network information -func ListContainers(socketPath string) ([]Container, error) { +func ListContainers(socketPath string, containerNameAsHostname bool) ([]Container, error) { // Use the provided socket path or default to standard location if socketPath == "" { socketPath = "/var/run/docker.sock" @@ -88,6 +126,12 @@ func ListContainers(socketPath string) ([]Container, error) { } defer cli.Close() + // Get the newt container + newtContainer, err := getNewtContainer(ctx, cli) + if err != nil { + return nil, fmt.Errorf("failed to list containers: %v", err) + } + // List containers containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) if err != nil { @@ -96,6 +140,12 @@ func ListContainers(socketPath string) ([]Container, error) { var dockerContainers []Container for _, c := range containers { + // Get container name (remove leading slash) + name := "" + if len(c.Names) > 0 { + name = strings.TrimPrefix(c.Names[0], "/") + } + // Convert ports var ports []Port for _, port := range c.Ports { @@ -112,29 +162,32 @@ func ListContainers(socketPath string) ([]Container, error) { ports = append(ports, dockerPort) } - // Get container name (remove leading slash) - name := "" - if len(c.Names) > 0 { - name = strings.TrimPrefix(c.Names[0], "/") - } - // Get network information by inspecting the container networks := make(map[string]Network) - // Inspect container to get detailed network information + // Inspect the container to get detailed network information containerInfo, err := cli.ContainerInspect(ctx, c.ID) if err != nil { logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err) // Continue without network info if inspection fails } else { + // Only containers within the newt network will be returned + isInNewtNetwork := false + // Extract network information from inspection if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil { for networkName, endpoint := range containerInfo.NetworkSettings.Networks { + // Determine if the current container is in the newt network + for _, newtNetwork := range newtContainer.NetworkSettings.Networks { + if !isInNewtNetwork { + isInNewtNetwork = endpoint.NetworkID == newtNetwork.NetworkID + } + } + dockerNetwork := Network{ NetworkID: endpoint.NetworkID, EndpointID: endpoint.EndpointID, Gateway: endpoint.Gateway, - IPAddress: endpoint.IPAddress, IPPrefixLen: endpoint.IPPrefixLen, IPv6Gateway: endpoint.IPv6Gateway, GlobalIPv6Address: endpoint.GlobalIPv6Address, @@ -143,9 +196,21 @@ func ListContainers(socketPath string) ([]Container, error) { Aliases: endpoint.Aliases, DNSNames: endpoint.DNSNames, } + + // Don't set the IP address if container name is used as hostname + if !containerNameAsHostname { + dockerNetwork.IPAddress = endpoint.IPAddress + } + networks[networkName] = dockerNetwork } } + + // Don't continue returning this container if not in the newt network(s) + if !isInNewtNetwork { + logger.Debug("container not found within the newt network, skipping: %s", name) + continue + } } dockerContainer := Container{ @@ -164,3 +229,19 @@ func ListContainers(socketPath string) ([]Container, error) { return dockerContainers, nil } + +func getNewtContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { + // Get newt hostname from the os + newtContainerName, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("failed to find newt hostname: %v", err) + } + + // Get newt container from the docker socket + newtContainer, err := dockerClient.ContainerInspect(dockerContext, newtContainerName) + if err != nil { + return nil, fmt.Errorf("failed to find newt container: %v", err) + } + + return &newtContainer, nil +} \ No newline at end of file diff --git a/main.go b/main.go index fdece97..7cfb002 100644 --- a/main.go +++ b/main.go @@ -341,18 +341,20 @@ func resolveDomain(domain string) (string, error) { } var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string - updownScript string - tlsPrivateKey string - dockerSocket string + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + updownScript string + tlsPrivateKey string + dockerSocket string + dockerContainerAsHostname string + dockerContainerAsHostnameBool bool ) func main() { @@ -366,6 +368,7 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + dockerContainerAsHostname = os.Getenv("DOCKER_CONTAINER_NAME_AS_HOSTNAME") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -394,6 +397,9 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + if dockerContainerAsHostname == "" { + flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name when hostname for networking (true or false)") + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -418,6 +424,13 @@ func main() { logger.Fatal("Failed to parse MTU: %v", err) } + // pase if to use hostname over ip address for network sent to pangolin + dockerContainerAsHostnameBool, err = strconv.ParseBool(dockerContainerAsHostname) + if err != nil { + logger.Info("Docker use container name cannot be parsed. Defaulting to 'false'") + dockerContainerAsHostnameBool = false + } + privateKey, err = wgtypes.GeneratePrivateKey() if err != nil { logger.Fatal("Failed to generate private key: %v", err) @@ -676,7 +689,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // List Docker containers - containers, err := docker.ListContainers(dockerSocket) + containers, err := docker.ListContainers(dockerSocket, dockerContainerAsHostnameBool) if err != nil { logger.Error("Failed to list Docker containers: %v", err) return @@ -760,12 +773,14 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } if action == "add" { - target := parts[1] + ":" + parts[2] + targetAddress := parts[1] + targetPort, _ := strconv.Atoi(parts[2]) + combinedAddress := targetAddress + ":" + parts[2] // Call updown script if provided - processedTarget := target + processedTarget := combinedAddress if updownScript != "" { - newTarget, err := executeUpdownScript(action, proto, target) + newTarget, err := executeUpdownScript(action, proto, combinedAddress) if err != nil { logger.Warn("Updown script error: %v", err) } else if newTarget != "" { @@ -783,8 +798,12 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - pm.AddTarget(proto, tunnelIP, port, processedTarget) - + isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) + if !isWithinNewtNetwork { + logger.Error("Not adding target: %v", err) + } else { + pm.AddTarget(proto, tunnelIP, port, processedTarget) + } } else if action == "remove" { logger.Info("Removing target with port %d", port) @@ -849,4 +868,4 @@ func executeUpdownScript(action, proto, target string) (string, error) { } return target, nil -} +} \ No newline at end of file From a52260b49d7b10a2656b992c224aed13ae15159e Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:06:29 +0100 Subject: [PATCH 02/11] Add an enforce network validation flag for docker to not break previous functionality --- docker/client.go | 7 +++-- main.go | 74 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/docker/client.go b/docker/client.go index 762ce28..aee7fdf 100644 --- a/docker/client.go +++ b/docker/client.go @@ -71,7 +71,8 @@ func CheckSocket(socketPath string) bool { // IsWithinNewtNetwork checks if a provided target is within the newt network func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { - containers, err := ListContainers(socketPath, containerNameAsHostname) + // Always enforce network validation + containers, err := ListContainers(socketPath, true, containerNameAsHostname) if err != nil { return false, fmt.Errorf("failed to list Docker containers: %s", err) } @@ -106,7 +107,7 @@ func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, target } // ListContainers lists all Docker containers with their network information -func ListContainers(socketPath string, containerNameAsHostname bool) ([]Container, error) { +func ListContainers(socketPath string, enforceNetworkValidation bool, containerNameAsHostname bool) ([]Container, error) { // Use the provided socket path or default to standard location if socketPath == "" { socketPath = "/var/run/docker.sock" @@ -207,7 +208,7 @@ func ListContainers(socketPath string, containerNameAsHostname bool) ([]Containe } // Don't continue returning this container if not in the newt network(s) - if !isInNewtNetwork { + if enforceNetworkValidation && !isInNewtNetwork { logger.Debug("container not found within the newt network, skipping: %s", name) continue } diff --git a/main.go b/main.go index 7cfb002..177f32e 100644 --- a/main.go +++ b/main.go @@ -341,20 +341,22 @@ func resolveDomain(domain string) (string, error) { } var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string - updownScript string - tlsPrivateKey string - dockerSocket string - dockerContainerAsHostname string - dockerContainerAsHostnameBool bool + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + updownScript string + tlsPrivateKey string + dockerSocket string + dockerContainerAsHostname string + dockerContainerAsHostnameBool bool + dockerEnforceNetworkValidation string + dockerEnforceNetworkValidationBool bool ) func main() { @@ -369,6 +371,7 @@ func main() { tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") dockerContainerAsHostname = os.Getenv("DOCKER_CONTAINER_NAME_AS_HOSTNAME") + dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -400,6 +403,9 @@ func main() { if dockerContainerAsHostname == "" { flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name when hostname for networking (true or false)") } + if dockerEnforceNetworkValidation == "" { + flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -431,6 +437,13 @@ func main() { dockerContainerAsHostnameBool = false } + // parse if we want to enforce container network validation + dockerEnforceNetworkValidationBool, err = strconv.ParseBool(dockerEnforceNetworkValidation) + if err != nil { + logger.Info("Docker enforce network validation cannot be parsed. Defaulting to 'false'") + dockerEnforceNetworkValidationBool = false + } + privateKey, err = wgtypes.GeneratePrivateKey() if err != nil { logger.Fatal("Failed to generate private key: %v", err) @@ -450,6 +463,25 @@ func main() { logger.Fatal("Failed to create client: %v", err) } + // output env var values if set + logger.Debug("Endpoint: %v", endpoint) + logger.Debug("Log Level: %v", logLevel) + logger.Debug("Docker Container Name as Hostname: %v", dockerContainerAsHostnameBool) + logger.Debug("Docker Network Validation Enabled: %v", dockerEnforceNetworkValidationBool) + logger.Debug("TLS Private Key Set: %v", tlsPrivateKey != "") + if dns != "" { + logger.Debug("Dns: %v", dns) + } + if dockerSocket != "" { + logger.Debug("Docker Socket: %v", dockerSocket) + } + if mtu != "" { + logger.Debug("MTU: %v", mtu) + } + if updownScript != "" { + logger.Debug("Up Down Script: %v", updownScript) + } + // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net @@ -689,7 +721,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // List Docker containers - containers, err := docker.ListContainers(dockerSocket, dockerContainerAsHostnameBool) + containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool, dockerContainerAsHostnameBool) if err != nil { logger.Error("Failed to list Docker containers: %v", err) return @@ -798,9 +830,15 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) - if !isWithinNewtNetwork { - logger.Error("Not adding target: %v", err) + if dockerEnforceNetworkValidationBool { + logger.Info("Enforcing docker network validation") + + isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) + if !isWithinNewtNetwork { + logger.Error("Not adding target: %v", err) + } else { + pm.AddTarget(proto, tunnelIP, port, processedTarget) + } } else { pm.AddTarget(proto, tunnelIP, port, processedTarget) } From e335bb8a1f3f9744f3856d828060ee073b48ee25 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:57:50 +0100 Subject: [PATCH 03/11] Rename added functions for docker client --- docker/client.go | 46 +++++++++++++++++++++++----------------------- main.go | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docker/client.go b/docker/client.go index aee7fdf..13eabd9 100644 --- a/docker/client.go +++ b/docker/client.go @@ -69,8 +69,8 @@ func CheckSocket(socketPath string) bool { return true } -// IsWithinNewtNetwork checks if a provided target is within the newt network -func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { +// IsWithinHostNetwork checks if a provided target is within the host container network +func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { // Always enforce network validation containers, err := ListContainers(socketPath, true, containerNameAsHostname) if err != nil { @@ -103,7 +103,7 @@ func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, target } combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort) - return false, fmt.Errorf("target address not within newt network: %s", combinedTargetAddress) + return false, fmt.Errorf("target address not within host container network: %s", combinedTargetAddress) } // ListContainers lists all Docker containers with their network information @@ -127,10 +127,10 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN } defer cli.Close() - // Get the newt container - newtContainer, err := getNewtContainer(ctx, cli) + // Get the host container + hostContainer, err := getHostContainer(ctx, cli) if err != nil { - return nil, fmt.Errorf("failed to list containers: %v", err) + return nil, fmt.Errorf("failed to get host container: %v", err) } // List containers @@ -172,16 +172,16 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err) // Continue without network info if inspection fails } else { - // Only containers within the newt network will be returned - isInNewtNetwork := false + // Only containers within the host container network will be returned + isInHostContainerNetwork := false // Extract network information from inspection if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil { for networkName, endpoint := range containerInfo.NetworkSettings.Networks { - // Determine if the current container is in the newt network - for _, newtNetwork := range newtContainer.NetworkSettings.Networks { - if !isInNewtNetwork { - isInNewtNetwork = endpoint.NetworkID == newtNetwork.NetworkID + // Determine if the current container is in the host container network + for _, hostContainerNetwork := range hostContainer.NetworkSettings.Networks { + if !isInHostContainerNetwork { + isInHostContainerNetwork = endpoint.NetworkID == hostContainerNetwork.NetworkID } } @@ -207,9 +207,9 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN } } - // Don't continue returning this container if not in the newt network(s) - if enforceNetworkValidation && !isInNewtNetwork { - logger.Debug("container not found within the newt network, skipping: %s", name) + // Don't continue returning this container if not in the host container network(s) + if enforceNetworkValidation && !isInHostContainerNetwork { + logger.Debug("container not found within the host container network, skipping: %s", name) continue } } @@ -231,18 +231,18 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN return dockerContainers, nil } -func getNewtContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { - // Get newt hostname from the os - newtContainerName, err := os.Hostname() +func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { + // Get hostname from the os + containerHostname, err := os.Hostname() if err != nil { - return nil, fmt.Errorf("failed to find newt hostname: %v", err) + return nil, fmt.Errorf("failed to find hostname: %v", err) } - // Get newt container from the docker socket - newtContainer, err := dockerClient.ContainerInspect(dockerContext, newtContainerName) + // Get host container from the docker socket + hostContainer, err := dockerClient.ContainerInspect(dockerContext, containerHostname) if err != nil { - return nil, fmt.Errorf("failed to find newt container: %v", err) + return nil, fmt.Errorf("failed to inspect host container: %v", err) } - return &newtContainer, nil + return &hostContainer, nil } \ No newline at end of file diff --git a/main.go b/main.go index 177f32e..0606dec 100644 --- a/main.go +++ b/main.go @@ -833,7 +833,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto if dockerEnforceNetworkValidationBool { logger.Info("Enforcing docker network validation") - isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) + isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) if !isWithinNewtNetwork { logger.Error("Not adding target: %v", err) } else { From cbbd5b0c76602e375b540a727e373a3265787f14 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:58:37 +0100 Subject: [PATCH 04/11] Add extra pre-condition check for enforcing docker network --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 0606dec..5236a1e 100644 --- a/main.go +++ b/main.go @@ -830,7 +830,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - if dockerEnforceNetworkValidationBool { + if dockerSocket != "" && dockerEnforceNetworkValidationBool { logger.Info("Enforcing docker network validation") isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) From 5476a69963904b3e51da105c2b8d21c902e21632 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:05:41 +0100 Subject: [PATCH 05/11] Log the container name and id --- docker/client.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/client.go b/docker/client.go index 13eabd9..a407c81 100644 --- a/docker/client.go +++ b/docker/client.go @@ -141,6 +141,9 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN var dockerContainers []Container for _, c := range containers { + // Short ID like docker ps + shortId := c.ID[:12] + // Get container name (remove leading slash) name := "" if len(c.Names) > 0 { @@ -169,7 +172,7 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Inspect the container to get detailed network information containerInfo, err := cli.ContainerInspect(ctx, c.ID) if err != nil { - logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err) + logger.Debug("Failed to inspect container %s (%s) for network info: %v", shortId, name, err) // Continue without network info if inspection fails } else { // Only containers within the host container network will be returned @@ -209,13 +212,13 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Don't continue returning this container if not in the host container network(s) if enforceNetworkValidation && !isInHostContainerNetwork { - logger.Debug("container not found within the host container network, skipping: %s", name) + logger.Debug("Container not found within the host container network, skipping: %s (%s)", shortId, name) continue } } dockerContainer := Container{ - ID: c.ID[:12], // Show short ID like docker ps + ID: shortId, Name: name, Image: c.Image, State: c.State, From e26552a5d7329695ffb7afb92a7c2409b696ded2 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:25:27 +0100 Subject: [PATCH 06/11] Small refinement to how the docker enforcement setting is applied --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 5236a1e..a548fe4 100644 --- a/main.go +++ b/main.go @@ -401,7 +401,7 @@ func main() { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } if dockerContainerAsHostname == "" { - flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name when hostname for networking (true or false)") + flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name as hostname for networking (true or false)") } if dockerEnforceNetworkValidation == "" { flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") @@ -830,7 +830,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - if dockerSocket != "" && dockerEnforceNetworkValidationBool { + if dockerEnforceNetworkValidationBool { logger.Info("Enforcing docker network validation") isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) From 5cb86f3e4738f5f467370418c7fb3cbba28cae3b Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:26:06 +0100 Subject: [PATCH 07/11] Update to readme with new configuration settings --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 9cd2768..0867309 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ When Newt receives WireGuard control messages, it will use the information encod - `updown` (optional): A script to be called when targets are added or removed. - `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls) - `docker-socket` (optional): Set the Docker socket to use the container discovery integration +- `docker-container-name-as-hostname` (optional): Use the docker container name as the hostname rather then the IP of the container +- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process - Example: @@ -86,6 +88,24 @@ You can specify the Docker socket path using the `--docker-socket` CLI argument If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. +### Docker Container Name as Hostname + +When run as a Docker container, Newt by default will send the IP Address of the container. This feature will make it so you will be able to use the internal Docker DNS resolution, to be able to use the name of the container over the IP address. + +**Configuration:** + +This feature is `false` by default. It can be enabled via setting the `--docker-container-name-as-hostname` CLI argument or by setting the `DOCKER_CONTAINER_NAME_AS_HOSTNAME` environment variable. + +### Docker Enforce Network Validation + +When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and therefore is reachable. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. + +**Configuration:** + +Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. + +If validation is enforced and the Docker socket is enforced and the Docker socket is not available or accessible, Newt will **not** add the target as it cannot be verified. + ### Updown You can pass in a updown script for Newt to call when it is adding or removing a target: From 6d9160ab5e98b4d82a82a47ecf48a35a23e3145e Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Sat, 14 Jun 2025 01:26:11 +0100 Subject: [PATCH 08/11] Simplified based on PR feedback and support checking use of "bridge" network --- README.md | 15 +++--- docker/client.go | 132 ++++++++++++++++++++++++++--------------------- main.go | 30 +++-------- 3 files changed, 89 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 0867309..5196ac6 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ When Newt receives WireGuard control messages, it will use the information encod - `updown` (optional): A script to be called when targets are added or removed. - `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls) - `docker-socket` (optional): Set the Docker socket to use the container discovery integration -- `docker-container-name-as-hostname` (optional): Use the docker container name as the hostname rather then the IP of the container - `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process - Example: @@ -88,18 +87,20 @@ You can specify the Docker socket path using the `--docker-socket` CLI argument If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. -### Docker Container Name as Hostname +#### Hostnames vs IPs -When run as a Docker container, Newt by default will send the IP Address of the container. This feature will make it so you will be able to use the internal Docker DNS resolution, to be able to use the name of the container over the IP address. - -**Configuration:** - -This feature is `false` by default. It can be enabled via setting the `--docker-container-name-as-hostname` CLI argument or by setting the `DOCKER_CONTAINER_NAME_AS_HOSTNAME` environment variable. +When the Docker Socket Integration is used, depending on the network which Newt is run with, will determine if the hostname (generally considered the container name) or the IP address of the container is sent to Pangolin. Here are some of the scenarios below to describe what to expect: +- **Running in Network Mode 'host'**: IP addresses will be used +- **Running in Network Mode 'bridge'**: IP addresses will be used +- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default so hostnames will be used +- **Running on docker-compose with defined network**: Will use hostnames ### Docker Enforce Network Validation When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and therefore is reachable. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. +It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the container information cannot be retrieved to be able to carry out required validation + **Configuration:** Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. diff --git a/docker/client.go b/docker/client.go index a407c81..9fedf52 100644 --- a/docker/client.go +++ b/docker/client.go @@ -10,6 +10,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/fosrl/newt/logger" ) @@ -70,18 +71,22 @@ func CheckSocket(socketPath string) bool { } // IsWithinHostNetwork checks if a provided target is within the host container network -func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { +func IsWithinHostNetwork(socketPath string, targetAddress string, targetPort int) (bool, error) { // Always enforce network validation - containers, err := ListContainers(socketPath, true, containerNameAsHostname) + containers, err := ListContainers(socketPath, true) if err != nil { - return false, fmt.Errorf("failed to list Docker containers: %s", err) + + return false, err } - // If we can find the passed hostname/ip in the networks or as the container name, it is valid and can add it + // Determine if given an IP address + var parsedTargetAddressIp = net.ParseIP(targetAddress) + + // If we can find the passed hostname/IP address in the networks or as the container name, it is valid and can add it for _, c := range containers { for _, network := range c.Networks { - //If the container name matches, check the ports being mapped too - if containerNameAsHostname { + // If the target address is not an IP address, use the container name + if parsedTargetAddressIp == nil { if c.Name == targetAddress { for _, port := range c.Ports { if port.PublicPort == targetPort || port.PrivatePort == targetPort { @@ -90,7 +95,7 @@ func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, target } } } else { - //If the ip address matches, check the ports being mapped too + //If the IP address matches, check the ports being mapped too if network.IPAddress == targetAddress { for _, port := range c.Ports { if port.PublicPort == targetPort || port.PrivatePort == targetPort { @@ -107,12 +112,19 @@ func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, target } // ListContainers lists all Docker containers with their network information -func ListContainers(socketPath string, enforceNetworkValidation bool, containerNameAsHostname bool) ([]Container, error) { +func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Container, error) { // Use the provided socket path or default to standard location if socketPath == "" { socketPath = "/var/run/docker.sock" } + // Used to filter down containers returned to Pangolin + containerFilters := filters.NewArgs() + + // Used to determine if we will send IP addresses or hostnames to Pangolin + useContainerIpAddresses := true + hostContainerId := "" + // Create a new Docker client ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -125,16 +137,34 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN if err != nil { return nil, fmt.Errorf("failed to create Docker client: %v", err) } + defer cli.Close() - // Get the host container hostContainer, err := getHostContainer(ctx, cli) - if err != nil { - return nil, fmt.Errorf("failed to get host container: %v", err) + if enforceNetworkValidation && err != nil { + return nil, fmt.Errorf("network validation enforced, cannot validate due to: %v", err) + } + + // We may not be able to get back host container in scenarios like running the container in network mode 'host' + if hostContainer != nil { + // We can use the host container to filter out the list of returned containers + hostContainerId = hostContainer.ID + + for hostContainerNetworkName := range hostContainer.NetworkSettings.Networks { + // If we're enforcing network validation, we'll filter on the host containers networks + if enforceNetworkValidation { + containerFilters.Add("network", hostContainerNetworkName) + } + + // If the container is on the docker bridge network, we will use IP addresses over hostnames + if useContainerIpAddresses && hostContainerNetworkName != "bridge" { + useContainerIpAddresses = false + } + } } // List containers - containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: containerFilters}) if err != nil { return nil, fmt.Errorf("failed to list containers: %v", err) } @@ -144,6 +174,11 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Short ID like docker ps shortId := c.ID[:12] + // Skip host container if set + if hostContainerId != "" && c.ID == hostContainerId { + continue + } + // Get container name (remove leading slash) name := "" if len(c.Names) > 0 { @@ -169,51 +204,28 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Get network information by inspecting the container networks := make(map[string]Network) - // Inspect the container to get detailed network information - containerInfo, err := cli.ContainerInspect(ctx, c.ID) - if err != nil { - logger.Debug("Failed to inspect container %s (%s) for network info: %v", shortId, name, err) - // Continue without network info if inspection fails - } else { - // Only containers within the host container network will be returned - isInHostContainerNetwork := false - - // Extract network information from inspection - if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil { - for networkName, endpoint := range containerInfo.NetworkSettings.Networks { - // Determine if the current container is in the host container network - for _, hostContainerNetwork := range hostContainer.NetworkSettings.Networks { - if !isInHostContainerNetwork { - isInHostContainerNetwork = endpoint.NetworkID == hostContainerNetwork.NetworkID - } - } - - dockerNetwork := Network{ - NetworkID: endpoint.NetworkID, - EndpointID: endpoint.EndpointID, - Gateway: endpoint.Gateway, - IPPrefixLen: endpoint.IPPrefixLen, - IPv6Gateway: endpoint.IPv6Gateway, - GlobalIPv6Address: endpoint.GlobalIPv6Address, - GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen, - MacAddress: endpoint.MacAddress, - Aliases: endpoint.Aliases, - DNSNames: endpoint.DNSNames, - } - - // Don't set the IP address if container name is used as hostname - if !containerNameAsHostname { - dockerNetwork.IPAddress = endpoint.IPAddress - } - - networks[networkName] = dockerNetwork + // Extract network information from inspection + if c.NetworkSettings != nil && c.NetworkSettings.Networks != nil { + for networkName, endpoint := range c.NetworkSettings.Networks { + dockerNetwork := Network{ + NetworkID: endpoint.NetworkID, + EndpointID: endpoint.EndpointID, + Gateway: endpoint.Gateway, + IPPrefixLen: endpoint.IPPrefixLen, + IPv6Gateway: endpoint.IPv6Gateway, + GlobalIPv6Address: endpoint.GlobalIPv6Address, + GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen, + MacAddress: endpoint.MacAddress, + Aliases: endpoint.Aliases, + DNSNames: endpoint.DNSNames, } - } - // Don't continue returning this container if not in the host container network(s) - if enforceNetworkValidation && !isInHostContainerNetwork { - logger.Debug("Container not found within the host container network, skipping: %s (%s)", shortId, name) - continue + // Use IPs over hostnames/containers as we're on the bridge network + if useContainerIpAddresses { + dockerNetwork.IPAddress = endpoint.IPAddress + } + + networks[networkName] = dockerNetwork } } @@ -228,23 +240,25 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN Created: c.Created, Networks: networks, } + dockerContainers = append(dockerContainers, dockerContainer) } return dockerContainers, nil } +// getHostContainer gets the current container for the current host if possible func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { // Get hostname from the os - containerHostname, err := os.Hostname() + hostContainerName, err := os.Hostname() if err != nil { - return nil, fmt.Errorf("failed to find hostname: %v", err) + return nil, fmt.Errorf("failed to find hostname for container") } // Get host container from the docker socket - hostContainer, err := dockerClient.ContainerInspect(dockerContext, containerHostname) + hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName) if err != nil { - return nil, fmt.Errorf("failed to inspect host container: %v", err) + return nil, fmt.Errorf("failed to find host container") } return &hostContainer, nil diff --git a/main.go b/main.go index a548fe4..ffa539c 100644 --- a/main.go +++ b/main.go @@ -353,8 +353,6 @@ var ( updownScript string tlsPrivateKey string dockerSocket string - dockerContainerAsHostname string - dockerContainerAsHostnameBool bool dockerEnforceNetworkValidation string dockerEnforceNetworkValidationBool bool ) @@ -370,7 +368,6 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") - dockerContainerAsHostname = os.Getenv("DOCKER_CONTAINER_NAME_AS_HOSTNAME") dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION") if endpoint == "" { @@ -400,9 +397,6 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } - if dockerContainerAsHostname == "" { - flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name as hostname for networking (true or false)") - } if dockerEnforceNetworkValidation == "" { flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") } @@ -412,7 +406,7 @@ func main() { flag.Parse() - newtVersion := "Newt version replaceme" + newtVersion := "Newt version JB wip" if *version { fmt.Println(newtVersion) os.Exit(0) @@ -430,13 +424,6 @@ func main() { logger.Fatal("Failed to parse MTU: %v", err) } - // pase if to use hostname over ip address for network sent to pangolin - dockerContainerAsHostnameBool, err = strconv.ParseBool(dockerContainerAsHostname) - if err != nil { - logger.Info("Docker use container name cannot be parsed. Defaulting to 'false'") - dockerContainerAsHostnameBool = false - } - // parse if we want to enforce container network validation dockerEnforceNetworkValidationBool, err = strconv.ParseBool(dockerEnforceNetworkValidation) if err != nil { @@ -466,7 +453,6 @@ func main() { // output env var values if set logger.Debug("Endpoint: %v", endpoint) logger.Debug("Log Level: %v", logLevel) - logger.Debug("Docker Container Name as Hostname: %v", dockerContainerAsHostnameBool) logger.Debug("Docker Network Validation Enabled: %v", dockerEnforceNetworkValidationBool) logger.Debug("TLS Private Key Set: %v", tlsPrivateKey != "") if dns != "" { @@ -721,7 +707,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // List Docker containers - containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool, dockerContainerAsHostnameBool) + containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool) if err != nil { logger.Error("Failed to list Docker containers: %v", err) return @@ -829,17 +815,17 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } } - // Add the new target + // If docker network validation is enabled if dockerEnforceNetworkValidationBool { - logger.Info("Enforcing docker network validation") - - isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) - if !isWithinNewtNetwork { - logger.Error("Not adding target: %v", err) + // If the target address is within the host container network, the target will be added + isWithinHostContainerNetwork, err := docker.IsWithinHostNetwork(dockerSocket, targetAddress, targetPort) + if !isWithinHostContainerNetwork { + logger.Warn("Not adding target address: %v", err) } else { pm.AddTarget(proto, tunnelIP, port, processedTarget) } } else { + // If we're not enforcing network validation, just proceed with adding the target pm.AddTarget(proto, tunnelIP, port, processedTarget) } } else if action == "remove" { From 58f78350724e67c19b58892092a750f9d461fedd Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:22:14 +0100 Subject: [PATCH 09/11] Revise README docs --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5196ac6..b8917b2 100644 --- a/README.md +++ b/README.md @@ -89,23 +89,23 @@ If the Docker socket is not available or accessible, Newt will gracefully disabl #### Hostnames vs IPs -When the Docker Socket Integration is used, depending on the network which Newt is run with, will determine if the hostname (generally considered the container name) or the IP address of the container is sent to Pangolin. Here are some of the scenarios below to describe what to expect: +When the Docker Socket Integration is used, depending on the network which Newt is run with, either the hostname (generally considered the container name) or the IP address of the container will be sent to Pangolin. Here are some of the scenarios where IPs or hostname of the container will be utilised: - **Running in Network Mode 'host'**: IP addresses will be used - **Running in Network Mode 'bridge'**: IP addresses will be used -- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default so hostnames will be used +- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, will use hostnames - **Running on docker-compose with defined network**: Will use hostnames ### Docker Enforce Network Validation -When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and therefore is reachable. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. +When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and only return containers directly accessible by Newt. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. -It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the container information cannot be retrieved to be able to carry out required validation +It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the specific host container information for Newt cannot be retrieved to be able to carry out network validation. **Configuration:** Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. -If validation is enforced and the Docker socket is enforced and the Docker socket is not available or accessible, Newt will **not** add the target as it cannot be verified. +If validation is enforced and the Docker socket is available, Newt will **not** add the target as it cannot be verified. A warning will be presented in the Newt logs. ### Updown From 48cb0bf5a74add58cc55e0742b21da8c8bc27bc7 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:22:56 +0100 Subject: [PATCH 10/11] Minor README update for consistentcy --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8917b2..c2e6a28 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ If the Docker socket is not available or accessible, Newt will gracefully disabl When the Docker Socket Integration is used, depending on the network which Newt is run with, either the hostname (generally considered the container name) or the IP address of the container will be sent to Pangolin. Here are some of the scenarios where IPs or hostname of the container will be utilised: - **Running in Network Mode 'host'**: IP addresses will be used - **Running in Network Mode 'bridge'**: IP addresses will be used -- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, will use hostnames -- **Running on docker-compose with defined network**: Will use hostnames +- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, hostnames will be used +- **Running on docker-compose with defined network**: Hostnames will be used ### Docker Enforce Network Validation From 7c971d278ce7c3fe0d96da3c2be9e992f4488e2a Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Mon, 16 Jun 2025 19:38:10 +0100 Subject: [PATCH 11/11] Revert the newt version placeholder text --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index ffa539c..6622999 100644 --- a/main.go +++ b/main.go @@ -406,7 +406,7 @@ func main() { flag.Parse() - newtVersion := "Newt version JB wip" + newtVersion := "Newt version replaceme" if *version { fmt.Println(newtVersion) os.Exit(0) @@ -892,4 +892,4 @@ func executeUpdownScript(action, proto, target string) (string, error) { } return target, nil -} \ No newline at end of file +}