2025, Dec 07 21:00
Diagnosing getpeername() returning unexpected peer IP in rootless Podman bridged networking
Why getpeername() in rootless Podman with bridged networking shows a translated peer IP. See causes, logging impact, and fixes using pasta or slirp4netns.
Diagnosing unexpected networking behavior in containers often starts with a familiar syscall. Here, getpeername() inside a rootless Podman container on a bridged network returns a different peer address than when the same workload runs against the host network. The contrast is confusing at first glance, because the code calling getpeername() is identical. The key is that the syscall reports what the kernel recorded for the socket, and in a rootless bridge scenario, that record can differ from what you expect to see.
Reproducing the symptom with a minimal server
The following tiny TCP server accepts a connection and prints the peer address as seen by the containerized process. Run it inside a rootless Podman container configured with a bridged network, then connect from a client, and compare the output to the same test executed using the host network.
import socket
HOST = "0.0.0.0"
PORT = 8080
srv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv_sock.bind((HOST, PORT))
srv_sock.listen(1)
print("listening on", (HOST, PORT))
conn_sock, addr_info = srv_sock.accept()
print("accepted from:", addr_info)
print("getpeername():", conn_sock.getpeername())
conn_sock.close()
srv_sock.close()
If you prefer C, the same idea applies: accept a connection, then call getpeername() on the accepted socket.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void) {
int ls = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in bind_addr; memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(8080);
int opt = 1;
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(ls, (struct sockaddr*)&bind_addr, sizeof(bind_addr));
listen(ls, 1);
struct sockaddr_in src; socklen_t slen = sizeof(src);
int cs = accept(ls, (struct sockaddr*)&src, &slen);
char buf[64];
inet_ntop(AF_INET, &src.sin_addr, buf, sizeof(buf));
printf("accepted from: %s:%d\n", buf, ntohs(src.sin_port));
struct sockaddr_in peer; socklen_t plen = sizeof(peer);
getpeername(cs, (struct sockaddr*)&peer, &plen);
inet_ntop(AF_INET, &peer.sin_addr, buf, sizeof(buf));
printf("getpeername(): %s:%d\n", buf, ntohs(peer.sin_port));
close(cs);
close(ls);
return 0;
}
What is actually happening
The behavior stems from how rootless networking is implemented in Podman. The getpeername() API is doing its job: it returns the address stored in the kernel for the socket. The discrepancy appears because, under rootless bridging, the container’s network stack does not observe the original source IP in the first place. This is comparable in spirit to the NAT hairpinning effect people hit with home-router port forwarding. The mechanics are discussed in community threads such as this issue and this discussion.
In other words, the API is truthful to the socket state; the surrounding rootless networking layer alters what the stack records.
Practical workaround options
There are modes that change how rootless networking behaves. The guidance from the linked discussion is explicit:
you can set
network_mode: pastaornetwork_mode: port_handler=slirp4netnsbut this is incompatible with named (user-defined) networks, so it is a either or situation for now.Fixing this is not trivial at all [...]
If you control the container configuration, switching the network mode can help align what getpeername() reports with what you expect in your environment. Here is a minimal configuration sketch showing the idea:
services:
app:
network_mode: pasta
Alternatively:
services:
app:
network_mode: port_handler=slirp4netns
Be mindful that these modes are not compatible with named user-defined networks as noted above, so it is a trade-off.
Why this matters for diagnostics and observability
Socket-level metadata drives access controls, rate limiting, audit logs, and request attribution. When a rootless bridge abstracts away the original source IP, server code that relies on getpeername() for peer identification will faithfully log the translated address. That is correct from the kernel’s perspective but may be surprising to operators expecting end-client IPs.
Summary and guidance
If getpeername() in a rootless Podman container on a bridged network yields unexpected addresses, treat it as an artifact of the rootless networking mechanism rather than a buggy API. Validate your expectations by comparing with host networking, and when necessary, consider switching to network_mode: pasta or network_mode: port_handler=slirp4netns, understanding that this choice excludes named user-defined networks. Use this knowledge to set accurate logging and ACL strategies for workloads that depend on peer IP visibility.