Skip to content

Commit c593e2a

Browse files
Merge pull request #72 from fosrl/dev
Dev
2 parents 5aa00a6 + c3483de commit c593e2a

File tree

19 files changed

+3054
-618
lines changed

19 files changed

+3054
-618
lines changed

.github/workflows/cicd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
run: |
4040
TAG=${{ env.TAG }}
4141
if [ -f main.go ]; then
42-
sed -i 's/Newt version replaceme/Newt version '"$TAG"'/' main.go
42+
sed -i 's/version_replaceme/'"$TAG"'/' main.go
4343
echo "Updated main.go with version $TAG"
4444
else
4545
echo "main.go not found"

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
newt
22
.DS_Store
33
bin/
4+
nohup.out
45
.idea
56
*.iml
6-
certs/
7+
certs/
8+
newt_arm64

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ When Newt receives WireGuard control messages, it will use the information encod
3333
- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket.
3434
- `id`: Newt ID generated by Pangolin to identify the client.
3535
- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands.
36+
- `mtu`: MTU for the internal WG interface. Default: 1280
3637
- `dns`: DNS server to use to resolve the endpoint
3738
- `log-level` (optional): The log level to use. Default: INFO
3839
- `updown` (optional): A script to be called when targets are added or removed.
3940
- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls)
4041
- `docker-socket` (optional): Set the Docker socket to use the container discovery integration
42+
- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process
4143

4244
- Example:
4345

@@ -99,6 +101,26 @@ services:
99101
- DOCKER_SOCKET=/var/run/docker.sock
100102
```
101103

104+
#### Hostnames vs IPs
105+
106+
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:
107+
- **Running in Network Mode 'host'**: IP addresses will be used
108+
- **Running in Network Mode 'bridge'**: IP addresses will be used
109+
- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, hostnames will be used
110+
- **Running on docker-compose with defined network**: Hostnames will be used
111+
112+
### Docker Enforce Network Validation
113+
114+
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.
115+
116+
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.
117+
118+
**Configuration:**
119+
120+
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.
121+
122+
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.
123+
102124
### Updown
103125

104126
You can pass in a updown script for Newt to call when it is adding or removing a target:

docker/client.go

Lines changed: 130 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"context"
55
"fmt"
66
"net"
7+
"os"
8+
"strconv"
79
"strings"
810
"time"
911

1012
"github.com/docker/docker/api/types/container"
13+
"github.com/docker/docker/api/types/filters"
1114
"github.com/docker/docker/client"
1215
"github.com/fosrl/newt/logger"
1316
)
@@ -67,13 +70,60 @@ func CheckSocket(socketPath string) bool {
6770
return true
6871
}
6972

73+
// IsWithinHostNetwork checks if a provided target is within the host container network
74+
func IsWithinHostNetwork(socketPath string, targetAddress string, targetPort int) (bool, error) {
75+
// Always enforce network validation
76+
containers, err := ListContainers(socketPath, true)
77+
if err != nil {
78+
return false, err
79+
}
80+
81+
// Determine if given an IP address
82+
var parsedTargetAddressIp = net.ParseIP(targetAddress)
83+
84+
// If we can find the passed hostname/IP address in the networks or as the container name, it is valid and can add it
85+
for _, c := range containers {
86+
for _, network := range c.Networks {
87+
// If the target address is not an IP address, use the container name
88+
if parsedTargetAddressIp == nil {
89+
if c.Name == targetAddress {
90+
for _, port := range c.Ports {
91+
if port.PublicPort == targetPort || port.PrivatePort == targetPort {
92+
return true, nil
93+
}
94+
}
95+
}
96+
} else {
97+
//If the IP address matches, check the ports being mapped too
98+
if network.IPAddress == targetAddress {
99+
for _, port := range c.Ports {
100+
if port.PublicPort == targetPort || port.PrivatePort == targetPort {
101+
return true, nil
102+
}
103+
}
104+
}
105+
}
106+
}
107+
}
108+
109+
combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort)
110+
return false, fmt.Errorf("target address not within host container network: %s", combinedTargetAddress)
111+
}
112+
70113
// ListContainers lists all Docker containers with their network information
71-
func ListContainers(socketPath string) ([]Container, error) {
114+
func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Container, error) {
72115
// Use the provided socket path or default to standard location
73116
if socketPath == "" {
74117
socketPath = "/var/run/docker.sock"
75118
}
76119

120+
// Used to filter down containers returned to Pangolin
121+
containerFilters := filters.NewArgs()
122+
123+
// Used to determine if we will send IP addresses or hostnames to Pangolin
124+
useContainerIpAddresses := true
125+
hostContainerId := ""
126+
77127
// Create a new Docker client
78128
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
79129
defer cancel()
@@ -86,16 +136,54 @@ func ListContainers(socketPath string) ([]Container, error) {
86136
if err != nil {
87137
return nil, fmt.Errorf("failed to create Docker client: %v", err)
88138
}
139+
89140
defer cli.Close()
90141

142+
hostContainer, err := getHostContainer(ctx, cli)
143+
if enforceNetworkValidation && err != nil {
144+
return nil, fmt.Errorf("network validation enforced, cannot validate due to: %w", err)
145+
}
146+
147+
// We may not be able to get back host container in scenarios like running the container in network mode 'host'
148+
if hostContainer != nil {
149+
// We can use the host container to filter out the list of returned containers
150+
hostContainerId = hostContainer.ID
151+
152+
for hostContainerNetworkName := range hostContainer.NetworkSettings.Networks {
153+
// If we're enforcing network validation, we'll filter on the host containers networks
154+
if enforceNetworkValidation {
155+
containerFilters.Add("network", hostContainerNetworkName)
156+
}
157+
158+
// If the container is on the docker bridge network, we will use IP addresses over hostnames
159+
if useContainerIpAddresses && hostContainerNetworkName != "bridge" {
160+
useContainerIpAddresses = false
161+
}
162+
}
163+
}
164+
91165
// List containers
92-
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
166+
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: containerFilters})
93167
if err != nil {
94168
return nil, fmt.Errorf("failed to list containers: %v", err)
95169
}
96170

97171
var dockerContainers []Container
98172
for _, c := range containers {
173+
// Short ID like docker ps
174+
shortId := c.ID[:12]
175+
176+
// Skip host container if set
177+
if hostContainerId != "" && c.ID == hostContainerId {
178+
continue
179+
}
180+
181+
// Get container name (remove leading slash)
182+
name := ""
183+
if len(c.Names) > 0 {
184+
name = strings.TrimPrefix(c.Names[0], "/")
185+
}
186+
99187
// Convert ports
100188
var ports []Port
101189
for _, port := range c.Ports {
@@ -112,44 +200,36 @@ func ListContainers(socketPath string) ([]Container, error) {
112200
ports = append(ports, dockerPort)
113201
}
114202

115-
// Get container name (remove leading slash)
116-
name := ""
117-
if len(c.Names) > 0 {
118-
name = strings.TrimPrefix(c.Names[0], "/")
119-
}
120-
121203
// Get network information by inspecting the container
122204
networks := make(map[string]Network)
123205

124-
// Inspect container to get detailed network information
125-
containerInfo, err := cli.ContainerInspect(ctx, c.ID)
126-
if err != nil {
127-
logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err)
128-
// Continue without network info if inspection fails
129-
} else {
130-
// Extract network information from inspection
131-
if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil {
132-
for networkName, endpoint := range containerInfo.NetworkSettings.Networks {
133-
dockerNetwork := Network{
134-
NetworkID: endpoint.NetworkID,
135-
EndpointID: endpoint.EndpointID,
136-
Gateway: endpoint.Gateway,
137-
IPAddress: endpoint.IPAddress,
138-
IPPrefixLen: endpoint.IPPrefixLen,
139-
IPv6Gateway: endpoint.IPv6Gateway,
140-
GlobalIPv6Address: endpoint.GlobalIPv6Address,
141-
GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen,
142-
MacAddress: endpoint.MacAddress,
143-
Aliases: endpoint.Aliases,
144-
DNSNames: endpoint.DNSNames,
145-
}
146-
networks[networkName] = dockerNetwork
206+
// Extract network information from inspection
207+
if c.NetworkSettings != nil && c.NetworkSettings.Networks != nil {
208+
for networkName, endpoint := range c.NetworkSettings.Networks {
209+
dockerNetwork := Network{
210+
NetworkID: endpoint.NetworkID,
211+
EndpointID: endpoint.EndpointID,
212+
Gateway: endpoint.Gateway,
213+
IPPrefixLen: endpoint.IPPrefixLen,
214+
IPv6Gateway: endpoint.IPv6Gateway,
215+
GlobalIPv6Address: endpoint.GlobalIPv6Address,
216+
GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen,
217+
MacAddress: endpoint.MacAddress,
218+
Aliases: endpoint.Aliases,
219+
DNSNames: endpoint.DNSNames,
220+
}
221+
222+
// Use IPs over hostnames/containers as we're on the bridge network
223+
if useContainerIpAddresses {
224+
dockerNetwork.IPAddress = endpoint.IPAddress
147225
}
226+
227+
networks[networkName] = dockerNetwork
148228
}
149229
}
150230

151231
dockerContainer := Container{
152-
ID: c.ID[:12], // Show short ID like docker ps
232+
ID: shortId,
153233
Name: name,
154234
Image: c.Image,
155235
State: c.State,
@@ -159,8 +239,26 @@ func ListContainers(socketPath string) ([]Container, error) {
159239
Created: c.Created,
160240
Networks: networks,
161241
}
242+
162243
dockerContainers = append(dockerContainers, dockerContainer)
163244
}
164245

165246
return dockerContainers, nil
166247
}
248+
249+
// getHostContainer gets the current container for the current host if possible
250+
func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) {
251+
// Get hostname from the os
252+
hostContainerName, err := os.Hostname()
253+
if err != nil {
254+
return nil, fmt.Errorf("failed to find hostname for container")
255+
}
256+
257+
// Get host container from the docker socket
258+
hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName)
259+
if err != nil {
260+
return nil, fmt.Errorf("failed to find host container")
261+
}
262+
263+
return &hostContainer, nil
264+
}

0 commit comments

Comments
 (0)