Skip to main content

hello-127 - one pipe between two loopbacks

CI

Can a container with no network still reach Ollama on the Mac's 127.0.0.1? Yes, and it takes one command:

hello-127 tunnel docker h127 --out 11434:127.0.0.1:11434 --in 8080:127.0.0.1:8000

An agent works in h127, a container with --network none. It asks Ollama on the Mac's 127.0.0.1:11434 for help, as if Ollama ran right beside it. It serves the app it is building on its own 127.0.0.1:8000. You open that app at 127.0.0.1:8080 on the Mac. Swap docker for container, and the same command runs on Apple Container.

Simple is the point:

  • Nothing to install in the image. hello-127 sends a small helper to an interpreter the image already has.
  • The container keeps --network none. The tunnel is a pipe: the standard streams of docker exec -i or container exec -i.
  • No sudo, no firewall exception. On the Mac, hello-127 is a local program dialling loopback.
  • One command, in the foreground. No daemon, no config file. Ctrl-C stops it.

Install

hello-127 itself needs nothing beyond Python's standard library.

Why not the network

A program on a Mac binds to 127.0.0.1 to stay private. Ollama on a Mac does this by default. Only programs on the same machine should reach it.

Docker Desktop bends that. When a Docker container connects to the Mac, Docker Desktop's own process, com.docker.backend, opens a fresh connection to 127.0.0.1 on the Mac. Ollama sees a local client. The macOS firewall sees an outgoing connection, and never asks.

Measured on macOS with Docker Desktop 4.92.0 and Ollama 0.33.3:

  • During a run, lsof shows com.docker.backend as Ollama's only client.
  • A test web server bound to 127.0.0.1 refused the Mac's own LAN address. A container on Docker's default network read from it anyway.

That is a gap. "Bound to loopback" stops meaning "local programs only" the moment Docker Desktop is running.

Apple Container has no such hop. Its traffic arrives over a network interface, so reaching Ollama there needs a firewall exception or sudo.

Loopback only

Every address must be loopback, on both ends of both flags. A wider address on the Mac would make it a relay to the LAN, out of a container with no network or into one. A wider one inside, such as 0.0.0.0, would publish the service to any network the container does have. hello-127 refuses both before anything runs:

usage: hello-127 tunnel RUNTIME CONTAINER (--out SPEC | --in SPEC)... [--languages LIST]
hello-127 tunnel: error: argument --out: not a loopback address: '10.0.0.5'

How it works

hello-127 does the job of com.docker.backend on purpose, through a pipe every runtime already has. docker exec -i and container exec -i run a helper program inside a container and hand you its standard input and output.

  Mac                                          container (--network none)
 ┌────────────────────────────┐              ┌──────────────────────────────┐
 │       ┌──────────┐         │              │                 your program │
 │    ┌─►│ Ollama   │ ─┐      │              │              connects to │   │
 │    │  └──────────┘  ▼      │    stdin     │          127.0.0.1:11434 ▼   │
 │    │        hello-127  ────┼──────────────┼────────────────────►  helper │
 │    └─ 127.0.0.1:11434 ◄────┼──────────────┼◄─────────────────────────┘   │
 │                            │    stdout    │                              │
 └────────────────────────────┘              └──────────────────────────────┘
  • Inside the container, a small helper listens on 127.0.0.1:11434. It shares the container's network, so your program sees Ollama as local.
  • On the wire, each connection becomes frames: open, data, close, each tagged with a connection number. Many connections share one pipe.
  • On the Mac, hello-127 reads the frames and dials Ollama on loopback.

None of this is new. SSH's ProxyCommand carries one TCP stream over a program's standard streams. ssh -L and kubectl port-forward carry many connections through one channel.

The helper

The helper is never installed in the image. hello-127 sends its source as an argument to an interpreter the image already has. It is written six times, once per language, and all six speak the same wire as the Mac side:

Choice Language Sent to Needs An image with it
python Python python3 -c Python 3.10 python:3.12-slim
node JavaScript node -e Node 20 node:20-slim
perl Perl perl -e Perl 5.34 debian:bookworm-slim
ruby Ruby ruby -e Ruby 3.3 ruby:3.3-alpine
php PHP php -r PHP 8.2 php:8.2-cli
busybox sh busybox sh -c BusyBox 1.36 alpine

hello-127 tries them in that order, and the first one present and new enough wins. --languages takes names from the Choice column: --languages ruby,php sets a different order, and one name tries only that language. The Mac side never changes.

languages shows what a running container can use. It names the image, then one row per choice, in that order:

hello-127 languages docker h127
container h127 runs php:8.2-cli
python   ---
node     ---
perl     perl 5.40.1
ruby     ---
php      php 8.2
busybox  ---

Debian-based images carry Perl, so php:8.2-cli picks Perl unless --languages php pins PHP. --- marks a missing interpreter. One that is there but cannot be used says why, such as busybox 1.36.1 (needs the full nc).

BusyBox is for an image with no interpreter at all, such as alpine. It comes last, since a shell cannot close one direction of a socket.

Under load they differ. A stress test held 4000 connections at once through Python and Node, and neither broke. They are the ones to pick. Perl and Ruby hold thousands opened in turn, but a burst of a thousand loses some. PHP hangs at 1021. BusyBox is the weakest by far: a burst of a dozen loses some, and each connection costs about seven processes. Treat it as a backup, for a job that connects now and then, like a periodic database check.

An image with none of them stops the command with one line, before anything is relayed:

hello-127: container h127 has no interpreter tunnel can use: no python3, no node, no perl, no ruby, no php, no busybox

What it is not

It carries bytes, and never reads them. Whatever the service lets a local client do, a program in the container can do too. The tunnel opens the whole service, not one part of it. --in opens the other way: every program on the Mac reaches the server inside, not only a browser.

It also changes how a connection ends. TCP has two endings: a clean close, and a reset when something breaks. The pipe has no frame for a reset, so every ending arrives as a clean close. A protocol that marks its own end, as HTTP does, still notices a cut. A protocol that simply stops at the end of the connection cannot tell a crash from a finish.

A gatekeeper is the other design. It understands each request and decides what to allow. hello-127 makes the pipe on purpose and leaves the deciding out.

How it fails

Each of these was run on both runtimes. The tunnel is left as it is.

  • One slow reader stalls everyone. All connections share one pipe. A client that leaves about a megabyte unread freezes every other connection. When it reads again, they all finish, and nothing is lost.
  • A cut-off stream can look complete. If the tunnel dies mid-stream, the client sees a clean close, not an error. Whether it notices depends on the protocol. Against Ollama, the stock ollama library raises an error, and Python's built-in http.client does not.
  • A crash on the Mac reads as a finish. A test server sent 64 KiB, then reset the connection. A client that reads until the end got an error on the Mac itself. Through the tunnel, it got 64 KiB and a clean end: a short file that looks complete. Docker's default network does the same, so com.docker.backend hides the reset too.
  • A service that is down looks like one that hung up. The helper accepts a connection before the Mac dials. With nothing listening on the Mac, Python's smtplib is refused on the Mac and on Docker's default network. Through the tunnel, it connects, then reads "Connection unexpectedly closed". With the server up, its greeting arrives as usual. --in is the same mirrored: the Mac accepts first, and with nothing listening inside, curl reads "Empty reply from server".
  • The container is killed mid-stream. The tunnel notices, reports it, and exits. Nothing is left connected on the Mac.
  • A Mac address nobody answers on. Linux answers on all of 127.x, so the helper can listen on 127.0.0.2 inside. macOS answers only on the addresses its loopback carries, by default 127.0.0.1 alone. A forward to 127.0.0.2 on the Mac holds each connection for 76 seconds, until macOS gives up. The client inside then reads a closed connection, as if the service hung up.
  • A dead forward stalls the live ones. The Mac finishes each dial before it reads the next frame, so no data can overtake its own connection. A dial that hangs holds up every forward. Behind a forward to 127.0.0.2, a chat on a healthy forward waited about 75 seconds for its first word, then streamed in full. The helper's dials for --in wait the same way, but never hang: inside, every 127.x address answers or refuses at once, in about 20 ms.

Two more, in BusyBox only, since the shell cannot close one direction of a socket:

  • An --in half-close never arrives. A Mac client that ends its side, then waits for the reply, waits forever. The server inside never sees the end. The same client works through every other language.
  • Stopping can leave dead processes behind. The helper runs a few processes per connection. When the container's first process never collects the dead, as sleep infinity does not, they stay listed until the container stops. They hold nothing open.

One more, in PHP only, since it refuses to watch more than 1024 sockets at once:

  • Connection 1021 hangs the tunnel. Every forward stops, and the helper spins at full CPU. The Mac side gives up after five seconds, but the PHP process lives on inside the container until it stops.

One more, read from the code and never run: Windows. tunnel stops before the helper starts, since asyncio on Windows cannot catch Ctrl-C the way the tunnel asks. Past that, Ctrl-C would reach docker too, which the tunnel means to stop on its own, by closing the helper's input.

And one thing that works: a client that has never heard of hello-127. It connects to 127.0.0.1 as usual, and the tunnel does the rest. The stock ollama Python library, with its default settings, works unchanged from a container with no network.

Unit tests

The tests need a clone, not the PyPI install:

git clone https://github.com/east-van-ai/hello-127
cd hello-127
uv run python -m unittest

They need no container and no Ollama. Each helper runs as a local process, with real sockets on both sides. Every tunnel, failure, and query case runs once per language, against the same Mac side. A language whose interpreter is missing, or older than its floor, is skipped. The Python floor runs only with python3.10 on PATH.

Each of the other languages also has tests in its own language, one section each in the languages doc. macOS has no BusyBox, so its tests run in an Alpine image, as that doc shows.

Try it yourself

A container started with --network none streams a chat from Ollama through hello-127. Without the tunnel, the same chat is refused. It takes three terminals.

Setup

The demo needs Ollama, running, with Cohere's Tiny Aya pulled, a 2.1 GB download:

ollama pull hf.co/CohereLabs/tiny-aya-global-GGUF:Q4_K_M

Start the tunnel

In the first terminal, start a container with no network, then the tunnel into it:

docker run -d --rm --name h127 --network none python:3.12-slim sleep infinity
hello-127 tunnel docker h127 --out 11434:127.0.0.1:11434
  • -d runs the container in the background.
  • --rm deletes the container once it stops.
  • --name h127 gives it the name the other commands use.
  • --network none leaves it with loopback only.
  • sleep infinity keeps it running with nothing else to do.

--out makes one forward, for Ollama. The tunnel has no default, so every forward is typed. The helper listens on 11434 inside, and the Mac dials 127.0.0.1:11434:

hello-127: tunnel uses python3 3.12 in container h127
hello-127: outside: relaying inside 127.0.0.1:11434 to 127.0.0.1:11434
hello-127: outside: Ctrl-C to stop
hello-127: inside: listening on 127.0.0.1:11434

Chat from inside the container

In the second terminal, run ollama-query, hello-127's sidekick. It runs a small Python program in the container with docker exec. The program asks Cohere's Tiny Aya one fixed question at 127.0.0.1:11434 and prints the answer as it streams. It knows nothing else about the Mac.

hello-127 ollama-query docker h127
hello-127: ollama-query uses python3 3.12 in container h127
Containerized applications are a revolutionary way to deploy and manage software, ...

Run it before the tunnel is up, and the same program is refused:

hello-127: ollama-query uses python3 3.12 in container h127
hello-127: nothing answers on 127.0.0.1:11434 here. Is the tunnel running?
hello-127: the query exited with code 1

Who is using Ollama?

In the third terminal, while the answer streams, ask who holds Ollama's port:

lsof -nP -iTCP:11434 -sTCP:ESTABLISHED
  • -n and -P print addresses and ports as numbers, not names.
  • -iTCP:11434 picks TCP sockets on Ollama's port, 11434.
  • -sTCP:ESTABLISHED keeps only live connections, in the ESTABLISHED state.
COMMAND     PID   USER   FD   TYPE ... NAME
ollama      803 you       7u  IPv4 ... TCP 127.0.0.1:11434->127.0.0.1:52235 (ESTABLISHED)
python3.1 55300 you       6u  IPv4 ... TCP 127.0.0.1:52235->127.0.0.1:11434 (ESTABLISHED)

Ollama's only client is hello-127's Python, which lsof cuts to python3.1. It is not com.docker.backend. Once the answer ends, only Ollama's listening socket is left, so the list comes back empty.

Reach into the container

Now the other direction. In the second terminal, start a web server inside the container, standing in for the agent's app. It listens on the container's own loopback, so nothing on the Mac can reach it yet:

docker exec -d h127 python3 -m http.server 8000 --bind 127.0.0.1

Back in the first terminal, Ctrl-C the tunnel. Start it again with the command from the top of this page, which adds --in:

hello-127 tunnel docker h127 --out 11434:127.0.0.1:11434 --in 8080:127.0.0.1:8000
hello-127: tunnel uses python3 3.12 in container h127
hello-127: outside: relaying inside 127.0.0.1:11434 to 127.0.0.1:11434
hello-127: outside: listening on 127.0.0.1:8080 for 127.0.0.1:8000 inside
hello-127: outside: Ctrl-C to stop
hello-127: inside: listening on 127.0.0.1:11434

Now open http://127.0.0.1:8080/ in a browser on the Mac. The page lists the container's root folder, and .dockerenv near the top gives it away. A container with no network is serving the Mac's browser. ollama-query still works through --out at the same time.

Other ports, both ways

Ollama is only the specimen. The tunnel carries any TCP service on loopback, in either direction. Each forward is one flag, in the shape [BIND:]PORT:HOST:HOSTPORT that ssh uses. The listener comes first, then what it dials. The Mac is where the command runs, so it is ssh's local side:

  • --out listens inside and dials the Mac, like ssh -R.
  • --in listens on the Mac and dials inside, like ssh -L. It reads like Docker's -p HOST:CONTAINER, and reaches a service that -p cannot publish, since the container has no network.

Both flags repeat, and one tunnel carries them all. Leave BIND out, and it is 127.0.0.1. Ctrl-C the tunnel, then run the first line in the first terminal and the second in the second:

hello-127 tunnel docker h127 --out 127.0.0.2:80:127.0.0.1:11434
hello-127 ollama-query docker h127 --connect 127.0.0.2:80

Here the helper listens on 127.0.0.2:80 inside, and --connect points ollama-query at it. Two forwards may dial the same target. Two listeners on the same address and port would never both bind, so hello-127 refuses them.

Clean up

Back in the first terminal, Ctrl-C closes the helper's input, and the helper exits:

hello-127: outside: stopping, closing the helper's input
hello-127: inside: input ended, helper exiting

Then docker stop h127 removes the container.

Apple Container

Apple Container works the same way, with the same flags:

container run -d --rm --name h127 --network none python:3.12-slim sleep infinity
container exec -d h127 python3 -m http.server 8000 --bind 127.0.0.1
hello-127 tunnel container h127 --out 11434:127.0.0.1:11434 --in 8080:127.0.0.1:8000

The chat is hello-127 ollama-query container h127, and lsof again names hello-127's Python. http://127.0.0.1:8080/ shows the container's root folder, with no firewall exception and no sudo. Then container stop h127 removes the container.

Tested with

macOS 26.6.2, Docker 29.9.2, Apple Container 1.6.0, and Ollama 0.33.3.

More documentation

  • Languages: trying the tunnel on Node, Perl, Ruby, PHP, and BusyBox images
  • Stress: how many connections each language holds, and how each one breaks
  • Changelog: project history and releases

Use of AI

Code and documentation are written in collaboration with remote and local AI; design decisions, code review, semantic and auditory review, and final judgement stay human.


East Van AI · AI for the rest of us! · Vancouver, BC, Canada

github.com/east-van-ai · east-van-ai@proton.me

Copyright (c) 2026 Go Nakamaru

Metadata

Release files for hello-127 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hello-127 0.2.0
File Size Uploaded Provenance Actions
hello_127-0.2.0.tar.gz 36.9 kB With provenance Details for hello_127-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for hello-127 0.2.0
File Interpreter ABI Platform Actions
hello_127-0.2.0-py3-none-any.whl Python 3 none any Details for hello_127-0.2.0-py3-none-any.whl

Total release size: 85.7 kB

Release files / hello_127-0.2.0.tar.gz

Download URL hello_127-0.2.0.tar.gz
Size 36.9 kB
Tags Source
SHA-256 checksum
How to use checksums
5727f648ed00517ed9c886b4e9d8581dd3d5bb65ae3b7ac28eab88d517e183b2
BLAKE2b-256 checksum
How to use checksums
2ce49af719f335c5dabf12cb25626a81bdd948f3cb778e3a01456d0301f98934
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Release files / hello_127-0.2.0-py3-none-any.whl

Download URL hello_127-0.2.0-py3-none-any.whl
Size 48.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
586f2e6193e0c687e6ea1951a5d102424dc3291745c94fa25fc12c0830c2a072
BLAKE2b-256 checksum
How to use checksums
5a30cf163f151c5fdf7504cca9d03996a0aa8b1c19e0d11c36dc001fa9c0da76
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Security for hello-127 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for hello_127-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page