hello-127 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hello_127-0.1.1/LICENSE +21 -0
- hello_127-0.1.1/PKG-INFO +305 -0
- hello_127-0.1.1/README.md +291 -0
- hello_127-0.1.1/pyproject.toml +25 -0
- hello_127-0.1.1/pyproject.toml.orig +24 -0
- hello_127-0.1.1/src/hello_127/__init__.py +1 -0
- hello_127-0.1.1/src/hello_127/args.py +205 -0
- hello_127-0.1.1/src/hello_127/cli.py +146 -0
- hello_127-0.1.1/src/hello_127/cli_query.py +41 -0
- hello_127-0.1.1/src/hello_127/cli_tunnel.py +49 -0
- hello_127-0.1.1/src/hello_127/errors.py +9 -0
- hello_127-0.1.1/src/hello_127/host.py +67 -0
- hello_127-0.1.1/src/hello_127/launch.py +20 -0
- hello_127-0.1.1/src/hello_127/query.py +57 -0
- hello_127-0.1.1/src/hello_127/wire.py +196 -0
hello_127-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Go Nakamaru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hello_127-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hello-127
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Carry TCP over a container's standard streams to a service on the Mac's loopback
|
|
5
|
+
Author: Go Nakamaru
|
|
6
|
+
Author-email: Go Nakamaru <gonakamaru@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Project-URL: Repository, https://github.com/east-van-ai/hello-127
|
|
12
|
+
Project-URL: Changelog, https://github.com/east-van-ai/hello-127/blob/main/CHANGELOG.md
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# hello-127
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
Can a Docker or Apple Container with no network access still reach a service on
|
|
20
|
+
the Mac's 127.0.0.1, such as Ollama? Yes, through a pipe: the standard streams
|
|
21
|
+
of `docker exec -i` or `container exec -i`. hello-127 is a small command that
|
|
22
|
+
carries TCP through that pipe.
|
|
23
|
+
|
|
24
|
+
hello-127 is a reusable CLI tool for this purpose. This page covers its design,
|
|
25
|
+
limitations, install, behaviour, and known failure modes.
|
|
26
|
+
|
|
27
|
+
## How a container reaches loopback today
|
|
28
|
+
|
|
29
|
+
A program on a Mac binds to 127.0.0.1 to stay private. Ollama on a Mac does
|
|
30
|
+
this by default. Only programs on the same machine should reach it.
|
|
31
|
+
|
|
32
|
+
Docker Desktop bends that. When a Docker container connects to the Mac, Docker
|
|
33
|
+
Desktop's own process, `com.docker.backend`, opens a fresh connection to
|
|
34
|
+
127.0.0.1 on the Mac. Ollama sees a local client. The macOS firewall sees an
|
|
35
|
+
outgoing connection, and never asks.
|
|
36
|
+
|
|
37
|
+
Measured on macOS with Docker Desktop 4.92.0 and Ollama 0.33.3:
|
|
38
|
+
|
|
39
|
+
- During a run, `lsof` shows `com.docker.backend` as Ollama's only client.
|
|
40
|
+
- A test web server bound to 127.0.0.1 refused the Mac's own LAN address.
|
|
41
|
+
A container on Docker's default network read from it anyway.
|
|
42
|
+
|
|
43
|
+
That is a gap. "Bound to loopback" stops meaning "local programs only" the
|
|
44
|
+
moment Docker Desktop is running.
|
|
45
|
+
|
|
46
|
+
Apple Container has no such hop. Its traffic arrives over a network
|
|
47
|
+
interface, so reaching Ollama there needs a firewall exception or sudo.
|
|
48
|
+
|
|
49
|
+
## The same job, on purpose
|
|
50
|
+
|
|
51
|
+
Every container runtime can already run a program inside a container and
|
|
52
|
+
hand you its standard input and output: `docker exec -i`, or
|
|
53
|
+
`container exec -i`. That is a pipe. hello-127 carries TCP through it.
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
container (--network none) Mac
|
|
57
|
+
┌──────────────────────────────┐ ┌──────────────────────────┐
|
|
58
|
+
│ your program │ │ ┌──────────┐ │
|
|
59
|
+
│ │ connects to │ │ ┌─ │ Ollama │◄─┐ │
|
|
60
|
+
│ ▼ 127.0.0.1:11434 │ stdin │ ▼ └──────────┘ │ │
|
|
61
|
+
│ helper ◄─────────────────┼─────────┼── hello-127 │ │
|
|
62
|
+
│ └─────────────────────────►┼─────────┼──► dials 127.0.0.1 ─┘ │
|
|
63
|
+
│ │ stdout │ │
|
|
64
|
+
└──────────────────────────────┘ └──────────────────────────┘
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **Inside the container**, a small helper listens on 127.0.0.1:11434. It
|
|
68
|
+
shares the container's network, so your program sees Ollama as local.
|
|
69
|
+
- **Over the pipe**, each connection becomes frames: open, data, close, each
|
|
70
|
+
tagged with a connection number. Many connections share one pipe.
|
|
71
|
+
- **On the Mac**, hello-127 reads the frames and dials Ollama on loopback.
|
|
72
|
+
Every connection goes out, none comes in. No firewall prompt, no sudo.
|
|
73
|
+
|
|
74
|
+
The helper is not installed in the image. hello-127 sends its source as an
|
|
75
|
+
argument to `python3 -c`, so the image needs only `python3`, 3.10 or newer.
|
|
76
|
+
|
|
77
|
+
None of this is new. SSH's `ProxyCommand` carries one TCP stream over a
|
|
78
|
+
program's standard streams. `ssh -L` and `kubectl port-forward` carry many
|
|
79
|
+
connections through one channel.
|
|
80
|
+
|
|
81
|
+
## What it is not
|
|
82
|
+
|
|
83
|
+
It carries bytes, and never reads them. Whatever the service lets a local
|
|
84
|
+
client do, a program in the container can do too. The tunnel opens the whole
|
|
85
|
+
service, not one part of it.
|
|
86
|
+
|
|
87
|
+
It also changes how a connection ends. TCP has two endings: a clean close,
|
|
88
|
+
and a reset when something breaks. The pipe has no frame for a reset, so
|
|
89
|
+
every ending arrives as a clean close. A protocol that marks its own end,
|
|
90
|
+
as HTTP does, still notices a cut. A protocol that simply stops at the end
|
|
91
|
+
of the connection cannot tell a crash from a finish.
|
|
92
|
+
|
|
93
|
+
A gatekeeper is the other design. It understands each request and decides
|
|
94
|
+
what to allow. hello-127 makes the pipe on purpose and leaves the deciding
|
|
95
|
+
out.
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
- A Mac.
|
|
100
|
+
- [uv](https://docs.astral.sh/uv/), which installs hello-127 and fetches
|
|
101
|
+
its Python.
|
|
102
|
+
- [Docker Desktop](https://www.docker.com/products/docker-desktop/), or
|
|
103
|
+
[Apple Container](https://github.com/apple/container).
|
|
104
|
+
- hello-127, from PyPI:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv tool install hello-127
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
hello-127 itself needs nothing beyond Python's standard library.
|
|
111
|
+
|
|
112
|
+
## Try it yourself
|
|
113
|
+
|
|
114
|
+
A container started with `--network none` streams a chat from Ollama
|
|
115
|
+
through hello-127. Without the tunnel, the same chat is refused.
|
|
116
|
+
|
|
117
|
+
It needs [Ollama](https://ollama.com), running, with Cohere's Tiny Aya
|
|
118
|
+
pulled, a 2.1 GB download:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
ollama pull hf.co/CohereLabs/tiny-aya-global-GGUF:Q4_K_M
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It takes three terminals. In the first, start a container with no network,
|
|
125
|
+
then the tunnel into it:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
docker run -d --rm --name h127 --network none python:3.12-slim sleep infinity
|
|
129
|
+
hello-127 tunnel docker h127
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `-d` runs the container in the background.
|
|
133
|
+
- `--rm` deletes the container once it stops.
|
|
134
|
+
- `--name h127` gives it the name the other commands use.
|
|
135
|
+
- `--network none` leaves it with loopback only.
|
|
136
|
+
- `sleep infinity` keeps it running with nothing else to do.
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
hello-127: outside: relaying 127.0.0.1:11434 to 127.0.0.1:11434
|
|
140
|
+
hello-127: outside: Ctrl-C to stop
|
|
141
|
+
hello-127: inside: listening on 127.0.0.1:11434
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
In the second, chat from inside the container:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
hello-127 ollama-query docker h127
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
Containerized applications are a revolutionary way to deploy and manage software, ...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`ollama-query` runs a small Python program in the container with
|
|
155
|
+
`docker exec`. It asks Cohere's Tiny Aya one fixed question at
|
|
156
|
+
127.0.0.1:11434 and prints the answer as it streams. It knows nothing else
|
|
157
|
+
about the Mac.
|
|
158
|
+
|
|
159
|
+
Run it before the tunnel is up, and the same program is refused:
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
hello-127: nothing answers on 127.0.0.1:11434 here. Is the tunnel running?
|
|
163
|
+
hello-127: the query exited with code 1
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
In the third, while the answer streams, ask who holds Ollama's port:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
lsof -nP -iTCP:11434 -sTCP:ESTABLISHED
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `-n` and `-P` print addresses and ports as numbers, not names.
|
|
173
|
+
- `-iTCP:11434` picks TCP sockets on Ollama's port, 11434.
|
|
174
|
+
- `-sTCP:ESTABLISHED` keeps only live connections, in the ESTABLISHED state.
|
|
175
|
+
|
|
176
|
+
Once the answer ends, only Ollama's listening socket is left, so the list
|
|
177
|
+
comes back empty.
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
COMMAND PID USER FD TYPE ... NAME
|
|
181
|
+
ollama 803 you 7u IPv4 ... TCP 127.0.0.1:11434->127.0.0.1:52235 (ESTABLISHED)
|
|
182
|
+
python3.1 55300 you 6u IPv4 ... TCP 127.0.0.1:52235->127.0.0.1:11434 (ESTABLISHED)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Ollama's only client is hello-127's Python, which `lsof` cuts to
|
|
186
|
+
`python3.1`. It is not `com.docker.backend`.
|
|
187
|
+
|
|
188
|
+
Back in the first terminal, Ctrl-C closes the helper's input, and the helper
|
|
189
|
+
exits:
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
hello-127: outside: stopping, closing the helper's input
|
|
193
|
+
hello-127: inside: input ended, helper exiting
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Then `docker stop h127` removes the container.
|
|
197
|
+
|
|
198
|
+
Apple Container works the same way, with the same flags:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
container run -d --rm --name h127 --network none python:3.14-slim sleep infinity
|
|
202
|
+
hello-127 tunnel container h127
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The chat is `hello-127 ollama-query container h127`, and `lsof` again
|
|
206
|
+
names hello-127's Python. Then `container stop h127` removes the container.
|
|
207
|
+
|
|
208
|
+
Measured on macOS with Docker 29.8.0, Apple Container 1.6.0, and Ollama
|
|
209
|
+
0.33.3.
|
|
210
|
+
|
|
211
|
+
## Other ports
|
|
212
|
+
|
|
213
|
+
Ollama is only the specimen. The tunnel carries any TCP service on the
|
|
214
|
+
Mac's loopback. `--forward` picks both ends, in the shape `ssh -L` uses:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
hello-127 tunnel docker h127 --forward 127.0.0.2:80:127.0.0.1:11434
|
|
218
|
+
hello-127 ollama-query docker h127 --connect 127.0.0.2:80
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- `127.0.0.2:80` is where the helper listens inside. Leave the address out,
|
|
222
|
+
and it is 127.0.0.1.
|
|
223
|
+
- `127.0.0.1:11434` is what the Mac dials.
|
|
224
|
+
- `--connect` points `ollama-query` at the helper.
|
|
225
|
+
|
|
226
|
+
`--forward` repeats, and one helper carries every listener:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
hello-127 tunnel docker h127 --forward 11434:127.0.0.1:11434 --forward 8080:127.0.0.1:8000
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Two forwards may dial the same target. Two listeners on the same address
|
|
233
|
+
and port would never both bind, so hello-127 refuses them.
|
|
234
|
+
|
|
235
|
+
Every address must be loopback, on both ends. A wider address on the Mac
|
|
236
|
+
would make it a relay to the LAN for a container with no network. A wider
|
|
237
|
+
one inside, such as 0.0.0.0, would publish the Mac's service to any
|
|
238
|
+
network the container does have. hello-127 refuses both before anything
|
|
239
|
+
runs:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
usage: hello-127 tunnel RUNTIME CONTAINER [--forward SPEC]...
|
|
243
|
+
hello-127 tunnel: error: argument --forward: not a loopback address: '10.0.0.5'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## How it fails
|
|
247
|
+
|
|
248
|
+
Each of these was run on both runtimes. The tunnel is left as it is.
|
|
249
|
+
|
|
250
|
+
- **One slow reader stalls everyone.** All connections share one pipe. A
|
|
251
|
+
client that leaves about a megabyte unread freezes every other connection.
|
|
252
|
+
When it reads again, they all finish, and nothing is lost.
|
|
253
|
+
- **A cut-off stream can look complete.** If the tunnel dies mid-stream,
|
|
254
|
+
the client sees a clean close, not an error. Whether it notices depends on
|
|
255
|
+
the protocol. Against Ollama, the stock `ollama` library raises an error,
|
|
256
|
+
and Python's built-in `http.client` does not. So `ollama-query` also
|
|
257
|
+
waits for Ollama's own `done` line.
|
|
258
|
+
- **A crash on the Mac reads as a finish.** A test server sent 64 KiB, then
|
|
259
|
+
reset the connection. A client that reads until the end got an error on
|
|
260
|
+
the Mac itself. Through the tunnel, it got 64 KiB and a clean end: a
|
|
261
|
+
short file that looks complete. Docker's default network does the same,
|
|
262
|
+
so `com.docker.backend` hides the reset too.
|
|
263
|
+
- **A service that is down looks like one that hung up.** The helper
|
|
264
|
+
accepts a connection before the Mac dials. With nothing listening on the
|
|
265
|
+
Mac, Python's `smtplib` is refused on the Mac and on Docker's default
|
|
266
|
+
network. Through the tunnel, it connects, then reads "Connection
|
|
267
|
+
unexpectedly closed". With the server up, its greeting arrives as usual.
|
|
268
|
+
- **No `python3` in the image.** The tunnel stops at once. Apple Container
|
|
269
|
+
says why. Docker prints its reason on the exec's output, which is the
|
|
270
|
+
pipe, so hello-127 swallows it and shows only "code 127".
|
|
271
|
+
- **The container is killed mid-stream.** The tunnel notices, reports it,
|
|
272
|
+
and exits. Nothing is left connected on the Mac.
|
|
273
|
+
- **A Mac address nobody answers on.** Linux answers on all of 127.x, so
|
|
274
|
+
the helper can listen on 127.0.0.2 inside. macOS answers only on the
|
|
275
|
+
addresses its loopback carries, by default 127.0.0.1 alone. A forward to
|
|
276
|
+
127.0.0.2 on the Mac holds each connection for 76 seconds, until macOS
|
|
277
|
+
gives up. `ollama-query` then blames Ollama: "the tunnel closed the
|
|
278
|
+
connection".
|
|
279
|
+
- **A dead forward stalls the live ones.** The Mac finishes each dial
|
|
280
|
+
before it reads the next frame, so no data can overtake its own
|
|
281
|
+
connection. A dial that hangs holds up every forward. Behind a forward to
|
|
282
|
+
127.0.0.2, a chat on a healthy forward waited about 75 seconds for its
|
|
283
|
+
first word, then streamed in full.
|
|
284
|
+
|
|
285
|
+
And one thing that works: a client that has never heard of hello-127. It
|
|
286
|
+
connects to 127.0.0.1 as usual, and the tunnel does the rest. The stock
|
|
287
|
+
`ollama` Python library, with its default settings, works unchanged from a
|
|
288
|
+
container with no network.
|
|
289
|
+
|
|
290
|
+
## More documentation
|
|
291
|
+
|
|
292
|
+
- [Changelog](https://github.com/east-van-ai/hello-127/blob/main/CHANGELOG.md): project history and releases
|
|
293
|
+
|
|
294
|
+
## Use of AI
|
|
295
|
+
|
|
296
|
+
Code and documentation are written in collaboration with remote and local AI; design
|
|
297
|
+
decisions, code review, semantic and auditory review, and final judgement stay human.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
**East Van AI** · AI for the rest of us! · Vancouver, BC, Canada
|
|
302
|
+
|
|
303
|
+
[github.com/east-van-ai](https://github.com/east-van-ai) · <east-van-ai@proton.me>
|
|
304
|
+
|
|
305
|
+
Copyright (c) 2026 Go Nakamaru
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# hello-127
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Can a Docker or Apple Container with no network access still reach a service on
|
|
6
|
+
the Mac's 127.0.0.1, such as Ollama? Yes, through a pipe: the standard streams
|
|
7
|
+
of `docker exec -i` or `container exec -i`. hello-127 is a small command that
|
|
8
|
+
carries TCP through that pipe.
|
|
9
|
+
|
|
10
|
+
hello-127 is a reusable CLI tool for this purpose. This page covers its design,
|
|
11
|
+
limitations, install, behaviour, and known failure modes.
|
|
12
|
+
|
|
13
|
+
## How a container reaches loopback today
|
|
14
|
+
|
|
15
|
+
A program on a Mac binds to 127.0.0.1 to stay private. Ollama on a Mac does
|
|
16
|
+
this by default. Only programs on the same machine should reach it.
|
|
17
|
+
|
|
18
|
+
Docker Desktop bends that. When a Docker container connects to the Mac, Docker
|
|
19
|
+
Desktop's own process, `com.docker.backend`, opens a fresh connection to
|
|
20
|
+
127.0.0.1 on the Mac. Ollama sees a local client. The macOS firewall sees an
|
|
21
|
+
outgoing connection, and never asks.
|
|
22
|
+
|
|
23
|
+
Measured on macOS with Docker Desktop 4.92.0 and Ollama 0.33.3:
|
|
24
|
+
|
|
25
|
+
- During a run, `lsof` shows `com.docker.backend` as Ollama's only client.
|
|
26
|
+
- A test web server bound to 127.0.0.1 refused the Mac's own LAN address.
|
|
27
|
+
A container on Docker's default network read from it anyway.
|
|
28
|
+
|
|
29
|
+
That is a gap. "Bound to loopback" stops meaning "local programs only" the
|
|
30
|
+
moment Docker Desktop is running.
|
|
31
|
+
|
|
32
|
+
Apple Container has no such hop. Its traffic arrives over a network
|
|
33
|
+
interface, so reaching Ollama there needs a firewall exception or sudo.
|
|
34
|
+
|
|
35
|
+
## The same job, on purpose
|
|
36
|
+
|
|
37
|
+
Every container runtime can already run a program inside a container and
|
|
38
|
+
hand you its standard input and output: `docker exec -i`, or
|
|
39
|
+
`container exec -i`. That is a pipe. hello-127 carries TCP through it.
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
container (--network none) Mac
|
|
43
|
+
┌──────────────────────────────┐ ┌──────────────────────────┐
|
|
44
|
+
│ your program │ │ ┌──────────┐ │
|
|
45
|
+
│ │ connects to │ │ ┌─ │ Ollama │◄─┐ │
|
|
46
|
+
│ ▼ 127.0.0.1:11434 │ stdin │ ▼ └──────────┘ │ │
|
|
47
|
+
│ helper ◄─────────────────┼─────────┼── hello-127 │ │
|
|
48
|
+
│ └─────────────────────────►┼─────────┼──► dials 127.0.0.1 ─┘ │
|
|
49
|
+
│ │ stdout │ │
|
|
50
|
+
└──────────────────────────────┘ └──────────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **Inside the container**, a small helper listens on 127.0.0.1:11434. It
|
|
54
|
+
shares the container's network, so your program sees Ollama as local.
|
|
55
|
+
- **Over the pipe**, each connection becomes frames: open, data, close, each
|
|
56
|
+
tagged with a connection number. Many connections share one pipe.
|
|
57
|
+
- **On the Mac**, hello-127 reads the frames and dials Ollama on loopback.
|
|
58
|
+
Every connection goes out, none comes in. No firewall prompt, no sudo.
|
|
59
|
+
|
|
60
|
+
The helper is not installed in the image. hello-127 sends its source as an
|
|
61
|
+
argument to `python3 -c`, so the image needs only `python3`, 3.10 or newer.
|
|
62
|
+
|
|
63
|
+
None of this is new. SSH's `ProxyCommand` carries one TCP stream over a
|
|
64
|
+
program's standard streams. `ssh -L` and `kubectl port-forward` carry many
|
|
65
|
+
connections through one channel.
|
|
66
|
+
|
|
67
|
+
## What it is not
|
|
68
|
+
|
|
69
|
+
It carries bytes, and never reads them. Whatever the service lets a local
|
|
70
|
+
client do, a program in the container can do too. The tunnel opens the whole
|
|
71
|
+
service, not one part of it.
|
|
72
|
+
|
|
73
|
+
It also changes how a connection ends. TCP has two endings: a clean close,
|
|
74
|
+
and a reset when something breaks. The pipe has no frame for a reset, so
|
|
75
|
+
every ending arrives as a clean close. A protocol that marks its own end,
|
|
76
|
+
as HTTP does, still notices a cut. A protocol that simply stops at the end
|
|
77
|
+
of the connection cannot tell a crash from a finish.
|
|
78
|
+
|
|
79
|
+
A gatekeeper is the other design. It understands each request and decides
|
|
80
|
+
what to allow. hello-127 makes the pipe on purpose and leaves the deciding
|
|
81
|
+
out.
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
- A Mac.
|
|
86
|
+
- [uv](https://docs.astral.sh/uv/), which installs hello-127 and fetches
|
|
87
|
+
its Python.
|
|
88
|
+
- [Docker Desktop](https://www.docker.com/products/docker-desktop/), or
|
|
89
|
+
[Apple Container](https://github.com/apple/container).
|
|
90
|
+
- hello-127, from PyPI:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv tool install hello-127
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
hello-127 itself needs nothing beyond Python's standard library.
|
|
97
|
+
|
|
98
|
+
## Try it yourself
|
|
99
|
+
|
|
100
|
+
A container started with `--network none` streams a chat from Ollama
|
|
101
|
+
through hello-127. Without the tunnel, the same chat is refused.
|
|
102
|
+
|
|
103
|
+
It needs [Ollama](https://ollama.com), running, with Cohere's Tiny Aya
|
|
104
|
+
pulled, a 2.1 GB download:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
ollama pull hf.co/CohereLabs/tiny-aya-global-GGUF:Q4_K_M
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
It takes three terminals. In the first, start a container with no network,
|
|
111
|
+
then the tunnel into it:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker run -d --rm --name h127 --network none python:3.12-slim sleep infinity
|
|
115
|
+
hello-127 tunnel docker h127
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `-d` runs the container in the background.
|
|
119
|
+
- `--rm` deletes the container once it stops.
|
|
120
|
+
- `--name h127` gives it the name the other commands use.
|
|
121
|
+
- `--network none` leaves it with loopback only.
|
|
122
|
+
- `sleep infinity` keeps it running with nothing else to do.
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
hello-127: outside: relaying 127.0.0.1:11434 to 127.0.0.1:11434
|
|
126
|
+
hello-127: outside: Ctrl-C to stop
|
|
127
|
+
hello-127: inside: listening on 127.0.0.1:11434
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
In the second, chat from inside the container:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
hello-127 ollama-query docker h127
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
Containerized applications are a revolutionary way to deploy and manage software, ...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`ollama-query` runs a small Python program in the container with
|
|
141
|
+
`docker exec`. It asks Cohere's Tiny Aya one fixed question at
|
|
142
|
+
127.0.0.1:11434 and prints the answer as it streams. It knows nothing else
|
|
143
|
+
about the Mac.
|
|
144
|
+
|
|
145
|
+
Run it before the tunnel is up, and the same program is refused:
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
hello-127: nothing answers on 127.0.0.1:11434 here. Is the tunnel running?
|
|
149
|
+
hello-127: the query exited with code 1
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In the third, while the answer streams, ask who holds Ollama's port:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
lsof -nP -iTCP:11434 -sTCP:ESTABLISHED
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- `-n` and `-P` print addresses and ports as numbers, not names.
|
|
159
|
+
- `-iTCP:11434` picks TCP sockets on Ollama's port, 11434.
|
|
160
|
+
- `-sTCP:ESTABLISHED` keeps only live connections, in the ESTABLISHED state.
|
|
161
|
+
|
|
162
|
+
Once the answer ends, only Ollama's listening socket is left, so the list
|
|
163
|
+
comes back empty.
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
COMMAND PID USER FD TYPE ... NAME
|
|
167
|
+
ollama 803 you 7u IPv4 ... TCP 127.0.0.1:11434->127.0.0.1:52235 (ESTABLISHED)
|
|
168
|
+
python3.1 55300 you 6u IPv4 ... TCP 127.0.0.1:52235->127.0.0.1:11434 (ESTABLISHED)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Ollama's only client is hello-127's Python, which `lsof` cuts to
|
|
172
|
+
`python3.1`. It is not `com.docker.backend`.
|
|
173
|
+
|
|
174
|
+
Back in the first terminal, Ctrl-C closes the helper's input, and the helper
|
|
175
|
+
exits:
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
hello-127: outside: stopping, closing the helper's input
|
|
179
|
+
hello-127: inside: input ended, helper exiting
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Then `docker stop h127` removes the container.
|
|
183
|
+
|
|
184
|
+
Apple Container works the same way, with the same flags:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
container run -d --rm --name h127 --network none python:3.14-slim sleep infinity
|
|
188
|
+
hello-127 tunnel container h127
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The chat is `hello-127 ollama-query container h127`, and `lsof` again
|
|
192
|
+
names hello-127's Python. Then `container stop h127` removes the container.
|
|
193
|
+
|
|
194
|
+
Measured on macOS with Docker 29.8.0, Apple Container 1.6.0, and Ollama
|
|
195
|
+
0.33.3.
|
|
196
|
+
|
|
197
|
+
## Other ports
|
|
198
|
+
|
|
199
|
+
Ollama is only the specimen. The tunnel carries any TCP service on the
|
|
200
|
+
Mac's loopback. `--forward` picks both ends, in the shape `ssh -L` uses:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
hello-127 tunnel docker h127 --forward 127.0.0.2:80:127.0.0.1:11434
|
|
204
|
+
hello-127 ollama-query docker h127 --connect 127.0.0.2:80
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- `127.0.0.2:80` is where the helper listens inside. Leave the address out,
|
|
208
|
+
and it is 127.0.0.1.
|
|
209
|
+
- `127.0.0.1:11434` is what the Mac dials.
|
|
210
|
+
- `--connect` points `ollama-query` at the helper.
|
|
211
|
+
|
|
212
|
+
`--forward` repeats, and one helper carries every listener:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
hello-127 tunnel docker h127 --forward 11434:127.0.0.1:11434 --forward 8080:127.0.0.1:8000
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Two forwards may dial the same target. Two listeners on the same address
|
|
219
|
+
and port would never both bind, so hello-127 refuses them.
|
|
220
|
+
|
|
221
|
+
Every address must be loopback, on both ends. A wider address on the Mac
|
|
222
|
+
would make it a relay to the LAN for a container with no network. A wider
|
|
223
|
+
one inside, such as 0.0.0.0, would publish the Mac's service to any
|
|
224
|
+
network the container does have. hello-127 refuses both before anything
|
|
225
|
+
runs:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
usage: hello-127 tunnel RUNTIME CONTAINER [--forward SPEC]...
|
|
229
|
+
hello-127 tunnel: error: argument --forward: not a loopback address: '10.0.0.5'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## How it fails
|
|
233
|
+
|
|
234
|
+
Each of these was run on both runtimes. The tunnel is left as it is.
|
|
235
|
+
|
|
236
|
+
- **One slow reader stalls everyone.** All connections share one pipe. A
|
|
237
|
+
client that leaves about a megabyte unread freezes every other connection.
|
|
238
|
+
When it reads again, they all finish, and nothing is lost.
|
|
239
|
+
- **A cut-off stream can look complete.** If the tunnel dies mid-stream,
|
|
240
|
+
the client sees a clean close, not an error. Whether it notices depends on
|
|
241
|
+
the protocol. Against Ollama, the stock `ollama` library raises an error,
|
|
242
|
+
and Python's built-in `http.client` does not. So `ollama-query` also
|
|
243
|
+
waits for Ollama's own `done` line.
|
|
244
|
+
- **A crash on the Mac reads as a finish.** A test server sent 64 KiB, then
|
|
245
|
+
reset the connection. A client that reads until the end got an error on
|
|
246
|
+
the Mac itself. Through the tunnel, it got 64 KiB and a clean end: a
|
|
247
|
+
short file that looks complete. Docker's default network does the same,
|
|
248
|
+
so `com.docker.backend` hides the reset too.
|
|
249
|
+
- **A service that is down looks like one that hung up.** The helper
|
|
250
|
+
accepts a connection before the Mac dials. With nothing listening on the
|
|
251
|
+
Mac, Python's `smtplib` is refused on the Mac and on Docker's default
|
|
252
|
+
network. Through the tunnel, it connects, then reads "Connection
|
|
253
|
+
unexpectedly closed". With the server up, its greeting arrives as usual.
|
|
254
|
+
- **No `python3` in the image.** The tunnel stops at once. Apple Container
|
|
255
|
+
says why. Docker prints its reason on the exec's output, which is the
|
|
256
|
+
pipe, so hello-127 swallows it and shows only "code 127".
|
|
257
|
+
- **The container is killed mid-stream.** The tunnel notices, reports it,
|
|
258
|
+
and exits. Nothing is left connected on the Mac.
|
|
259
|
+
- **A Mac address nobody answers on.** Linux answers on all of 127.x, so
|
|
260
|
+
the helper can listen on 127.0.0.2 inside. macOS answers only on the
|
|
261
|
+
addresses its loopback carries, by default 127.0.0.1 alone. A forward to
|
|
262
|
+
127.0.0.2 on the Mac holds each connection for 76 seconds, until macOS
|
|
263
|
+
gives up. `ollama-query` then blames Ollama: "the tunnel closed the
|
|
264
|
+
connection".
|
|
265
|
+
- **A dead forward stalls the live ones.** The Mac finishes each dial
|
|
266
|
+
before it reads the next frame, so no data can overtake its own
|
|
267
|
+
connection. A dial that hangs holds up every forward. Behind a forward to
|
|
268
|
+
127.0.0.2, a chat on a healthy forward waited about 75 seconds for its
|
|
269
|
+
first word, then streamed in full.
|
|
270
|
+
|
|
271
|
+
And one thing that works: a client that has never heard of hello-127. It
|
|
272
|
+
connects to 127.0.0.1 as usual, and the tunnel does the rest. The stock
|
|
273
|
+
`ollama` Python library, with its default settings, works unchanged from a
|
|
274
|
+
container with no network.
|
|
275
|
+
|
|
276
|
+
## More documentation
|
|
277
|
+
|
|
278
|
+
- [Changelog](https://github.com/east-van-ai/hello-127/blob/main/CHANGELOG.md): project history and releases
|
|
279
|
+
|
|
280
|
+
## Use of AI
|
|
281
|
+
|
|
282
|
+
Code and documentation are written in collaboration with remote and local AI; design
|
|
283
|
+
decisions, code review, semantic and auditory review, and final judgement stay human.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
**East Van AI** · AI for the rest of us! · Vancouver, BC, Canada
|
|
288
|
+
|
|
289
|
+
[github.com/east-van-ai](https://github.com/east-van-ai) · <east-van-ai@proton.me>
|
|
290
|
+
|
|
291
|
+
Copyright (c) 2026 Go Nakamaru
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hello-127"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Carry TCP over a container's standard streams to a service on the Mac's loopback"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = ["Operating System :: MacOS"]
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "Go Nakamaru"
|
|
14
|
+
email = "gonakamaru@gmail.com"
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/east-van-ai/hello-127"
|
|
18
|
+
Changelog = "https://github.com/east-van-ai/hello-127/blob/main/CHANGELOG.md"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
hello-127 = "hello_127.cli:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hello-127"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Carry TCP over a container's standard streams to a service on the Mac's loopback"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Go Nakamaru", email = "gonakamaru@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
classifiers = ["Operating System :: MacOS"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Repository = "https://github.com/east-van-ai/hello-127"
|
|
17
|
+
Changelog = "https://github.com/east-van-ai/hello-127/blob/main/CHANGELOG.md"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
hello-127 = "hello_127.cli:main"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""hello-127: carry TCP over a container's standard streams to Ollama on loopback."""
|