Simplified based on PR feedback and support checking use of "bridge" network

This commit is contained in:
Jonny Booker
2025-06-14 01:26:11 +01:00
parent 5cb86f3e47
commit 6d9160ab5e
3 changed files with 89 additions and 88 deletions

View File

@@ -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.

View File

@@ -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

30
main.go
View File

@@ -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" {