diff --git a/v1/networking_validation.go b/v1/networking_validation.go index bacbda4..8fc1b87 100644 --- a/v1/networking_validation.go +++ b/v1/networking_validation.go @@ -6,6 +6,8 @@ import ( "fmt" "net" "net/http" + "regexp" + "strconv" "strings" "time" @@ -188,8 +190,8 @@ func ValidateDockerFirewallAllowsEgress(ctx context.Context, client CloudInstanc if err != nil { return fmt.Errorf("failed to connect to Google's DNS server: %w, stderr: %s", err, stderr) } - if !strings.Contains(stdout, "3 packets transmitted, 3 packets received") { - return fmt.Errorf("expected successful ping, got: %s", stdout) + if !dockerEgressPingReceivedReply(stdout) { + return fmt.Errorf("expected at least one successful ping reply, got: %s", stdout) } return nil @@ -464,6 +466,21 @@ func runMicroK8sPodToPodTest(ctx context.Context, sshClient *ssh.Client, microK8 return nil } +var pingStatsRe = regexp.MustCompile(`(?m)(\d+)\s+packets transmitted,\s+(\d+)\s+(?:packets\s+)?received`) + +func dockerEgressPingReceivedReply(stdout string) bool { + matches := pingStatsRe.FindStringSubmatch(stdout) + if len(matches) != 3 { + return false + } + + received, err := strconv.Atoi(matches[2]) + if err != nil { + return false + } + return received > 0 +} + // setupDockerCommand ensures Docker is available and returns the command to use (always with sudo) func setupDockerCommand(ctx context.Context, sshClient *ssh.Client, instanceID CloudProviderInstanceID) (string, error) { // Check if Docker is available diff --git a/v1/networking_validation_test.go b/v1/networking_validation_test.go new file mode 100644 index 0000000..5fe5687 --- /dev/null +++ b/v1/networking_validation_test.go @@ -0,0 +1,58 @@ +package v1 + +import "testing" + +func TestDockerEgressPingReceivedReply(t *testing.T) { + tests := []struct { + name string + out string + want bool + }{ + { + name: "all packets received", + out: `PING 8.8.8.8 (8.8.8.8): 56 data bytes +64 bytes from 8.8.8.8: seq=0 ttl=117 time=41.193 ms +64 bytes from 8.8.8.8: seq=1 ttl=117 time=41.023 ms +64 bytes from 8.8.8.8: seq=2 ttl=117 time=41.023 ms + +--- 8.8.8.8 ping statistics --- +3 packets transmitted, 3 packets received, 0% packet loss`, + want: true, + }, + { + name: "partial packet loss still proves egress", + out: `PING 8.8.8.8 (8.8.8.8): 56 data bytes +64 bytes from 8.8.8.8: seq=0 ttl=117 time=41.193 ms +64 bytes from 8.8.8.8: seq=2 ttl=117 time=41.023 ms + +--- 8.8.8.8 ping statistics --- +3 packets transmitted, 2 packets received, 33% packet loss`, + want: true, + }, + { + name: "iputils ping format", + out: `--- 8.8.8.8 ping statistics --- +3 packets transmitted, 1 received, 66% packet loss, time 2003ms`, + want: true, + }, + { + name: "no replies", + out: `--- 8.8.8.8 ping statistics --- +3 packets transmitted, 0 packets received, 100% packet loss`, + want: false, + }, + { + name: "no parseable stats", + out: `ping failed`, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := dockerEgressPingReceivedReply(tt.out); got != tt.want { + t.Fatalf("dockerEgressPingReceivedReply() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v1/providers/nebius/scripts/brev-apply-docker-firewall.sh b/v1/providers/nebius/scripts/brev-apply-docker-firewall.sh index 001797c..3fc373d 100644 --- a/v1/providers/nebius/scripts/brev-apply-docker-firewall.sh +++ b/v1/providers/nebius/scripts/brev-apply-docker-firewall.sh @@ -11,7 +11,8 @@ iptables -A DOCKER-USER -i docker0 -o docker0 -j ACCEPT iptables -A DOCKER-USER -i br+ -o br+ -j ACCEPT iptables -A DOCKER-USER -i cni+ -o cni+ -j ACCEPT iptables -A DOCKER-USER -i cali+ -o cali+ -j ACCEPT -iptables -A DOCKER-USER -i lo -j ACCEPT +iptables -A DOCKER-USER -i lo -j ACCEPT +iptables -A DOCKER-USER -i wt0 -j ACCEPT iptables -A DOCKER-USER -j DROP iptables -A DOCKER-USER -j RETURN diff --git a/v1/providers/shadeform/firewall.go b/v1/providers/shadeform/firewall.go index 49bc4e4..37da141 100644 --- a/v1/providers/shadeform/firewall.go +++ b/v1/providers/shadeform/firewall.go @@ -40,6 +40,9 @@ const ( // Allow inbound traffic on the loopback interface. ipTablesAllowDockerUserInpboundLoopback = "iptables -A DOCKER-USER -i lo -j ACCEPT" + // Allow inbound traffic on the netbird interface. + ipTablesAllowDockerUserInpboundNetbird = "iptables -A DOCKER-USER -i wt0 -j ACCEPT" + // Allow external inbound TCP traffic to any container port 22 (SSH) ipTablesAllowDockerUserContainerSSH = "iptables -A DOCKER-USER -p tcp --dport 22 -j ACCEPT" @@ -99,6 +102,7 @@ func (c *ShadeformClient) getIPTablesCommands() []string { ipTablesAllowDockerUserDockerToDocker2, ipTablesAllowDockerUserDockerToDocker3, ipTablesAllowDockerUserInpboundLoopback, + ipTablesAllowDockerUserInpboundNetbird, ipTablesAllowDockerUserContainerSSH, ipTablesDropDockerUserInbound, ipTablesReturnDockerUser, // Expected by Docker