diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e633ec1..b019b2f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -58,8 +58,16 @@ jobs:
with:
go-version-file: go.mod
+ - name: Install kubectl
+ run: brew install kubectl
+
- name: Setup Docker via Colima
uses: douglascamata/setup-docker-macos-action@v1
+ with:
+ colima-additional-options: "--memory 7 --kubernetes"
+
+ - name: Wait for Kubernetes
+ run: kubectl wait --for=condition=ready nodes --all --timeout=120s
- name: Build
run: make build
diff --git a/AGENTS.md b/AGENTS.md
index cd59dba..072da02 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,6 +12,7 @@ Two Go binaries, one on each side of the tunnel:
- Creates a `utun` interface with a WireGuard server
- Generates ephemeral WireGuard key pairs per run
- Watches Docker events to add/remove routes for container subnets
+ - Watches Kubernetes Nodes and ServiceCIDRs for pod/service CIDR routes
- Reconnects automatically if Docker Desktop restarts
2. **Setup container** (`client/main.go`) - ephemeral container inside Docker Desktop VM
@@ -28,10 +29,14 @@ The tunnel peers use a fixed `10.33.33.0/24` subnet (`10.33.33.1` = host, `10.33
main.go # Host binary entry point (darwin-only build tag)
version/version.go # Build-time version + setup image vars (set via ldflags)
networkmanager/ # macOS routing table management (ifconfig, route)
+networkwatcher/ # Docker and Kubernetes network monitoring
+ docker.go # Docker VM setup, network listing, event watching
+ kube.go # Kubernetes CIDR watcher (Nodes + ServiceCIDRs)
+consoleuser/ # Console user resolution (for root/launchd context)
client/ # Setup container (separate Go module)
main.go # VM-side WireGuard + iptables setup
Dockerfile # Multi-stage build (golang -> debian)
-scripts/e2e-test.sh # Smoke test - spins up nginx, curls its container IP
+scripts/e2e-test.sh # Smoke test - Docker + k8s connectivity
```
## Build and run
diff --git a/README.md b/README.md
index b5142d5..a57aab0 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,10 @@
## Features
- **L3 connectivity:** Connect to Docker containers from macOS host (without port binding).
+- **Kubernetes support:** Automatically routes pod and service CIDRs when local k8s is detected (Docker Desktop k8s, Colima/k3s).
- **Lightweight:** Based on WireGuard (built-in to Linux kernel).
- **Hands-off:** Install once and forget. No need to re-configure every time you restart your Mac or Docker daemon.
-- **Automatic:** Docker networks are automatically added/removed from macOS routing table.
+- **Automatic:** Docker networks and Kubernetes CIDRs are automatically added/removed from macOS routing table.
- **No bloat:** Everything is handled by a single binary. No external dependencies/tools are needed.
## Requirements
@@ -141,6 +142,51 @@ Under the hood, your macOS host's WireGuard IP is translated (NAT) to the Docker
This is safe because only your local macOS host can reach internal containers through the tunnel. Other devices on your LAN cannot reach them unless you have explicitly enabled IP forwarding on your Mac (which is off by default). Even then, LAN traffic is not NAT'd, so the container has no route to reply - effectively making internal containers unreachable from the LAN.
+## Kubernetes
+
+If you have Kubernetes enabled in Docker Desktop or running via Colima, `docker-mac-net-connect` automatically detects it and routes pod and service CIDRs through the tunnel. No configuration needed.
+
+```bash
+# Deploy a pod
+$ kubectl run nginx --image=nginx:alpine
+
+# Connect directly to the pod IP from macOS
+$ curl -I $(kubectl get pod nginx -o jsonpath='{.status.podIP}')
+HTTP/1.1 200 OK
+
+# Service ClusterIPs work too
+$ kubectl expose pod nginx --port=80 --name=nginx-svc
+$ curl -I $(kubectl get svc nginx-svc -o jsonpath='{.spec.clusterIP}')
+HTTP/1.1 200 OK
+```
+
+### How it works
+
+The server reads your Docker and kubeconfig contexts at startup and pins them for the session. It monitors:
+
+- **Pod CIDRs** - discovered from Node `spec.podCIDR` fields via the Kubernetes API. On Docker Desktop (which doesn't set `spec.podCIDR`), pod CIDRs are discovered from the VM's routing table instead.
+- **Service CIDRs** - discovered from the ServiceCIDR API (k8s 1.33+)
+
+Routes are added/removed automatically as CIDRs change.
+
+### Supported configurations
+
+Docker Desktop supports two cluster modes: **Kubeadm** (single-node, the default) and **kind** (multi-node). Both are supported.
+
+| Runtime | Pod routing | Service routing |
+| ------------------------ | ----------- | --------------- |
+| Docker Desktop (Kubeadm) | Yes | Yes |
+| Docker Desktop (kind) | Yes | Yes |
+| Colima (k3s) | Yes | Yes |
+
+Service CIDR routing requires the ServiceCIDR API (k8s 1.33+). On older versions, pod routing still works but service ClusterIPs won't be routable.
+
+### Context pinning
+
+The Docker and kubeconfig contexts are snapshotted once at startup (or when Docker Desktop restarts) and stay fixed for the session. If you switch Docker or Kubernetes contexts later, the service won't pick up the change automatically - you'll need to restart it.
+
+We chose this approach to keep the Docker and Kubernetes contexts coupled together - if they drifted independently mid-session, the service could end up routing CIDRs from one cluster through the wrong Docker runtime's tunnel. In practice this is an edge case since most setups use a single runtime, but it's worth knowing about if you switch between Docker Desktop and Colima. In the future we may add support for setting the contexts via a config file so that you don't have to rely on the correct contexts being active at startup.
+
## Accessing Containers from the LAN
By default, `docker-mac-net-connect` enables your macOS host to reach containers directly by IP. With some additional configuration, other devices on your local network can reach containers too.
@@ -197,11 +243,11 @@ This tool was designed to assist with development on macOS. Since Docker-for-Mac
### What happens if Docker Desktop restarts?
-The server detects when the Docker daemon stops and automatically reconfigures the tunnel when it starts back up.
+The server detects when the Docker daemon stops and automatically reconfigures the tunnel when it starts back up. If Kubernetes is enabled, pod and service CIDR routes are also re-added.
### Do you add/remove routes when Docker networks change?
-Yes, the server watches the Docker daemon for both network creations and deletions and will add/remove routes accordingly.
+Yes, the server watches the Docker daemon for both network creations and deletions and will add/remove routes accordingly. Kubernetes pod and service CIDRs are also watched and routed automatically.
For example, let's create a Docker network with subnet `172.200.0.0/16`:
@@ -249,6 +295,16 @@ sudo docker-mac-net-connect
This will show any debug messages that may indicate what is causing your issue.
+- **Kubernetes connections not working?** Your kubeconfig context must match your Docker runtime. For example, if you're using Docker Desktop, your kube context should be `docker-desktop`. If you're using Colima, it should be `colima`. The kube context is snapshotted when the service starts - if they were mismatched at startup, fix both contexts and restart the service:
+
+```bash
+# Set the correct kube context
+kubectl config use-context docker-desktop
+
+# Restart the service
+sudo brew services restart chipmk/tap/docker-mac-net-connect
+```
+
## License
MIT
diff --git a/TRIAGE.md b/TRIAGE.md
new file mode 100644
index 0000000..b3c70b6
--- /dev/null
+++ b/TRIAGE.md
@@ -0,0 +1,124 @@
+# Issue & PR Triage - docker-mac-net-connect
+
+> Snapshot taken 2026-02-12. 24 open issues, 12 open PRs.
+
+## Project Context
+
+docker-mac-net-connect creates a WireGuard tunnel between macOS and the Docker Desktop Linux VM so you can reach containers by IP without port binding. It runs as a Homebrew service. The codebase is small (~650 lines of Go across 3 files) but has accumulated a backlog of issues and PRs spanning 4+ years.
+
+---
+
+## Theme 1: Docker Desktop Compatibility (Critical)
+
+Every major Docker Desktop update risks breaking the tool. This is the #1 recurring pain point.
+
+| # | Type | Title | Notes |
+|---|------|-------|-------|
+| [#62](https://github.com/chipmk/docker-mac-net-connect/issues/62) | Issue | Docker Desktop 4.52.0 broke things | API version 1.41 too old, min is now 1.44 |
+| [#61](https://github.com/chipmk/docker-mac-net-connect/pulls/61) | PR | Updated Docker API and other modernisations | **Fixes #62** - bumps Docker client SDK |
+| [#46](https://github.com/chipmk/docker-mac-net-connect/issues/46) | Issue | Docker Desktop 4.37.2 breaking things | Same class of problem, older version |
+| [#41](https://github.com/chipmk/docker-mac-net-connect/issues/41) | Issue | Docker Desktop host networking conflicts | DD 4.34.0 host networking feature causes login failures |
+| [#36](https://github.com/chipmk/docker-mac-net-connect/issues/36) | Issue | Resource Saver kills the tunnel | DD's idle VM shutdown (default since 4.24) breaks WireGuard |
+| [#21](https://github.com/chipmk/docker-mac-net-connect/issues/21) | Issue | Stopped working with DD 4.16.1 | Old - likely resolved by now |
+| [#20](https://github.com/chipmk/docker-mac-net-connect/issues/20) | Issue | Docker crashes | Old report, DD version unknown |
+
+**How it ties together:** PR #61 directly solves #62 (and likely #46) by updating the Docker Go client to support newer API versions. The recent commit `8b0ad18` already bumped Go to 1.26 and updated server-side deps, but #61 goes further with the client SDK. Issues #36 and #41 are architectural - they need design changes (reconnect logic for Resource Saver, conflict detection for host networking), not just dep bumps.
+
+**Suggested action:** Merge or supersede PR #61. Issues #21 and #20 are likely stale and could be closed.
+
+---
+
+## Theme 2: Dependency Updates & CI Hygiene
+
+A wave of Dependabot PRs just landed alongside two tracking issues for modernizing the build.
+
+| # | Type | Title | Notes |
+|---|------|-------|-------|
+| [#67](https://github.com/chipmk/docker-mac-net-connect/issues/67) | Issue | Fix golangci-lint errors | Currently suppressed with `only-new-issues` |
+| [#66](https://github.com/chipmk/docker-mac-net-connect/issues/66) | Issue | Bump client Go module to 1.26 | Client go.mod still targets Go 1.17 |
+| [#73](https://github.com/chipmk/docker-mac-net-connect/pulls/73) | PR | Bump golangci-lint-action 7 -> 9 | Dependabot |
+| [#72](https://github.com/chipmk/docker-mac-net-connect/pulls/72) | PR | Bump actions/setup-go 5 -> 6 | Dependabot |
+| [#71](https://github.com/chipmk/docker-mac-net-connect/pulls/71) | PR | Bump go-iptables 0.6.0 -> 0.8.0 (client) | Dependabot |
+| [#70](https://github.com/chipmk/docker-mac-net-connect/pulls/70) | PR | Bump actions/github-script 7 -> 8 | Dependabot |
+| [#69](https://github.com/chipmk/docker-mac-net-connect/pulls/69) | PR | Bump netlink to 1.3.1 (client) | Dependabot |
+| [#68](https://github.com/chipmk/docker-mac-net-connect/pulls/68) | PR | Bump actions/checkout 4 -> 6 | Dependabot |
+| [#55](https://github.com/chipmk/docker-mac-net-connect/pulls/55) | PR | Update dependencies | Community PR from avoidik |
+| [#35](https://github.com/chipmk/docker-mac-net-connect/pulls/35) | PR | Bump vulnerable dependencies | 2+ years old, likely superseded |
+
+**How it ties together:** Issue #66 is the root cause - the client module's go.mod is stuck on Go 1.17 with 2021-era deps. Once #66 is resolved (bump client to Go 1.26), Dependabot PRs #71 and #69 become straightforward merges. The CI action PRs (#73, #72, #70, #68) are independent and can be merged anytime. Issue #67 (lint fixes) is blocked until the Go version bump lands since golangci-lint v2 won't run against Go 1.17 code. PR #35 is almost certainly superseded by the newer dep updates. PR #55 may also be superseded depending on overlap with #61 and the Dependabot PRs.
+
+**Suggested action:** Resolve #66 first, then merge Dependabot PRs, then tackle #67. Close #35 and evaluate #55 for overlap.
+
+---
+
+## Theme 3: Alternative Runtime Support
+
+Users want this tool to work beyond Docker Desktop for Mac.
+
+| # | Type | Title | Notes |
+|---|------|-------|-------|
+| [#26](https://github.com/chipmk/docker-mac-net-connect/issues/26) | Issue | Support lima environments | Tracking issue from gregnr |
+| [#27](https://github.com/chipmk/docker-mac-net-connect/pulls/27) | PR | feat: Support Lima VMs iptables rules | DRAFT - adds Lima support, needs polish |
+| [#16](https://github.com/chipmk/docker-mac-net-connect/issues/16) | Issue | Help making it work with colima | Community request |
+| [#22](https://github.com/chipmk/docker-mac-net-connect/issues/22) | Issue | Minikube support? | Community request |
+| [#13](https://github.com/chipmk/docker-mac-net-connect/issues/13) | Issue | Podman support | Community request |
+| [#37](https://github.com/chipmk/docker-mac-net-connect/pulls/37) | PR | Support Windows (Docker Desktop) | Community PR, author unsure if it belongs here |
+
+**How it ties together:** Issue #26 is the umbrella for Lima-based tools (colima, Rancher Desktop). Draft PR #27 is a partial implementation - it adds iptables rules for Lima VMs but still requires manual Docker socket symlinks. Issues #16 and #22 are effectively duplicates of #26 for specific Lima-based tools. #13 (Podman) and #37 (Windows) are separate platforms entirely. The config file feature (#24 in Theme 4) would help here - supporting custom Docker socket paths is a prerequisite for clean Lima/colima support.
+
+**Suggested action:** #27 needs a maintainer to shepherd it to completion. #37 (Windows) is a large scope change - decide if it belongs in this repo or a fork. #13 and #22 could be consolidated under #26.
+
+---
+
+## Theme 4: Feature Requests
+
+| # | Type | Title | Notes |
+|---|------|-------|-------|
+| [#24](https://github.com/chipmk/docker-mac-net-connect/issues/24) | Issue | Config file | Filter networks, custom socket paths, etc. |
+| [#25](https://github.com/chipmk/docker-mac-net-connect/pulls/25) | PR | Allow connections to internal networks | DRAFT - addresses #23 |
+| [#23](https://github.com/chipmk/docker-mac-net-connect/issues/23) | Issue | Support --internal networks | Currently skipped |
+| [#15](https://github.com/chipmk/docker-mac-net-connect/issues/15) | Issue | Filter specific networks | Want to limit which networks get routed |
+| [#47](https://github.com/chipmk/docker-mac-net-connect/issues/47) | Issue | Support DD's built-in Kubernetes | Feature request |
+| [#8](https://github.com/chipmk/docker-mac-net-connect/issues/8) | Issue | --net="host" support | Want host network mode to work |
+| [#6](https://github.com/chipmk/docker-mac-net-connect/issues/6) | Issue | Bind multiple ports to one IP | Feature request |
+| [#54](https://github.com/chipmk/docker-mac-net-connect/issues/54) | Issue | Add IPs to network diagram | Docs improvement |
+| [#3](https://github.com/chipmk/docker-mac-net-connect/issues/3) | Issue | GitHub Actions | CI/CD tracking issue from 2021 |
+
+**How it ties together:** Issues #15 and #23 both want network filtering - #24 (config file) is the enabler for both. Draft PR #25 partially addresses #23 by allowing internal networks. #47, #8, and #6 are standalone feature requests with no active work. #3 is likely partially resolved given CI exists now. #54 is a small docs fix.
+
+**Suggested action:** #24 (config file) would unblock multiple issues and is worth prioritizing. PR #25 is close to addressing #23. #3 can probably be closed since CI workflows exist.
+
+---
+
+## Theme 5: Support / Troubleshooting (Low Priority)
+
+| # | Type | Title | Notes |
+|---|------|-------|-------|
+| [#30](https://github.com/chipmk/docker-mac-net-connect/issues/30) | Issue | Mac M2 can't connect | Likely resolved or needs more info |
+| [#29](https://github.com/chipmk/docker-mac-net-connect/issues/29) | Issue | Question about root ownership | Brew service runs as root |
+| [#28](https://github.com/chipmk/docker-mac-net-connect/issues/28) | Issue | Access containers from other machines | Out of scope - tool is for local access |
+| [#12](https://github.com/chipmk/docker-mac-net-connect/issues/12) | Issue | How to uninstall? | Docs gap |
+
+**How it ties together:** These are one-off support questions, mostly from 2022-2023. They indicate gaps in documentation (uninstall instructions, root ownership explanation) rather than code issues.
+
+**Suggested action:** Answer and close. #28 is out of scope. #30 is likely stale. #12 and #29 could be addressed with README updates.
+
+---
+
+## Recommended Priority Order
+
+1. **Merge dependency updates** - CI actions (#68, #70, #72, #73), then tackle #66 (client Go bump), then client deps (#69, #71)
+2. **Fix Docker Desktop compat** - Evaluate PR #61 vs what's already landed, resolve #62
+3. **Lint cleanup** - #67, once Go version is bumped
+4. **Config file** - #24 unblocks network filtering (#15, #23) and custom sockets (#26)
+5. **Internal networks** - Finish PR #25
+6. **Lima support** - Finish PR #27, consolidate #16/#22 under #26
+7. **Stale issue cleanup** - Close #21, #20, #3, #30, #28, #12 with appropriate responses
+
+## PR Overlap / Superseded Work
+
+| PR | Status | Likely superseded by |
+|----|--------|---------------------|
+| #35 (vuln deps) | Stale (2024) | Dependabot PRs + #61 |
+| #55 (update deps) | Stale (2025) | Dependabot PRs + #61 |
+| #61 (Docker API) | Active | Partially by commit `8b0ad18`, but still needed for client SDK |
diff --git a/assets/connection-diagram.html b/assets/connection-diagram.html
new file mode 100644
index 0000000..6c659f6
--- /dev/null
+++ b/assets/connection-diagram.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Connection Diagram
+
+
+
+
+
+
diff --git a/client/main.go b/client/main.go
index 7b382aa..6717244 100644
--- a/client/main.go
+++ b/client/main.go
@@ -287,4 +287,19 @@ func main() {
fmt.Printf("Failed to add route: %v\n", err)
os.Exit(ExitSetupFailed)
}
+
+ // Report CNI routes from the VM so the host can add them for k8s pod
+ // connectivity. Docker Desktop doesn't set spec.podCIDR on nodes, so
+ // the host can't discover pod CIDRs via the k8s API alone.
+ cni, err := netlink.LinkByName("cni0")
+ if err == nil {
+ routes, err := netlink.RouteList(cni, netlink.FAMILY_V4)
+ if err == nil {
+ for _, r := range routes {
+ if r.Dst != nil {
+ fmt.Printf("VM_ROUTE=%s\n", r.Dst.String())
+ }
+ }
+ }
+ }
}
diff --git a/consoleuser/consoleuser.go b/consoleuser/consoleuser.go
new file mode 100644
index 0000000..f23c82c
--- /dev/null
+++ b/consoleuser/consoleuser.go
@@ -0,0 +1,48 @@
+//go:build darwin
+
+package consoleuser
+
+import (
+ "fmt"
+ "os/user"
+ "strconv"
+ "syscall"
+
+ "os"
+)
+
+// Username returns the username of the currently logged-in GUI user
+// by checking the owner of /dev/console.
+func Username() (string, error) {
+ info, err := os.Stat("/dev/console")
+ if err != nil {
+ return "", fmt.Errorf("stat /dev/console: %w", err)
+ }
+ stat, ok := info.Sys().(*syscall.Stat_t)
+ if !ok {
+ return "", fmt.Errorf("unexpected stat type for /dev/console")
+ }
+ u, err := user.LookupId(strconv.FormatUint(uint64(stat.Uid), 10))
+ if err != nil {
+ return "", fmt.Errorf("lookup uid %d: %w", stat.Uid, err)
+ }
+ if u.Username == "root" {
+ return "", fmt.Errorf("no console user logged in")
+ }
+ return u.Username, nil
+}
+
+// HomeDir returns the home directory of the currently logged-in GUI user.
+// This is useful when running as root (e.g. via launchd) where ~ resolves
+// to /var/root instead of the actual user's home.
+func HomeDir() (string, error) {
+ username, err := Username()
+ if err != nil {
+ return "", err
+ }
+ u, err := user.Lookup(username)
+ if err != nil {
+ return "", fmt.Errorf("lookup user %s: %w", username, err)
+ }
+ return u.HomeDir, nil
+}
diff --git a/go.mod b/go.mod
index 0cfc4d4..d0f6189 100644
--- a/go.mod
+++ b/go.mod
@@ -4,8 +4,12 @@ go 1.26
require (
github.com/docker/docker v28.5.2+incompatible
+ github.com/docker/go-sdk/context v0.1.0-alpha012
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
+ k8s.io/api v0.35.1
+ k8s.io/apimachinery v0.35.1
+ k8s.io/client-go v0.35.1
)
require (
@@ -18,13 +22,22 @@ require (
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-sdk/config v0.1.0-alpha012 // indirect
- github.com/docker/go-sdk/context v0.1.0-alpha012 // indirect
github.com/docker/go-units v0.5.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
@@ -32,24 +45,44 @@ require (
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/morikuni/aec v1.1.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/spf13/pflag v1.0.9 // indirect
github.com/stretchr/testify v1.11.1 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
+ golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/term v0.40.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+ golang.org/x/time v0.9.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- gotest.tools/v3 v3.5.2 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
+ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
+ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+ sigs.k8s.io/randfill v1.0.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+ sigs.k8s.io/yaml v1.6.0 // indirect
)
diff --git a/go.sum b/go.sum
index 02e8cf0..d49eea2 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -16,6 +18,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -30,15 +34,29 @@ github.com/docker/go-sdk/context v0.1.0-alpha012 h1:977FC+15aKg3KxA+VqvzEHANcKRT
github.com/docker/go-sdk/context v0.1.0-alpha012/go.mod h1:UJfIj4J1ogiYPUSt+W0NLM5OWgpYHJEQVI9dHhQYss8=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -51,19 +69,37 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
-github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=
@@ -82,8 +118,20 @@ github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7z
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
+github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
+github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -94,14 +142,29 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
@@ -122,6 +185,10 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
@@ -129,6 +196,8 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -136,6 +205,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -147,16 +218,20 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
-golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
-golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
@@ -187,14 +262,41 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
-gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
-gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
+k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
+k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
+k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
+k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/main.go b/main.go
index 58444b3..5e80b68 100644
--- a/main.go
+++ b/main.go
@@ -5,23 +5,14 @@ package main
import (
"context"
"fmt"
- "io"
"net"
"os"
"os/signal"
- "os/user"
"path/filepath"
- "strconv"
"syscall"
"time"
- "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/api/types/events"
- "github.com/docker/docker/api/types/filters"
- "github.com/docker/docker/api/types/image"
- "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
- "github.com/docker/docker/pkg/stdcopy"
dcontext "github.com/docker/go-sdk/context"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
@@ -30,7 +21,9 @@ import (
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+ "github.com/chipmk/docker-mac-net-connect/consoleuser"
"github.com/chipmk/docker-mac-net-connect/networkmanager"
+ "github.com/chipmk/docker-mac-net-connect/networkwatcher"
"github.com/chipmk/docker-mac-net-connect/version"
)
@@ -185,25 +178,20 @@ func main() {
logger.Verbosef("Interface %s created\n", interfaceName)
- // When running as root (e.g. via launchd), the docker config lives under
- // the console user's home directory. Set DOCKER_CONFIG so the context
- // resolver can find it.
- if os.Getenv("DOCKER_CONFIG") == "" {
- consoleUser, err := getConsoleUser()
- if err != nil {
- logger.Verbosef("Failed to get console user: %v\n", err)
+ // Resolve console user's home directory for config file lookups
+ // when running as root (e.g. via launchd).
+ homeDir, err := consoleuser.HomeDir()
+ if err != nil {
+ logger.Verbosef("Failed to resolve console user home: %v\n", err)
+ }
+
+ // Set DOCKER_CONFIG so the context resolver can find it.
+ if os.Getenv("DOCKER_CONFIG") == "" && homeDir != "" {
+ dockerConfig := filepath.Join(homeDir, ".docker")
+ if err := os.Setenv("DOCKER_CONFIG", dockerConfig); err != nil {
+ logger.Verbosef("Failed to set DOCKER_CONFIG: %v\n", err)
} else {
- u, err := user.Lookup(consoleUser)
- if err != nil {
- logger.Verbosef("Failed to lookup user %s: %v\n", consoleUser, err)
- } else {
- dockerConfig := filepath.Join(u.HomeDir, ".docker")
- if err := os.Setenv("DOCKER_CONFIG", dockerConfig); err != nil {
- logger.Verbosef("Failed to set DOCKER_CONFIG: %v\n", err)
- } else {
- logger.Verbosef("Set DOCKER_CONFIG to %s (console user: %s)\n", dockerConfig, consoleUser)
- }
- }
+ logger.Verbosef("Set DOCKER_CONFIG to %s\n", dockerConfig)
}
}
@@ -227,70 +215,72 @@ func main() {
ctx := context.Background()
+ addRoute := func(cidr, name string) {
+ fmt.Printf("Adding route for %s -> %s (%s)\n", cidr, interfaceName, name)
+ _, stderr, err := networkManager.AddRoute(cidr, interfaceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to add route: %v. %v\n", err, stderr)
+ }
+ }
+
+ removeRoute := func(cidr, name string) {
+ fmt.Printf("Deleting route for %s (%s)\n", cidr, name)
+ _, stderr, err := networkManager.DeleteRoute(cidr)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to delete route: %v. %v\n", err, stderr)
+ }
+ }
+
go func() {
for {
+ // -- Session start --
+ // Each iteration is a full Docker Desktop session. If Docker
+ // restarts, everything in the VM is gone (WireGuard interface,
+ // iptables rules, k8s API server), so we re-initialize everything.
+
+ sessionCtx, cancelSession := context.WithCancel(ctx)
+
logger.Verbosef("Setting up Wireguard on Docker Desktop VM\n")
- err = setupVm(ctx, cli, port, hostPeerIp, vmPeerIp, hostPrivateKey, vmPrivateKey)
+ vmRoutes, err := networkwatcher.SetupVM(sessionCtx, cli, port, hostPeerIp, vmPeerIp, hostPrivateKey, vmPrivateKey)
if err != nil {
logger.Errorf("Failed to setup VM: %v", err)
+ cancelSession()
time.Sleep(5 * time.Second)
continue
}
- networks, err := cli.NetworkList(ctx, network.ListOptions{})
+ // Discover and route existing Docker networks.
+ err = networkwatcher.ListNetworks(sessionCtx, cli, addRoute)
if err != nil {
logger.Errorf("Failed to list Docker networks: %v", err)
+ cancelSession()
time.Sleep(5 * time.Second)
continue
}
- for _, network := range networks {
- networkManager.ProcessDockerNetworkCreate(network, interfaceName)
+ // Start k8s watcher in background (scoped to this session).
+ // Retries independently - k8s can start/stop separately from Docker.
+ // Pin the kube context now so it stays coupled to this Docker session
+ // even if the user switches kube contexts later.
+ if homeDir != "" {
+ if kubeContext := networkwatcher.ResolveKubeContext(homeDir); kubeContext != "" {
+ go networkwatcher.RunKubeWatcher(sessionCtx, homeDir, kubeContext, vmRoutes, addRoute, removeRoute)
+ } else {
+ fmt.Println("No kubeconfig context set, skipping Kubernetes watcher")
+ }
}
+ // Watch Docker events (blocks until disconnect).
logger.Verbosef("Watching Docker events\n")
- msgs, errsChan := cli.Events(ctx, events.ListOptions{
- Filters: filters.NewArgs(
- filters.Arg("type", "network"),
- filters.Arg("event", "create"),
- filters.Arg("event", "destroy"),
- ),
- })
-
- for loop := true; loop; {
- select {
- case err := <-errsChan:
- logger.Errorf("Error: %v\n", err)
- loop = false
- case msg := <-msgs:
- // Add routes when new Docker networks are created
- if msg.Type == "network" && msg.Action == "create" {
- loopNetwork, err := cli.NetworkInspect(ctx, msg.Actor.ID, network.InspectOptions{})
- if err != nil {
- logger.Errorf("Failed to inspect new Docker network: %v", err)
- continue
- }
-
- networkManager.ProcessDockerNetworkCreate(loopNetwork, interfaceName)
- continue
- }
-
- // Delete routes when Docker networks are destroyed
- if msg.Type == "network" && msg.Action == "destroy" {
- loopNetwork, exists := networkManager.DockerNetworks[msg.Actor.ID]
- if !exists {
- logger.Errorf("Unknown Docker network with ID %s. No routes will be removed.")
- continue
- }
-
- networkManager.ProcessDockerNetworkDestroy(loopNetwork)
- continue
- }
- }
+ err = networkwatcher.WatchEvents(sessionCtx, cli, addRoute, removeRoute)
+ if err != nil {
+ logger.Errorf("Docker watch error: %v", err)
}
+ // -- Session over --
+ cancelSession()
time.Sleep(5 * time.Second)
}
}()
@@ -313,98 +303,3 @@ func main() {
logger.Verbosef("Shutting down\n")
}
-
-func setupVm(
- ctx context.Context,
- dockerCli *client.Client,
- serverPort int,
- hostPeerIp string,
- vmPeerIp string,
- hostPrivateKey wgtypes.Key,
- vmPrivateKey wgtypes.Key,
-) error {
- imageName := fmt.Sprintf("%s:%s", version.SetupImage, version.Version)
-
- _, err := dockerCli.ImageInspect(ctx, imageName)
- if err != nil {
- fmt.Printf("Image doesn't exist locally. Pulling...\n")
-
- pullStream, err := dockerCli.ImagePull(ctx, imageName, image.PullOptions{})
- if err != nil {
- return fmt.Errorf("failed to pull setup image: %w", err)
- }
-
- _, _ = io.Copy(os.Stdout, pullStream)
- }
-
- resp, err := dockerCli.ContainerCreate(ctx, &container.Config{
- Image: imageName,
- Env: []string{
- "SERVER_PORT=" + strconv.Itoa(serverPort),
- "HOST_PEER_IP=" + hostPeerIp,
- "VM_PEER_IP=" + vmPeerIp,
- "HOST_PUBLIC_KEY=" + hostPrivateKey.PublicKey().String(),
- "VM_PRIVATE_KEY=" + vmPrivateKey.String(),
- },
- }, &container.HostConfig{
- AutoRemove: true,
- NetworkMode: "host",
- CapAdd: []string{"NET_ADMIN"},
- }, nil, nil, "wireguard-setup")
- if err != nil {
- return fmt.Errorf("failed to create container: %w", err)
- }
-
- // Run container to completion
- err = dockerCli.ContainerStart(ctx, resp.ID, container.StartOptions{})
- if err != nil {
- return fmt.Errorf("failed to start container: %w", err)
- }
-
- if err := func() error {
- reader, err := dockerCli.ContainerLogs(ctx, resp.ID, container.LogsOptions{
- ShowStdout: true,
- ShowStderr: true,
- Follow: true,
- })
- if err != nil {
- return fmt.Errorf("failed to get logs for container %s: %w", resp.ID, err)
- }
-
- defer func() { _ = reader.Close() }()
-
- _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, reader)
- if err != nil {
- return err
- }
-
- return nil
- }(); err != nil {
- return err
- }
-
- fmt.Println("Setup container complete")
-
- return nil
-}
-
-// getConsoleUser returns the username of the currently logged-in GUI user
-// by checking the owner of /dev/console.
-func getConsoleUser() (string, error) {
- info, err := os.Stat("/dev/console")
- if err != nil {
- return "", fmt.Errorf("stat /dev/console: %w", err)
- }
- stat, ok := info.Sys().(*syscall.Stat_t)
- if !ok {
- return "", fmt.Errorf("unexpected stat type for /dev/console")
- }
- u, err := user.LookupId(strconv.FormatUint(uint64(stat.Uid), 10))
- if err != nil {
- return "", fmt.Errorf("lookup uid %d: %w", stat.Uid, err)
- }
- if u.Username == "root" {
- return "", fmt.Errorf("no console user logged in")
- }
- return u.Username, nil
-}
diff --git a/networkmanager/networkmanager.go b/networkmanager/networkmanager.go
index d706f70..8ea32a4 100644
--- a/networkmanager/networkmanager.go
+++ b/networkmanager/networkmanager.go
@@ -2,26 +2,17 @@ package networkmanager
import (
"bytes"
- "fmt"
- "os"
"os/exec"
-
- "github.com/docker/docker/api/types/network"
)
-type NetworkManager struct {
- DockerNetworks map[string]network.Inspect
-}
+type NetworkManager struct{}
func New() NetworkManager {
- return NetworkManager{
- DockerNetworks: map[string]network.Inspect{},
- }
+ return NetworkManager{}
}
-// SetInterfaceAddress Set the point-to-point IP address configuration on a network interface.
+// SetInterfaceAddress sets the point-to-point IP address configuration on a network interface.
func (manager *NetworkManager) SetInterfaceAddress(ip string, peerIp string, iface string) (string, string, error) {
-
cmd := exec.Command("ifconfig", iface, "inet", ip+"/32", peerIp)
var stdout bytes.Buffer
@@ -35,9 +26,8 @@ func (manager *NetworkManager) SetInterfaceAddress(ip string, peerIp string, ifa
return stdout.String(), stderr.String(), err
}
-// AddRoute Add a route to the macOS routing table.
+// AddRoute adds a route to the macOS routing table.
func (manager *NetworkManager) AddRoute(net string, iface string) (string, string, error) {
-
cmd := exec.Command("route", "-q", "-n", "add", "-inet", net, "-interface", iface)
var stdout bytes.Buffer
@@ -51,9 +41,8 @@ func (manager *NetworkManager) AddRoute(net string, iface string) (string, strin
return stdout.String(), stderr.String(), err
}
-// DeleteRoute Delete a route from the macOS routing table.
+// DeleteRoute deletes a route from the macOS routing table.
func (manager *NetworkManager) DeleteRoute(net string) (string, string, error) {
-
cmd := exec.Command("route", "-q", "-n", "delete", "-inet", net)
var stdout bytes.Buffer
@@ -66,34 +55,3 @@ func (manager *NetworkManager) DeleteRoute(net string) (string, string, error) {
return stdout.String(), stderr.String(), err
}
-
-func (manager *NetworkManager) ProcessDockerNetworkCreate(network network.Inspect, iface string) {
- manager.DockerNetworks[network.ID] = network
-
- for _, config := range network.IPAM.Config {
- if network.Scope == "local" {
- fmt.Printf("Adding route for %s -> %s (%s)\n", config.Subnet, iface, network.Name)
-
- _, stderr, err := manager.AddRoute(config.Subnet, iface)
-
- if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to add route: %v. %v\n", err, stderr)
- }
- }
- }
-}
-
-func (manager *NetworkManager) ProcessDockerNetworkDestroy(network network.Inspect) {
- for _, config := range network.IPAM.Config {
- if network.Scope == "local" {
- fmt.Printf("Deleting route for %s (%s)\n", config.Subnet, network.Name)
-
- _, stderr, err := manager.DeleteRoute(config.Subnet)
-
- if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to delete route: %v. %v\n", err, stderr)
- }
- }
- }
- delete(manager.DockerNetworks, network.ID)
-}
diff --git a/networkwatcher/docker.go b/networkwatcher/docker.go
new file mode 100644
index 0000000..010dce8
--- /dev/null
+++ b/networkwatcher/docker.go
@@ -0,0 +1,211 @@
+//go:build darwin
+
+package networkwatcher
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/events"
+ "github.com/docker/docker/api/types/filters"
+ "github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/api/types/network"
+ "github.com/docker/docker/client"
+ "github.com/docker/docker/pkg/stdcopy"
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+
+ "github.com/chipmk/docker-mac-net-connect/version"
+)
+
+// SetupVM runs the ephemeral setup container inside the Docker Desktop VM
+// to create the WireGuard interface and configure iptables rules.
+// Returns any VM routes discovered by the setup container (e.g. cni0 routes
+// for k8s pod networking).
+func SetupVM(
+ ctx context.Context,
+ dockerCli *client.Client,
+ serverPort int,
+ hostPeerIp string,
+ vmPeerIp string,
+ hostPrivateKey wgtypes.Key,
+ vmPrivateKey wgtypes.Key,
+) ([]string, error) {
+ imageName := fmt.Sprintf("%s:%s", version.SetupImage, version.Version)
+
+ _, err := dockerCli.ImageInspect(ctx, imageName)
+ if err != nil {
+ fmt.Printf("Image doesn't exist locally. Pulling...\n")
+
+ pullStream, err := dockerCli.ImagePull(ctx, imageName, image.PullOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to pull setup image: %w", err)
+ }
+
+ _, _ = io.Copy(os.Stdout, pullStream)
+ }
+
+ resp, err := dockerCli.ContainerCreate(ctx, &container.Config{
+ Image: imageName,
+ Env: []string{
+ "SERVER_PORT=" + strconv.Itoa(serverPort),
+ "HOST_PEER_IP=" + hostPeerIp,
+ "VM_PEER_IP=" + vmPeerIp,
+ "HOST_PUBLIC_KEY=" + hostPrivateKey.PublicKey().String(),
+ "VM_PRIVATE_KEY=" + vmPrivateKey.String(),
+ },
+ }, &container.HostConfig{
+ AutoRemove: true,
+ NetworkMode: "host",
+ CapAdd: []string{"NET_ADMIN"},
+ }, nil, nil, "wireguard-setup")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create container: %w", err)
+ }
+
+ err = dockerCli.ContainerStart(ctx, resp.ID, container.StartOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to start container: %w", err)
+ }
+
+ var stdoutBuf bytes.Buffer
+ if err := func() error {
+ reader, err := dockerCli.ContainerLogs(ctx, resp.ID, container.LogsOptions{
+ ShowStdout: true,
+ ShowStderr: true,
+ Follow: true,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to get logs for container %s: %w", resp.ID, err)
+ }
+
+ defer func() { _ = reader.Close() }()
+
+ // Tee stdout so we can both display logs and parse structured output.
+ stdoutWriter := io.MultiWriter(os.Stdout, &stdoutBuf)
+ _, err = stdcopy.StdCopy(stdoutWriter, os.Stderr, reader)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }(); err != nil {
+ return nil, err
+ }
+
+ fmt.Println("Setup container complete")
+
+ // Parse VM_ROUTE= lines from the setup container's stdout.
+ var vmRoutes []string
+ scanner := bufio.NewScanner(&stdoutBuf)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if after, ok := strings.CutPrefix(line, "VM_ROUTE="); ok {
+ vmRoutes = append(vmRoutes, after)
+ }
+ }
+
+ return vmRoutes, nil
+}
+
+// ListNetworks lists existing Docker networks and calls onAdd for each
+// local network subnet.
+func ListNetworks(
+ ctx context.Context,
+ cli *client.Client,
+ onAdd func(cidr, name string),
+) error {
+ networks, err := cli.NetworkList(ctx, network.ListOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to list Docker networks: %w", err)
+ }
+
+ for _, n := range networks {
+ for _, config := range n.IPAM.Config {
+ if n.Scope == "local" && isIPv4CIDR(config.Subnet) {
+ onAdd(config.Subnet, n.Name)
+ }
+ }
+ }
+
+ return nil
+}
+
+// WatchEvents watches Docker network create/destroy events and calls
+// onAdd/onRemove for subnet changes. Blocks until the event stream
+// disconnects or the context is cancelled.
+func WatchEvents(
+ ctx context.Context,
+ cli *client.Client,
+ onAdd func(cidr, name string),
+ onRemove func(cidr, name string),
+) error {
+ // Track networks for destroy events (need cached IPAM config
+ // since the network is gone by the time we get the event).
+ knownNetworks := map[string]network.Inspect{}
+
+ // Seed known networks from current state.
+ networks, err := cli.NetworkList(ctx, network.ListOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to list Docker networks: %w", err)
+ }
+ for _, n := range networks {
+ knownNetworks[n.ID] = n
+ }
+
+ msgs, errsChan := cli.Events(ctx, events.ListOptions{
+ Filters: filters.NewArgs(
+ filters.Arg("type", "network"),
+ filters.Arg("event", "create"),
+ filters.Arg("event", "destroy"),
+ ),
+ })
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-errsChan:
+ return fmt.Errorf("docker event stream error: %w", err)
+ case msg := <-msgs:
+ if msg.Type == "network" && msg.Action == "create" {
+ n, err := cli.NetworkInspect(ctx, msg.Actor.ID, network.InspectOptions{})
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to inspect new Docker network: %v\n", err)
+ continue
+ }
+
+ knownNetworks[n.ID] = n
+
+ for _, config := range n.IPAM.Config {
+ if n.Scope == "local" && isIPv4CIDR(config.Subnet) {
+ onAdd(config.Subnet, n.Name)
+ }
+ }
+ continue
+ }
+
+ if msg.Type == "network" && msg.Action == "destroy" {
+ n, exists := knownNetworks[msg.Actor.ID]
+ if !exists {
+ fmt.Fprintf(os.Stderr, "Unknown Docker network with ID %s. No routes will be removed.\n", msg.Actor.ID)
+ continue
+ }
+
+ for _, config := range n.IPAM.Config {
+ if n.Scope == "local" && isIPv4CIDR(config.Subnet) {
+ onRemove(config.Subnet, n.Name)
+ }
+ }
+ delete(knownNetworks, msg.Actor.ID)
+ continue
+ }
+ }
+ }
+}
diff --git a/networkwatcher/kube.go b/networkwatcher/kube.go
new file mode 100644
index 0000000..3638a51
--- /dev/null
+++ b/networkwatcher/kube.go
@@ -0,0 +1,509 @@
+//go:build darwin
+
+package networkwatcher
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/url"
+ "path/filepath"
+ "sync"
+ "time"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/clientcmd"
+
+ corev1 "k8s.io/api/core/v1"
+)
+
+// KubeWatcher watches Kubernetes Nodes and ServiceCIDRs for CIDR changes.
+type KubeWatcher struct {
+ clientset *kubernetes.Clientset
+ vmRoutes []string // CNI routes discovered from the VM (fallback for empty spec.podCIDR)
+}
+
+// NewKubeWatcher creates a KubeWatcher pinned to the given kubeconfig context.
+// kubeContext must be non-empty - use ResolveKubeContext to snapshot the
+// context at session start.
+// It verifies the API server is on localhost (local cluster) and
+// reachable before returning. Returns an error if k8s is not available.
+func NewKubeWatcher(homeDir string, kubeContext string) (*KubeWatcher, error) {
+ kubeconfigPath := filepath.Join(homeDir, ".kube", "config")
+
+ loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+ &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
+ &clientcmd.ConfigOverrides{CurrentContext: kubeContext},
+ )
+
+ config, err := loader.ClientConfig()
+ if err != nil {
+ return nil, fmt.Errorf("load kubeconfig: %w", err)
+ }
+
+ // Only connect to local clusters. Remote clusters would have CIDRs
+ // for networks that aren't on the local VM tunnel.
+ serverURL, err := url.Parse(config.Host)
+ if err != nil {
+ return nil, fmt.Errorf("parse API server URL: %w", err)
+ }
+ host := serverURL.Hostname()
+ if !isLocalhostAddr(host) {
+ return nil, fmt.Errorf("skipping remote kubernetes cluster at %s", host)
+ }
+
+ fmt.Printf("Using kubeconfig context: %s (server: %s)\n", kubeContext, config.Host)
+
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return nil, fmt.Errorf("create kubernetes client: %w", err)
+ }
+
+ // Verify connectivity by listing nodes.
+ _, err = clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{Limit: 1})
+ if err != nil {
+ return nil, fmt.Errorf("kubernetes API unreachable: %w", err)
+ }
+
+ return &KubeWatcher{clientset: clientset}, nil
+}
+
+// ResolveKubeContext returns the current kubeconfig context name.
+// Used to snapshot the context at session start so it stays pinned
+// even if the user switches contexts later.
+func ResolveKubeContext(homeDir string) string {
+ kubeconfigPath := filepath.Join(homeDir, ".kube", "config")
+ loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+ &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
+ &clientcmd.ConfigOverrides{},
+ )
+ rawConfig, err := loader.RawConfig()
+ if err != nil {
+ return ""
+ }
+ return rawConfig.CurrentContext
+}
+
+// RunKubeWatcher retries connecting to k8s within a session. If k8s is
+// unavailable it polls every 30s. When connected, it watches CIDRs until
+// the watch errors out, then retries. Blocks until ctx is cancelled.
+// vmRoutes are CNI routes from the VM, used as fallback for empty spec.podCIDR.
+// kubeContext is the pinned context name for this session.
+func RunKubeWatcher(
+ ctx context.Context,
+ homeDir string,
+ kubeContext string,
+ vmRoutes []string,
+ onAdd func(cidr, name string),
+ onRemove func(cidr, name string),
+) {
+ for {
+ kw, err := NewKubeWatcher(homeDir, kubeContext)
+ if err != nil {
+ fmt.Printf("Kubernetes not available: %v\n", err)
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(30 * time.Second):
+ continue
+ }
+ }
+
+ kw.vmRoutes = vmRoutes
+ fmt.Println("Kubernetes detected, watching pod/service CIDRs")
+ if err := kw.run(ctx, onAdd, onRemove); err != nil {
+ if ctx.Err() != nil {
+ return
+ }
+ fmt.Printf("Kubernetes watcher error: %v\n", err)
+ }
+
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(5 * time.Second):
+ }
+ }
+}
+
+// run watches Nodes and ServiceCIDRs, calling onAdd/onRemove as CIDRs
+// change. Blocks until the context is cancelled or an unrecoverable error
+// occurs.
+func (kw *KubeWatcher) run(
+ ctx context.Context,
+ onAdd func(cidr, name string),
+ onRemove func(cidr, name string),
+) error {
+ var wg sync.WaitGroup
+ errCh := make(chan error, 2)
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if err := kw.watchNodes(ctx, onAdd, onRemove); err != nil {
+ if ctx.Err() == nil {
+ errCh <- fmt.Errorf("node watcher: %w", err)
+ }
+ }
+ }()
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if err := kw.watchServiceCIDRs(ctx, onAdd, onRemove); err != nil {
+ if ctx.Err() == nil {
+ errCh <- fmt.Errorf("service CIDR watcher: %w", err)
+ }
+ }
+ }()
+
+ // Wait for context cancellation or first error.
+ select {
+ case <-ctx.Done():
+ wg.Wait()
+ return ctx.Err()
+ case err := <-errCh:
+ return err
+ }
+}
+
+func (kw *KubeWatcher) watchNodes(
+ ctx context.Context,
+ onAdd func(cidr, name string),
+ onRemove func(cidr, name string),
+) error {
+ // Initial list.
+ nodeList, err := kw.clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Errorf("list nodes: %w", err)
+ }
+
+ knownCIDRs := map[string]string{} // node name -> podCIDR
+
+ for i := range nodeList.Items {
+ node := &nodeList.Items[i]
+ cidr := node.Spec.PodCIDR
+ if cidr != "" && isIPv4CIDR(cidr) {
+ knownCIDRs[node.Name] = cidr
+ onAdd(cidr, "k8s-pod/"+node.Name)
+ }
+ }
+
+ // If no node had spec.podCIDR set (e.g. Docker Desktop), fall back
+ // to CNI routes discovered from the VM's routing table.
+ if len(knownCIDRs) == 0 && len(kw.vmRoutes) > 0 {
+ fmt.Println("No spec.podCIDR on nodes, using VM routes for pod CIDRs")
+ for _, cidr := range kw.vmRoutes {
+ if isIPv4CIDR(cidr) {
+ onAdd(cidr, "k8s-pod/vm-route")
+ }
+ }
+ }
+
+ // Watch from the list's resource version.
+ resourceVersion := nodeList.ResourceVersion
+
+ for {
+ watcher, err := kw.clientset.CoreV1().Nodes().Watch(ctx, metav1.ListOptions{
+ ResourceVersion: resourceVersion,
+ })
+ if err != nil {
+ return fmt.Errorf("watch nodes: %w", err)
+ }
+
+ for event := range watcher.ResultChan() {
+ node, ok := event.Object.(*corev1.Node)
+ if !ok {
+ // Could be a Status object on 410 Gone.
+ if event.Type == watch.Error {
+ watcher.Stop()
+ break
+ }
+ continue
+ }
+
+ switch event.Type {
+ case watch.Added, watch.Modified:
+ newCIDR := node.Spec.PodCIDR
+ if !isIPv4CIDR(newCIDR) {
+ continue
+ }
+ oldCIDR, existed := knownCIDRs[node.Name]
+
+ if newCIDR != oldCIDR {
+ if existed {
+ onRemove(oldCIDR, "k8s-pod/"+node.Name)
+ }
+ knownCIDRs[node.Name] = newCIDR
+ onAdd(newCIDR, "k8s-pod/"+node.Name)
+ }
+
+ case watch.Deleted:
+ if cidr, exists := knownCIDRs[node.Name]; exists {
+ onRemove(cidr, "k8s-pod/"+node.Name)
+ delete(knownCIDRs, node.Name)
+ }
+ }
+ }
+
+ // Watch channel closed - re-list to get fresh resource version
+ // (handles 410 Gone).
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ nodeList, err = kw.clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Errorf("re-list nodes: %w", err)
+ }
+ resourceVersion = nodeList.ResourceVersion
+ }
+}
+
+func (kw *KubeWatcher) watchServiceCIDRs(
+ ctx context.Context,
+ onAdd func(cidr, name string),
+ onRemove func(cidr, name string),
+) error {
+ // Try ServiceCIDR API (k8s 1.31+ beta, 1.33+ GA).
+ cidrs, err := kw.discoverServiceCIDRsFromAPI(ctx)
+ if err == nil {
+ for _, cidr := range cidrs {
+ onAdd(cidr, "k8s-service")
+ }
+ return kw.watchServiceCIDRResource(ctx, onAdd, onRemove)
+ }
+
+ // ServiceCIDR API not available (k8s < 1.31). We can't reliably
+ // discover the service CIDR without it - skip service routing.
+ fmt.Println("ServiceCIDR API not available, skipping service CIDR routing")
+ <-ctx.Done()
+ return ctx.Err()
+}
+
+// discoverServiceCIDRsFromAPI tries the ServiceCIDR resource API.
+func (kw *KubeWatcher) discoverServiceCIDRsFromAPI(ctx context.Context) ([]string, error) {
+ // ServiceCIDR is in networking.k8s.io/v1 (GA in 1.33+) or v1beta1 (1.31-1.32).
+ // Use raw REST client since the typed client may not include ServiceCIDR
+ // depending on the client-go version.
+ data, err := kw.clientset.RESTClient().
+ Get().
+ AbsPath("/apis/networking.k8s.io/v1/servicecidrs").
+ DoRaw(ctx)
+ if err != nil {
+ // Try beta.
+ data, err = kw.clientset.RESTClient().
+ Get().
+ AbsPath("/apis/networking.k8s.io/v1beta1/servicecidrs").
+ DoRaw(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("ServiceCIDR API not available: %w", err)
+ }
+ }
+
+ var result serviceCIDRList
+ if err := json.Unmarshal(data, &result); err != nil {
+ return nil, fmt.Errorf("parse ServiceCIDR response: %w", err)
+ }
+
+ var cidrs []string
+ for _, item := range result.Items {
+ for _, cidr := range item.Spec.CIDRs {
+ if isIPv4CIDR(cidr) {
+ cidrs = append(cidrs, cidr)
+ }
+ }
+ }
+
+ if len(cidrs) == 0 {
+ return nil, fmt.Errorf("no ServiceCIDRs found")
+ }
+
+ return cidrs, nil
+}
+
+// watchServiceCIDRResource watches ServiceCIDR resources for changes.
+func (kw *KubeWatcher) watchServiceCIDRResource(
+ ctx context.Context,
+ onAdd func(cidr, name string),
+ onRemove func(cidr, name string),
+) error {
+ knownCIDRs := map[string][]string{} // resource name -> CIDRs
+
+ // Seed known CIDRs from initial list.
+ data, err := kw.clientset.RESTClient().
+ Get().
+ AbsPath("/apis/networking.k8s.io/v1/servicecidrs").
+ DoRaw(ctx)
+ if err != nil {
+ data, err = kw.clientset.RESTClient().
+ Get().
+ AbsPath("/apis/networking.k8s.io/v1beta1/servicecidrs").
+ DoRaw(ctx)
+ if err != nil {
+ <-ctx.Done()
+ return ctx.Err()
+ }
+ }
+
+ var list serviceCIDRList
+ if err := json.Unmarshal(data, &list); err != nil {
+ <-ctx.Done()
+ return ctx.Err()
+ }
+ for _, item := range list.Items {
+ var v4 []string
+ for _, cidr := range item.Spec.CIDRs {
+ if isIPv4CIDR(cidr) {
+ v4 = append(v4, cidr)
+ }
+ }
+ if len(v4) > 0 {
+ knownCIDRs[item.Metadata.Name] = v4
+ }
+ }
+
+ // Watch for changes. Use the raw API since typed watchers may not
+ // support ServiceCIDR.
+ apiPath := "/apis/networking.k8s.io/v1/servicecidrs"
+ resourceVersion := list.Metadata.ResourceVersion
+
+ for {
+ req := kw.clientset.RESTClient().
+ Get().
+ AbsPath(apiPath).
+ Param("watch", "true").
+ Param("resourceVersion", resourceVersion)
+
+ body, err := req.Stream(ctx)
+ if err != nil {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ // API might not be available, just wait.
+ <-ctx.Done()
+ return ctx.Err()
+ }
+
+ decoder := json.NewDecoder(body)
+ for {
+ var event serviceCIDRWatchEvent
+ if err := decoder.Decode(&event); err != nil {
+ _ = body.Close()
+ break
+ }
+
+ switch event.Type {
+ case "ADDED", "MODIFIED":
+ oldCIDRs := knownCIDRs[event.Object.Metadata.Name]
+ var newCIDRs []string
+ for _, cidr := range event.Object.Spec.CIDRs {
+ if isIPv4CIDR(cidr) {
+ newCIDRs = append(newCIDRs, cidr)
+ }
+ }
+
+ // Remove old CIDRs that are no longer present.
+ for _, old := range oldCIDRs {
+ found := false
+ for _, n := range newCIDRs {
+ if old == n {
+ found = true
+ break
+ }
+ }
+ if !found {
+ onRemove(old, "k8s-service")
+ }
+ }
+
+ // Add new CIDRs.
+ for _, n := range newCIDRs {
+ found := false
+ for _, old := range oldCIDRs {
+ if n == old {
+ found = true
+ break
+ }
+ }
+ if !found {
+ onAdd(n, "k8s-service")
+ }
+ }
+
+ knownCIDRs[event.Object.Metadata.Name] = newCIDRs
+
+ case "DELETED":
+ for _, cidr := range knownCIDRs[event.Object.Metadata.Name] {
+ onRemove(cidr, "k8s-service")
+ }
+ delete(knownCIDRs, event.Object.Metadata.Name)
+ }
+ }
+
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ // Re-list on stream close (410 Gone or disconnect).
+ data, err = kw.clientset.RESTClient().
+ Get().
+ AbsPath(apiPath).
+ DoRaw(ctx)
+ if err != nil {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ <-ctx.Done()
+ return ctx.Err()
+ }
+ if err := json.Unmarshal(data, &list); err != nil {
+ <-ctx.Done()
+ return ctx.Err()
+ }
+ resourceVersion = list.Metadata.ResourceVersion
+ }
+}
+
+// isLocalhostAddr returns true if the given host (IP or hostname) resolves
+// to a loopback address. Handles cases like Docker Desktop's
+// "kubernetes.docker.internal" which resolves to 127.0.0.1.
+func isLocalhostAddr(host string) bool {
+ addrs, err := net.LookupHost(host)
+ if err != nil {
+ return false
+ }
+ for _, addr := range addrs {
+ if ip := net.ParseIP(addr); ip != nil && ip.IsLoopback() {
+ return true
+ }
+ }
+ return false
+}
+
+// Minimal types for ServiceCIDR API responses.
+
+type serviceCIDRList struct {
+ Metadata struct {
+ ResourceVersion string `json:"resourceVersion"`
+ } `json:"metadata"`
+ Items []serviceCIDR `json:"items"`
+}
+
+type serviceCIDR struct {
+ Metadata struct {
+ Name string `json:"name"`
+ ResourceVersion string `json:"resourceVersion"`
+ } `json:"metadata"`
+ Spec struct {
+ CIDRs []string `json:"cidrs"`
+ } `json:"spec"`
+}
+
+type serviceCIDRWatchEvent struct {
+ Type string `json:"type"`
+ Object serviceCIDR `json:"object"`
+}
diff --git a/networkwatcher/net.go b/networkwatcher/net.go
new file mode 100644
index 0000000..997f5b4
--- /dev/null
+++ b/networkwatcher/net.go
@@ -0,0 +1,12 @@
+//go:build darwin
+
+package networkwatcher
+
+import "net"
+
+// isIPv4CIDR returns true if the CIDR is IPv4. We only add IPv4 routes
+// since the macOS route command uses -inet.
+func isIPv4CIDR(cidr string) bool {
+ ip, _, err := net.ParseCIDR(cidr)
+ return err == nil && ip.To4() != nil
+}
diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh
index dd046a0..eb00fc1 100755
--- a/scripts/e2e-test.sh
+++ b/scripts/e2e-test.sh
@@ -22,6 +22,8 @@ cleanup() {
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
docker rm -f "${CONTAINER_NAME}-internal" 2>/dev/null || true
docker network rm "$INTERNAL_NETWORK_NAME" 2>/dev/null || true
+ kubectl delete pod e2e-test 2>/dev/null || true
+ kubectl delete svc e2e-test-svc 2>/dev/null || true
}
trap cleanup EXIT
cleanup
@@ -79,3 +81,54 @@ else
echo "FAIL: Could not reach internal container at $INTERNAL_CONTAINER_IP"
exit 1
fi
+
+# --- Test 3: Kubernetes pod connectivity (if k8s enabled) ---
+# Uses the active kubeconfig context (same as the app does).
+
+if kubectl cluster-info >/dev/null 2>&1; then
+ echo ""
+ echo "=== Test 3: Kubernetes pod connectivity ==="
+
+ KUBE_CONTEXT=$(kubectl config current-context)
+ echo "Using kubeconfig context: $KUBE_CONTEXT"
+
+ echo "Deploying test pod..."
+ kubectl run e2e-test --image=nginx:alpine --restart=Never >/dev/null
+ kubectl wait --for=condition=ready pod/e2e-test --timeout=60s >/dev/null
+
+ POD_IP=$(kubectl get pod e2e-test -o jsonpath='{.status.podIP}')
+
+ echo "Pod IP: $POD_IP"
+ echo "Attempting to reach pod directly..."
+ if curl -sf --connect-timeout 5 "http://$POD_IP" >/dev/null; then
+ echo "PASS: Successfully reached k8s pod at $POD_IP"
+ else
+ echo "FAIL: Could not reach k8s pod at $POD_IP"
+ exit 1
+ fi
+
+ # --- Test 4: Kubernetes service connectivity ---
+
+ echo ""
+ echo "=== Test 4: Kubernetes service connectivity ==="
+
+ kubectl expose pod e2e-test --port=80 --name=e2e-test-svc >/dev/null
+ SVC_IP=$(kubectl get svc e2e-test-svc -o jsonpath='{.spec.clusterIP}')
+
+ echo "Service ClusterIP: $SVC_IP"
+ echo "Waiting for service to become reachable..."
+ for i in $(seq 1 10); do
+ if curl -sf --connect-timeout 2 "http://$SVC_IP" >/dev/null 2>&1; then
+ echo "PASS: Successfully reached k8s service at $SVC_IP"
+ break
+ fi
+ if [ "$i" -eq 10 ]; then
+ echo "FAIL: Could not reach k8s service at $SVC_IP"
+ exit 1
+ fi
+ sleep 1
+ done
+else
+ echo ""
+ echo "=== Test 3: Kubernetes (SKIPPED - k8s not enabled) ==="
+fi