惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
I
InfoQ
The Register - Security
The Register - Security
L
LangChain Blog
H
Help Net Security
The GitHub Blog
The GitHub Blog
S
Schneier on Security
博客园 - 【当耐特】
W
WeLiveSecurity
Attack and Defense Labs
Attack and Defense Labs
IT之家
IT之家
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Google DeepMind News
Google DeepMind News
The Cloudflare Blog
H
Heimdal Security Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
雷峰网
雷峰网
N
Netflix TechBlog - Medium
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Exploit Database - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
V
Visual Studio Blog
博客园 - 聂微东
PCI Perspectives
PCI Perspectives
Last Week in AI
Last Week in AI
A
Arctic Wolf
宝玉的分享
宝玉的分享
T
The Blog of Author Tim Ferriss
S
Secure Thoughts
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
SegmentFault 最新的问题
SecWiki News
SecWiki News
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
Schneier on Security
Schneier on Security
P
Proofpoint News Feed
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
AI
AI
Engineering at Meta
Engineering at Meta

Ivan on Containers, Kubernetes, and Server-Side

A grounded take on agentic coding for production environments Server-Side Playgrounds Reimagined: Build, Boot, and Network Your Own Virtual Labs [not a] Kubernetes 101 - Pods, Deployments, and Services As an Attempt To Automate Age-Old Infra Patterns JavaScript or TypeScript? How To Benefit From the Dichotomy On Software Design... and Good Writing Building a Firecracker-Powered Course Platform To Learn Docker and Kubernetes How To Publish a Port of a Running Container A Visual Guide to SSH Tunnels: Local and Remote Port Forwarding Debugging Containers Like a Pro Docker: How To Debug Distroless And Slim Containers How To Extract Container Image Filesystem Using Docker | iximiuz Labs In Pursuit of Better Container Images: Alpine, Distroless, Apko, Chisel, DockerSlim, oh my! How To Start Programming In Go: Advice For Fellow DevOps Engineers Kubernetes Ephemeral Containers and kubectl debug Command How To Develop Kubernetes CLIs Like a Pro Docker Container Commands Explained: Understand, Don't Memorize | iximiuz Labs Learning Docker with Docker - Toying With DinD For Fun And Profit How To Extend Kubernetes API - Kubernetes vs. Django The Influence of Plumbing on Programming How To Call Kubernetes API from Go - Types and Common Machinery How To Call Kubernetes API using Simple HTTP Client Kubernetes API Basics - Resources, Kinds, and Objects OpenFaaS - Run Containerized Functions On Your Own Terms Learning Containers From The Bottom Up Docker Containers vs. Kubernetes Pods - Taking a Deeper Look | iximiuz Labs Learn-by-Doing Platforms for Dev, DevOps, and SRE Folks How HTTP Keep-Alive can cause TCP race condition How to Work with Container Images Using ctr | iximiuz Labs Multiple Containers, Same Port, no Reverse Proxy... Exploring Go net/http Package - On How Not To Set Socket Options Disposable Local Development Environments with Vagrant, Docker, and Arkade DevOps, SRE, and Platform Engineering My Choice of Programming Languages Prometheus Is Not a TSDB How to learn PromQL with Prometheus Playground Prometheus Cheat Sheet - Basics (Metrics, Labels, Time Series, Scraping) Rust - Writing Parsers With nom Parser Combinator Framework pq - parse and query log files as time series Prometheus Cheat Sheet - Moving Average, Max, Min, etc (Aggregation Over Time) Prometheus Cheat Sheet - How to Join Multiple Metrics (Vector Matching) The Need For Slimmer Containers Understanding Rust Privacy and Visibility Model Bridge vs. Switch: Takeaways from a Real Data Center Tour | iximiuz Labs From LAN to VXLAN: Networking Basics for Non-Network Engineers | iximiuz Labs KiND - How I Wasted a Day Loading Local Docker Images Go, HTTP handlers, panic, and deadlocks Exploring Kubernetes Operator Pattern Making Sense Out Of Cloud Native Buzz Service Discovery in Kubernetes: Combining the Best of Two Worlds API Developers Never REST How Container Networking Works: Building a Bridge Network From Scratch | iximiuz Labs Traefik: canary deployments with weighted load balancing Service Proxy, Pod, Sidecar, oh my! You Need Containers To Build Images You Don't Need an Image To Run a Container Not Every Container Has an Operating System Inside Working with container images in Go Master Go While Learning Containers Implementing Container Runtime Shim: Interactive Containers How to use Flask with gevent (uWSGI and Gunicorn editions) My 10 Years of Programming Experience Implementing Container Runtime Shim: First Code Implementing Container Runtime Shim: runc Kubernetes Repository On Flame Dealing with process termination in Linux (with Rust examples) conman - [the] Container Manager: Inception Journey From Containerization To Orchestration And Beyond Linux PTY - How docker attach and docker exec Commands Work Inside Illustrated introduction to Linux iptables From Docker Container to Bootable Linux Disk Image Пишем свой веб-сервер на Python: протокол HTTP 9001 способ создать веб-сервер на Python Explaining async/await in 200 lines of code Explaining event loop in 100 lines of code Save the day with gevent Пишем свой веб-сервер на Python: процессы, потоки и асинхронный I/O Truly optional scalar types in protobuf3 (with Go examples) Node.js Writable streams distilled Node.js Readable streams distilled How to on starting processes (mostly in Linux) Дайджест интересных ссылок – Июль 2016 Пишем свой веб-сервер на Python: сокеты Наследование в JavaScript Мастерить!
What Actually Happens When You Publish a Container Port
Ivan Velichko · 2022-11-27 · via Ivan on Containers, Kubernetes, and Server-Side

If you're dealing with containers regularly, you've probably published ports many, many times already. A typical need for publishing arises like this: you're developing a web app, locally but in a container, and you want to test it using your laptop's browser. The next thing you do is docker run -p 8080:80 app and then open localhost:8080 in the browser. Easy-peasy!

But have you ever wondered what actually happens when you ask Docker to publish a port?

In this article, I'll try to connect the dots between port publishing, the term apparently coined by Docker, and a more traditional networking technique called port forwarding. I'll also take a look under the hood of different "single-host" container runtimes (Docker Engine, Docker Desktop, containerd, nerdclt, and Lima) to compare the port publishing implementations and capabilities.

As always, the ultimate goal is to gain a deeper understanding of the technology and get closer to becoming a power user of containers. Let the diving begin!

What is Port Forwarding aka Port Mapping

Ports aren't a thing on their own. When we say port we usually refer to a certain [Internet] socket address defined by a pair of IP address and port number.

Sockets are meant to represent the participants of a network data exchange. So, port forwarding or, as it's also called, port mapping is just a fancy way to say that data addressed to one socket is redirected to another by an intermediary (for instance, by a network router or a proxy process).

Port forwarding is a form of Network Address Translation.

There are two common ways to redirect network data (hence, implement port forwarding):

  • By sneakily modifying the destination address of packets.
  • By explicitly putting a proxy between the client and the server.

In the first case, packets originally destined to some <addr1:port1> get modified (or translated) on their way and instead delivered to <addr2:port2>. Often, this is done by a Linux kernel component such as netfilter configured with a bunch of iptables rules.

In the second case, the client-facing socket is actually maintained by an L4+ proxy process that reads the incoming data and re-sends it to the final destination. I.e., no packets are actually translated, and only the payload data is proxied.

While there is a subtle difference between the two methods, most of the time, they are interchangeable:

Types of port forwarding.

But why the rant?

Because I think that understanding the idea behind port forwarding and the most common ways to implement it is vital for the in-depth discussion of why and how to publish container ports.

Containers and Port Forwarding

Normally, a container has its own network stack and IP address. Being able to access that IP address allows you to call any services running inside of the container (assuming they listen on the container's public interface).

For instance, with Docker Engine on Linux (not to be confused with Docker Desktop that also supports Linux since May 2022), you can create an nginx container and curl it from the host:

$ docker run -d --name nginx-1 nginx

$ CONT_IP=$(
  docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx-1
)

$ curl $CONT_IP:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

You can even open the above address in your favorite browser if you're brave enough to run containers on your main system. This is a handy capability, and I often rely on it while performing some ad-hoc tasks. But is it any good for real use?

Well, I'm aware of at least two significant "inconveniences" of accessing Docker containers by their IPs:

  • Container IPs are assigned dynamically, and the container's restart (or rather recreation) may cause its IP address to change.
  • By default, container IPs are routable only from the inside of the host and inaccessible from the outside world.

And that's where port publishing comes in handy!

In its simple form, the docker run --publish 8080:80 nginx command creates a regular port forwarding from the host's 0.0.0.0:8080 to the container's $CONT_IP:80.

Since both sides of the mapping reside on one Linux host, such a forwarding can be implemented with just a couple of iptables rules:

$ docker run -d -p 8080:80 --name nginx-1 nginx

$ sudo iptables -t nat -L
...

Chain POSTROUTING (policy ACCEPT)
target     prot opt source          destination
...
MASQUERADE  tcp  --  172.17.0.2      172.17.0.2     tcp dpt:http

Chain DOCKER (2 references)
target     prot opt source          destination
...
DNAT       tcp  --  anywhere        anywhere        tcp dpt:8080 to:172.17.0.2:80

Thus, it seems that in the case of Docker Engine, there is not much difference between port publishing and good old kernel space port forwarding. The only "extra" functionality that Docker adds on top is the automatic update of the destination part of the mapping in the case of the container's IP address change.

Publishing container ports with Docker Engine.

Port Publishing and Different Network Drivers

The experiment in the section above was done using the default (bridge) network driver. Clearly, bridge-based networks support and benefit from port publishing. However, Docker has a few other network types. In Docker's own words:

The type of network a container uses, whether it is a bridge, an overlay, a macvlan network, or a custom network plugin, is transparent from within the container. From the container's point of view, it has a network interface with an IP address, a gateway, a routing table, DNS services, and other networking details (assuming the container is not using the none network driver).

But does the type of network affects the port publishing capabilities?

Let's review every network type one by one:

  • none network means there is no external network interface (hence, no IP address), and only the loopback exists in the container. Well, no IP address - no port forwarding.
  • host network means the container doesn't have its own network stack. Instead, it reuses the host's one. You still can forward ports, but you'll have to do it by yourself (for instance, by manually creating iptables rules) because, from Docker's standpoint, there is very little need for it.
  • container:<other> network means the current container reuses the network mode of another container. In particular, it makes the container inherit the already published ports (if any). But you cannot publish your own ports in that mode.
  • overlay network seems to be an echo of the Swarm days. Apparently, you can do some sort of port publishing there, but for "services" and not individual containers. Don't have much experience with this mode, so won't speculate.
  • ipvlan and macvlan network modes don't support port forwarding because they probably assume that IP addresses assigned to containers are directly routable and fall out of basic Docker's responsibilities.

To summarize, the only network type you'll want to use port publishing with is bridge. But this is the default and probably the most widespread type, so this limitation doesn't diminish the value of port publishing much.

Docker Desktop and Port Publishing

Surprisingly or not, Docker Desktop's behavior differs from Docker Engine's when it comes to container networking. Here is what happens when I try to access a container by its IP address on my MacBook:

$ docker run -d --name nginx-1 nginx

$ CONT_IP=$(
  docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx-1
)

$ ping $CONT_IP
PING 172.17.0.3 (172.17.0.3): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
^C
--- 172.17.0.3 ping statistics ---
3 packets transmitted, 0 packets received, 100.0% packet loss

Well, it kind of makes sense. Docker containers require a Linux kernel. So, Docker Desktop for Mac (and likely for Windows and Linux as well) sneakily starts a full-blown virtual machine running a custom Linux distro with pre-installed Docker Engine. Therefore, the above 172.17.0.3 address is an address from a bridge network that exists only inside that virtual machine. Evidently, it's not routable from the host system (macOS, in my case).

Publishing container ports with Docker Engine.

But nevertheless, port publishing works in Docker Desktop!

$ docker run -d -p 8080:80 --name nginx-1 nginx

$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

The implementation of port publishing in Docker Desktop had been remaining puzzling for me until relatively recently. But then David Scott published (no pun intended) a great article explaining the internals of Docker Desktop networking. In particular, it sheds some light on how port forwarding is done:

To cut a long story short, in the case of Docker Desktop, it's a combination of user space-based and traditional iptables-based port forwarding. When you start a container that publishes a port, a proxy process on the host (e.g, on macOS) opens a real socket using the specified IP address and port. You can pinpoint this process with lsof:

$ docker run -d -p 8080:80 --name nginx-1 nginx

$ sudo lsof -i -P | grep LISTEN | grep :8080
com.docke... 24294   iximiuz   76u  IPv6 0x89404e558d90602b    0t0   TCP *:8080 (LISTEN)

$ ps 24294
  PID   TT  STAT      TIME COMMAND
24294   ??  S     38:09.83 /Applications/Docker.app/Contents/MacOS/com.docker.backend -watchdog -native-api

Inside the VM, the implementation of port publishing seems to be no different from the one described in the Docker Engine section of this article. It can be verified using a privileged container running in the VM's network namespace (the --net host flag because for Docker Engine the VM is the host):

$ docker run --privileged --net host -it --rm alpine
/ $# apk add iptables
/ $# iptables -t nat -L
<typical Docker Engine port forwarding iptables rules>

And the mysterious vpnkit-bridge (part of the VPNKit project) helps cross the borders between the host and the VM systems.

Summarizing, when you publish a port in Docker Desktop:

  • The com.docker.backend process on the host system starts acting as a user space proxy and opens a socket using the specified address.
  • This socket can be used by anyone on the host (or with access to the host's network) to send data to the container.
  • When data appears on the socket, it gets forwarded by vpnkit-bridge to the VM's external network interface.
  • Inside the VM, the container's port has already been published to that interface using the "standard" for Docker Engine iptables-based technique.

How To Publish Ports With containerd

It's no secret that Docker heavily relies on containerd for lower-level container management. But the exact separation of concerns may not always be clear. For instance, publishing ports always sounded pretty low-level to me. But it turns out that containerd cannot publish ports on its own!

Similarly to docker run --name nginx-1 nginx:latest, you can run a container directly in containerd using ctr run docker.io/library/nginx:latest nginx-1. But there is no equivalent for the -p flag. Like at all 🙈

So, port publishing is evidently one of Docker's own responsibilities. But there still might be a way...

Port Forwarding With Mighty nerdctl

nerdctl is a popular CLI client for containerd. Unlike the minimalistic ctr, nerdctl tries to be feature-rich and Docker-compatible (meaning it mimics the docker CLI commands). But if it's Docker-compatible, it must be implementing port publishing!

A quick check of the nerdctl source code revealed (cmd/nerdctl/run.go, ocihook/ocihook.go) that it relies on an OCI Runtime lifecycle hook (on "CreateRuntime" to be precise) to set up some extra networking capabilities of to-be-started containers. But nerdctl doesn't do the actual networking configuration on its own. Instead, it uses the helper containerd/go-cni package that, in particular, allows to configure port mapping (most likely also delegating the actual job to the corresponding portmap CNI plugin).

A tiny experiment to verify the above findings shows that the portmap CNI plugin uses the already familiar iptables trick to implement port forwarding. So, indeed, nerdctl stays very close to what Docker Engine does in this particular case:

$ sudo nerdctl run -d --name nerd-nginx-1 -p 8080:80 nginx

$ sudo nerdctl inspect \
  -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  nerd-nginx-1
10.4.0.4

$ sudo iptables -t nat -L | grep '10.4.0.4'
CNI-6603d34f605da0cf8e0a0934  all  --  10.4.0.4    anywhere             
DNAT       tcp  --  anywhere           anywhere    tcp dpt:http-alt to:10.4.0.4:80

How Lima Implements Port Forwarding

The Lima project is an attempt to pack containerd with nerdctl and make the bundle run easily on macOS. Expectedly, this should involve running a virtual machine:

Lima launches Linux virtual machines with automatic file sharing and port forwarding (similar to WSL2), and containerd.

Thus, architecturally, Lima is somewhat similar to Docker Desktop.

$ limactl start

$ nerdctl.lima run -d --name lima-nginx-1 -p 8080:80 nginx
$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Ok, publishing container ports works in Lima. But how is it implemented?

$ sudo lsof -i -P | grep LISTEN | grep :8080
ssh    86663    iximiuz   15u  IPv4 0xcf4f91a867ed7809    0t0  TCP localhost:8080 (LISTEN)

$ ps 86663
  PID   TT  STAT      TIME COMMAND
86663   ??  Ss     0:00.02 ssh: /Users/iximiuz/.lima/default/ssh.sock [mux]

And I absolutely love what I found there! Lima uses good old SSH tunnels to forward ports between the host system (macOS) and services in the VM.

Inside the VM, it's just a regular container (started by nerdctl) with its port published to 0.0.0.0:

$ limactl shell default

$ nerdctl ps -a
CONTAINER ID  IMAGE         COMMAND                 CREATED        STATUS  PORTS                 NAMES
6f2aa0304e4c  nginx:latest  "/docker-entrypoint.…"  5 seconds ago  Up      0.0.0.0:8080->80/tcp  lima-nginx-1

It's LEGO bricks all the way down... 👷

Instead of Conclusion

So, is there a real difference between port publishing and port forwarding? Or is port publishing just a Docker's way to call port forwarding for containers? Well, you tell me 😉

In any case, understanding what actually happens when you publish a container's port is helpful. For instance, it allows you to predict what use cases will be technically feasible and enables implementation of some rather advanced scenarios like publishing ports of an already running container or accessing services listening on the container's localhost.