hello-127 - one pipe between two loopbacks
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 ofdocker exec -iorcontainer 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.
Working with an AI agent? Give it the skill in skills/hello-127. It
tells the agent when to use the tunnel and how, before it reaches for
--network host.
Install
-
A Mac.
-
uv, which installs hello-127 and fetches its Python.
-
hello-127, from PyPI:
uv tool install hello-127
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,
lsofshowscom.docker.backendas 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
ollamalibrary raises an error, and Python's built-inhttp.clientdoes 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.backendhides 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
smtplibis 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.--inis the same mirrored: the Mac accepts first, and with nothing listening inside,curlreads "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
--inwait 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
--inhalf-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 infinitydoes 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.
The agent skill is tested too. tests/test_skill.py runs every
hello-127 command in the skill through the real parser. A renamed flag
fails the suite, so the skill never teaches an agent a command that no
longer works.
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
-druns the container in the background.--rmdeletes the container once it stops.--name h127gives it the name the other commands use.--network noneleaves it with loopback only.sleep infinitykeeps 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
-nand-Pprint addresses and ports as numbers, not names.-iTCP:11434picks TCP sockets on Ollama's port, 11434.-sTCP:ESTABLISHEDkeeps 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:
--outlistens inside and dials the Mac, likessh -R.--inlistens on the Mac and dials inside, likessh -L. It reads like Docker's-p HOST:CONTAINER, and reaches a service that-pcannot 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
- Skill: the same tool, written for an AI agent to load
- 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.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| hello_127-0.2.1.tar.gz | 37.2 kB | With provenance | Details for hello_127-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| hello_127-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for hello_127-0.2.1-py3-none-any.whl |
Total release size: 86.1 kB
Release files / hello_127-0.2.1.tar.gz
| Download URL | hello_127-0.2.1.tar.gz |
|---|---|
| Size | 37.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7d9df8fa081aa7fb3ca1d3b986168e22cda270afb935b4cd08d76351a61ad716
|
|
BLAKE2b-256 checksum How to use checksums |
49122a96e04d17665f4d88c2c573dd0d0fa842d396373220c7dc78604a323f6e
|
| 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 28, 2026.
Transparency logRelease files / hello_127-0.2.1-py3-none-any.whl
| Download URL | hello_127-0.2.1-py3-none-any.whl |
|---|---|
| Size | 48.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
df7ae129720b2a5e630ad7b2d6f312424c6a128b019354fea06851cfce469ff0
|
|
BLAKE2b-256 checksum How to use checksums |
4e7e249069d8e0e4a4e3ff280edc5da427b5bb642ff5b43fc29b4b7d07b8a6ea
|
| 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 28, 2026.
Transparency logSecurity for hello-127 0.2.1
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.
Attestation for hello_127-0.2.1.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 28, 2026.
Transparency logA 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.