1 | 1 | | # cdebug - a swiss army knife of container debugging (WIP) |
2 | 2 | | |
3 | | - | TODO: Add table command (exec, port-forward, export, explore) x runtime (docker, containerd, k8s, etc). |
| 3 | + | With this tool you can: |
| 4 | + | |
| 5 | + | - Troubleshoot containers lacking shell and/or debugging tools |
| 6 | + | - Forward unpublished or even localhost ports to your host system |
| 7 | + | - Expose endpoints from the host system to containers & Kubernetes networks |
| 8 | + | - Handily export image's and/or container's filesystem to local folders |
| 9 | + | - and more :) |
4 | 10 | | |
5 | | - | A handy way to troubleshoot containers lacking a shell and/or debugging tools |
6 | | - | (e.g, scratch, slim, or distroless): |
| 11 | + | The following _commands_ x _runtimes_ are supported: |
| 12 | + | |
| 13 | + | | | Docker | Containerd | Kubernetes | Kubernetes CRI | runc | |
| 14 | + | | :--- | :---: | :---: | :---: | :---: | :---: | |
| 15 | + | | `exec` | ✅ | 🛠️ | - | - | - | |
| 16 | + | | `port-forward` local | ✅ | - | - | - | - | |
| 17 | + | | `port-forward` remote | 🛠️ | - | 🛠️ | - | - | |
| 18 | + | | `export` | - | - | - | - | - | |
| 19 | + | |
| 20 | + | ## Installation |
| 21 | + | |
| 22 | + | It's a statically linked Go binary, so you know the drill: |
7 | 23 | | |
8 | 24 | | ```sh |
9 | | - | # Simple (busybox) |
10 | | - | $ cdebug exec -it <target-container> |
| 25 | + | GOOS=linux |
| 26 | + | GOARCH=amd64 |
| 27 | + | |
| 28 | + | curl -Ls https://github.com/iximiuz/cdebug/releases/latest/download/cdebug_${GOOS}_${GOARCH}.tar.gz | tar xvz |
| 29 | + | |
| 30 | + | sudo mv cdebug /usr/local/bin |
| 31 | + | ``` |
| 32 | + | |
| 33 | + | At the moment, the following systems are (kinda sorta) supported: |
| 34 | + | |
| 35 | + | - linux/amd64 |
| 36 | + | - darwin/amd64 |
| 37 | + | - darwin/arm64 |
| 38 | + | |
| 39 | + | ## Commands |
| 40 | + | |
| 41 | + | ### cdebug exec |
11 | 42 | | |
12 | | - | # Advanced (shell + ps + vim) |
13 | | - | $ cdebug exec -it --image nixery.dev/shell/ps/vim <target-container> |
| 43 | + | Run an interactive shell in a scratch, slim, or distroless container, with ease: |
| 44 | + | |
| 45 | + | ```sh |
| 46 | + | cdebug exec -it <target-container> |
14 | 47 | | ``` |
15 | 48 | | |
16 | | - | The `cdebug exec` command is some sort of crossbreeding of `docker exec` and `kubectl debug` commands. |
| 49 | + | The `cdebug exec` command is a crossbreeding of `docker exec` and `kubectl debug` commands. |
17 | 50 | | You point the tool at a running container, say what toolkit image to use, and it starts |
18 | | - | a debugging "sidecar" container that _feels_ like a `docker exec` session into the target container: |
| 51 | + | a debugging "sidecar" container that _feels_ like a `docker exec` session to the target container: |
19 | 52 | | |
20 | 53 | | - The root filesystem of the debugger **_is_** the root filesystem of the target container. |
21 | 54 | | - The target container isn't recreated and/or restarted. |
22 | 55 | | - No extra volumes or copying of debugging tools is needed. |
23 | 56 | | - The debugging tools **_are_** available in the target container. |
24 | 57 | | |
25 | | - | Currently supported toolkit images: |
| 58 | + | By default, the `busybox:latest` image is used for the debugger sidecar, but you can override it |
| 59 | + | with the `--image` flag. Combining this with the superpower of Nix and [Nixery](https://nixery.dev/), |
| 60 | + | you can get all your favorite tools by simply listing them in the image name: |
| 61 | + | |
| 62 | + | ``` |
| 63 | + | cdebug exec -it --image nixery.dev/shell/ps/vim/tshark <target-container> |
| 64 | + | ``` |
| 65 | + | |
| 66 | + | <details> |
| 67 | + | <summary>How it works</summary> |
| 68 | + | |
| 69 | + | The technique is based on the ideas from this [blog post](https://iximiuz.com/en/posts/docker-debug-slim-containers). |
| 70 | + | Oversimplifying, the debugger container is started like: |
| 71 | + | |
| 72 | + | ```sh |
| 73 | + | docker run [-it] \ |
| 74 | + | --network container:<target> \ |
| 75 | + | --pid container:<target> \ |
| 76 | + | --uts container:<target> \ |
| 77 | + | <toolkit-image> |
| 78 | + | sh -c <<EOF |
| 79 | + | ln -s /proc/$$/root/bin/ /proc/1/root/.cdebug |
| 80 | + | |
| 81 | + | export PATH=$PATH:/.cdebug |
| 82 | + | chroot /proc/1/root sh |
| 83 | + | EOF |
| 84 | + | ``` |
| 85 | + | |
| 86 | + | The secret sauce is the symlink + PATH modification + chroot-ing. |
| 87 | + | |
| 88 | + | </details> |
| 89 | + | |
| 90 | + | ### cdebug port-forward |
| 91 | + | |
| 92 | + | Forward local ports to containers and vice versa. This command is another crossbreeding - |
| 93 | + | this time it's `kubectl port-forward` and `ssh -L|-R`. |
| 94 | + | |
| 95 | + | Currently, only local port forwarding (`cdebug port-forward -L`) is supported, |
| 96 | + | but remote port forwarding is under active development. |
26 | 97 | | |
27 | | - | - 🧰 `busybox` - a good default choice |
28 | | - | - 🧙 `nixery.dev/shell/...` - [a very powerful way to assemble images on the fly](https://nixery.dev/). |
| 98 | + | Local port forwarding use cases (works for Docker Desktop too!): |
29 | 99 | | |
30 | | - | Supported runtimes: |
| 100 | + | - Publish "unpublished" port 80 to a random port on the host: `cdebug port-forward <target> -L 80` |
| 101 | + | - Expose container's localhost to the host system: `cdebug port-forward <target> -L 127.0.0.1:5432` |
| 102 | + | - Proxy local traffic to a remote host via the target: `cdebug port-forward <target> -L <LOCAL_HOST>:<LOCAL_PORT>:<REMOTE_HOST>:<REMOTE_PORT>` |
| 103 | + | - 🛠️ Expose a Kubernetes service to the host system: `cdebug port-forward <target> -L 8888:my.svc.cluster.local:443` |
| 104 | + | |
| 105 | + | Remote port forwarding use cases: |
| 106 | + | |
| 107 | + | - Start a container/Pod forwarding traffic destined to its `<IP>:<port>` to a non-cluster endpoint reachable from the host system. |
| 108 | + | - ... |
31 | 109 | | |
32 | | - | - Docker (via the socket file) |
33 | | - | - containerd (via the socket file) - coming soon |
34 | | - | - Kubernetes CRI (via the CRI gRPC API) - coming later |
35 | | - | - Kubernetes (via the API server) - coming later |
36 | | - | - runc or alike (via directly invoking the CLI) - coming later. |
| 110 | + | <details> |
| 111 | + | <summary>How it works</summary> |
37 | 112 | | |
38 | | - | ## Installation |
| 113 | + | **Local port forwarding** is implemented by starting an extra forwarder container in the |
| 114 | + | target's network and publishing its ports to the host using the standard means (e.g., |
| 115 | + | `docker run --publish`). The forwarder container itself runs something like: |
39 | 116 | | |
40 | | - | It's a statically linked Go binary, so you know the drill: |
| 117 | + | `socat TCP-LISTEN:<REMOTE_PORT>,fork TCP-CONNECT:<REMOTE_HOST>:<REMOTE_PORT>` |
41 | 118 | | |
42 | | - | ```sh |
43 | | - | GOOS=linux |
44 | | - | GOARCH=amd64 |
| 119 | + | If the _REMOTE_HOST_ doesn't belong to the target or it's the target's localhost, |
| 120 | + | an extra sidecar container is started in the target's network namespace with another |
| 121 | + | socat forwarding traffic from the target public interface to `REMOTE_HOST:REMOTE_PORT`. |
45 | 122 | | |
46 | | - | curl -Ls https://github.com/iximiuz/cdebug/releases/latest/download/cdebug_${GOOS}_${GOARCH}.tar.gz | tar xvz |
| 123 | + | **Remote port forwarding** will use similar tricks but combined with more advanced |
| 124 | + | reverse tunneling. |
47 | 125 | | |
48 | | - | sudo mv cdebug /usr/local/bin |
49 | | - | ``` |
| 126 | + | </details> |
50 | 127 | | |
51 | | - | At the moment, the following targets are (kinda sorta) supported: |
| 128 | + | ## Demos |
52 | 129 | | |
53 | | - | - linux/amd64 |
54 | | - | - darwin/amd64 |
55 | | - | - darwin/arm64 |
| 130 | + | Below are a few popular scenarios formatted as reproducible demos. |
56 | 131 | | |
57 | | - | ## Demo 1: An interactive shell with busybox |
| 132 | + | ### A simple interactive shell to a distroless container |
58 | 133 | | |
59 | 134 | | First, a target container is needed. Let's use a distroless nodejs image for that: |
60 | 135 | | |
61 | 136 | | ```sh |
62 | | - | docker run -d --rm \ |
| 137 | + | $ docker run -d --rm \ |
63 | 138 | | --name my-distroless gcr.io/distroless/nodejs \ |
64 | 139 | | -e 'setTimeout(() => console.log("Done"), 99999999)' |
65 | 140 | | ``` |
| skipped 1 lines |
67 | 142 | | Now, let's start an interactive shell (using busybox) into the above container: |
68 | 143 | | |
69 | 144 | | ```sh |
70 | | - | cdebug exec -it my-distroless |
| 145 | + | $ cdebug exec -it my-distroless |
71 | 146 | | ``` |
72 | 147 | | |
73 | 148 | | Exploring the filesystem shows that it's a rootfs of the nodejs container: |
| skipped 26 lines |
100 | 175 | | The process tree of the debugger container is the process tree of the target: |
101 | 176 | | |
102 | 177 | | ```sh |
103 | | - | / # ps auxf |
| 178 | + | / $# ps auxf |
104 | 179 | | PID USER TIME COMMAND |
105 | 180 | | 1 root 0:00 /nodejs/bin/node -e setTimeout(() => console.log("Done"), |
106 | 181 | | 13 root 0:00 sh -c set -euo pipefail sleep 999999999 & SANDBOX_PID=$! |
| skipped 4 lines |
111 | 186 | | 45 root 0:00 ps auxf |
112 | 187 | | ``` |
113 | 188 | | |
114 | | - | ## Demo 2: An interactive shell with code editor |
| 189 | + | ### An interactive shell with code editor (vim) |
115 | 190 | | |
116 | 191 | | If the tools provided by busybox aren't enough, you can bring your own tools with |
117 | 192 | | a ~~little~~ huge help of the [nixery](https://nixery.dev/) project: |
118 | 193 | | |
119 | 194 | | ```sh |
120 | | - | cdebug exec -it --image nixery.dev/shell/vim my-distroless |
| 195 | + | $ cdebug exec -it --image nixery.dev/shell/vim my-distroless |
121 | 196 | | ``` |
122 | 197 | | |
123 | | - | ## Demo 3: An interactive shell with tshark and other advanced tools |
| 198 | + | ### An interactive shell with tshark and other advanced tools |
124 | 199 | | |
125 | 200 | | Even more powerful exammple: |
126 | 201 | | |
127 | 202 | | ```sh |
128 | | - | cdebug exec -it --image nixery.dev/shell/ps/findutils/tshark my-distroless |
| 203 | + | $ cdebug exec -it --image nixery.dev/shell/ps/findutils/tshark my-distroless |
129 | 204 | | ``` |
130 | 205 | | |
131 | | - | ## How it works |
| 206 | + | ### Publish "forgotten" port |
| 207 | + | |
| 208 | + | Start an nginx container but don't expose its port 80: |
| 209 | + | |
| 210 | + | ```sh |
| 211 | + | $ docker run -d --name nginx-1 nginx:1.23 |
| 212 | + | ``` |
132 | 213 | | |
133 | | - | The technique is based on the ideas from this [blog post](https://iximiuz.com/en/posts/docker-debug-slim-containers). |
134 | | - | Oversimplifying, the debugger container is started like: |
| 214 | + | Forward local port 8080 to the nginx's 80: |
135 | 215 | | |
136 | 216 | | ```sh |
137 | | - | docker run [-it] \ |
138 | | - | --network container:<target> \ |
139 | | - | --pid container:<target> \ |
140 | | - | --uts container:<target> \ |
141 | | - | <toolkit-image> |
142 | | - | sh -c <<EOF |
143 | | - | ln -s /proc/$$/root/bin/ /proc/1/root/.cdebug |
| 217 | + | $ cdebug port-forward nginx-1 -L 8080:80 |
| 218 | + | $ curl localhost:8080 |
| 219 | + | ``` |
| 220 | + | |
| 221 | + | ### Expose localhost's ports |
144 | 222 | | |
145 | | - | export PATH=$PATH:/.cdebug |
146 | | - | chroot /proc/1/root sh |
147 | | - | EOF |
| 223 | + | Start a containerized service that listens only on its localhost: |
| 224 | + | |
| 225 | + | ```sh |
| 226 | + | $ docker run -d --name svc-1 python:3-alpine python3 -m 'http.server' -b 127.0.0.1 8888 |
148 | 227 | | ``` |
149 | 228 | | |
150 | | - | The secret sauce is the symlink + PATH modification + chroot-ing. |
| 229 | + | Tap into the above service: |
| 230 | + | |
| 231 | + | ```sh |
| 232 | + | $ cdebug port-forward svc-1 -L 127.0.0.1:8888 |
| 233 | + | Pulling forwarder image... |
| 234 | + | latest: Pulling from shell/socat |
| 235 | + | Digest: sha256:b43b6cf8d22615616b13c744b8ff525f5f6c0ca6c11b37fa3832a951ebb3c20c |
| 236 | + | Status: Image is up to date for nixery.dev/shell/socat:latest |
| 237 | + | Forwarding 127.0.0.1:49176 to 127.0.0.1:8888 through 172.17.0.4:34128 |
| 238 | + | |
| 239 | + | $ curl localhost:49176 |
| 240 | + | <!DOCTYPE HTML> |
| 241 | + | <html lang="en"> |
| 242 | + | <head> |
| 243 | + | ... |
| 244 | + | ``` |
151 | 245 | | |
152 | 246 | | ## F.A.Q |
153 | 247 | | |
| skipped 12 lines |
166 | 260 | | |
167 | 261 | | ## TODO: |
168 | 262 | | |
169 | | - | - Make exec accept (partial) container IDs (only names are supported at the moment) |
170 | | - | - Terminal resizing ([example](https://github.com/docker/cli/blob/110c4d92b883357c9fb3edc344c4fbec5f77896f/cli/command/container/tty.go#L71)) |
171 | 263 | | - More `exec` flags (like in `docker run`): `--cap-add`, `--cap-drop`, `--env`, `--volume`, etc. |
172 | 264 | | - Helper command(s) suggesting nix(ery) packages |
| 265 | + | - Non-docker runtimes (containerd, runc, k8s) |
173 | 266 | | - E2E Tests |
174 | | - | - Cross-platform builds + goreleaser |
175 | | - | - Non-docker runtimes (containerd, runc, k8s) |
176 | 267 | | |
177 | 268 | | ## Contributions |
178 | 269 | | |
| skipped 2 lines |