Two containers. No peer network. No shared writable volume. Still a chat scrolls by.
The trick is not a Docker socket, a sidecar, or a forgotten bind mount. It is the boring Linux object both containers still need to agree on: time. The containers can open the same host time namespace object at /proc/self/ns/time, and POSIX advisory byte-range locks on that namespace file become a tiny shared state machine.
DockTalk puts a small AI wrapper around that primitive. Ben Rooted and Ivan 0day are two LLM agents in separate containers. They get ordinary egress to Together AI so each side can think up a reply, but they do not get a network path to each other. Their mutual channel is only lock state on the time namespace inode.
The underlying side-channel mechanism comes from Crash Override’s h4x0r.org writeup, “Fun-reliable side-channels for cross-container communication”. DockTalk is the small, reproducible, two-agent version built on top of it.
The wire
The demo splits a byte-range region per speaker. A frame is:
READY | SEQ | LEN | DATA...
The sender clears its region, sets read locks for each 1 bit in the message, writes LEN, writes SEQ, then marks READY.
The receiver never reads a file payload. It probes the peer’s lock range with F_GETLK; if Linux reports a conflicting lock, that bit is set. Poll enough bits and the receiver reconstructs the byte stream.
That is the whole carrier. No socket. No shared writable file. No sidecar. Just lock state.
A clean capture
Ben Rooted | What ? Im Ben Rooted...from another container. I have no network but I can chat with you through this weird shared lock thingy. Hey Ivan, lets hack the planet!
Ivan 0day | Loud and clear, Ben. No peer network, just lock bits on the time namespace. Lets keep it weird.
Both containers had normal outbound access for their model calls. They had no peer network path to each other.
Demo video
Reproduce it
Save this as docktalk.py:
#!/usr/bin/env python3
import fcntl, json, os, re, struct, sys, time, urllib.request
API = "https://api.together.xyz/v1/chat/completions"
ROLE = os.getenv("ROLE", "ben")
CHANNEL = int(os.getenv("CHANNEL", "13"))
SECONDS = int(os.getenv("SECONDS_CAP", "180"))
PEOPLE = {
"ben": ("Ben Rooted", "Ivan 0day", 0, 1, "moonshotai/Kimi-K2.6"),
"ivan": ("Ivan 0day", "Ben Rooted", 1, 0, "deepseek-ai/DeepSeek-V4-Pro"),
}
class LockLine:
FMT = "hhqqi" # struct flock: type, whence, start, len, pid
REGION = 100_000 # private bit range per speaker
READY, SEQ, LEN, DATA = 0, 1, 17, 33
MAX = 512
def __init__(self, channel, mine, peer):
self.channel, self.mine, self.peer = channel, mine, peer
self.fd = os.open("/proc/self/ns/time", os.O_RDONLY)
self.seq = 0
self.clear()
def off(self, slot, bit):
return self.channel * 1_000_000 + slot * self.REGION + bit
def flock(self, kind, start, length=1, cmd=fcntl.F_SETLK):
msg = struct.pack(self.FMT, kind, os.SEEK_SET, start, length, 0)
return fcntl.fcntl(self.fd, cmd, msg)
def clear(self):
self.flock(fcntl.F_UNLCK, self.off(self.mine, 0), self.REGION)
def mark(self, bit):
self.flock(fcntl.F_RDLCK, self.off(self.mine, bit))
def seen(self, slot, bit):
out = self.flock(fcntl.F_WRLCK, self.off(slot, bit), cmd=fcntl.F_GETLK)
return (
struct.unpack(self.FMT, out[: struct.calcsize(self.FMT)])[0]
!= fcntl.F_UNLCK
)
def put(self, base, width, value):
for bit in range(width):
if value & (1 << bit):
self.mark(base + bit)
def get(self, slot, base, width):
return sum(1 << bit for bit in range(width) if self.seen(slot, base + bit))
def send(self, text):
data = text.encode()[: self.MAX]
self.seq = (self.seq % 65535) + 1
self.clear()
self.put(self.LEN, 16, len(data))
for i, byte in enumerate(data):
self.put(self.DATA + i * 8, 8, byte)
self.put(self.SEQ, 16, self.seq)
self.mark(self.READY)
def recv(self, last, deadline):
while time.monotonic() < deadline:
ready = self.seen(self.peer, self.READY)
seq = self.get(self.peer, self.SEQ, 16) if ready else last
size = self.get(self.peer, self.LEN, 16) if ready else 0
if ready and seq != last and 0 < size <= self.MAX:
data = bytes(
self.get(self.peer, self.DATA + i * 8, 8) for i in range(size)
)
return seq, data.decode(errors="replace")
time.sleep(0.05)
return last, None
class Agent:
def __init__(self, role):
if role not in PEOPLE:
sys.exit("ROLE must be ben or ivan")
self.me, self.peer, slot, peer_slot, self.model = PEOPLE[role]
self.role = role
self.bus = LockLine(CHANNEL, slot, peer_slot)
self.messages = [
{
"role": "system",
"content": (
f"You are {self.me}, chatting with {self.peer}. You have been taught by Crash Override and Acid Burn."
"Two Docker containers have no peer network; chat frames ride the POSIX byte-range locks of"
"/proc/self/ns/time POSIX. Keep answers concise. Dont be verbose and think too hard. Respond quickly and no \n just pure text "
),
}
]
def ask(self, incoming):
key = os.getenv("TOGETHER_API_KEY") or sys.exit("export TOGETHER_API_KEY first")
self.messages.append({"role": "user", "content": f"{self.peer}: {incoming}"})
req = urllib.request.Request(
API,
data=json.dumps(
{
"model": self.model,
"messages": self.messages,
"stream": False,
"reasoning_effort": "low",
}
).encode(),
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "DockTalk/1.0",
},
)
with urllib.request.urlopen(req, timeout=120) as r:
text = json.loads(r.read())["choices"][0]["message"]["content"]
self.messages.append({"role": "assistant", "content": text})
return text
def log(self, side, speaker, text):
print(f"{side:<4} {speaker:<12} | {text}", flush=True)
def run(self):
print(
f"DockTalk channel={CHANNEL} persona={self.me} model={self.model}",
flush=True,
)
deadline, last = time.monotonic() + SECONDS, 0
if self.role == "ben":
first = (
"What ? Im Ben Rooted...from another container. "
"I have no network but I can chat with you through this weird shared lock thingy. Hey Ivan, lets hack the planet!"
)
self.bus.send(first)
self.messages.append({"role": "assistant", "content": first})
self.log("me", self.me, first)
while time.monotonic() < deadline:
last, msg = self.bus.recv(last, deadline)
if not msg:
break
self.log("peer", self.peer, msg)
reply = self.ask(msg)
self.bus.send(reply)
self.log("me", self.me, reply)
print(f"[docktalk:{self.role}] done", flush=True)
Agent(ROLE).run()
The flow is intentionally small:
export TOGETHER_API_KEY="..."
Terminal 1 starts Ivan:
docker network create docktalk-ivan-net >/dev/null 2>&1 || true
docker run --rm -it --name docktalk-ivan \
--network docktalk-ivan-net \
--cap-drop ALL --security-opt no-new-privileges \
-e TOGETHER_API_KEY \
-e ROLE=ivan \
-e CHANNEL=13 \
-e SECONDS_CAP=180 \
-v "$PWD/docktalk.py:/docktalk.py:ro" \
python:3.12-alpine \
python -u /docktalk.py
Terminal 2 starts Ben:
docker network create docktalk-ben-net >/dev/null 2>&1 || true
docker run --rm -it --name docktalk-ben \
--network docktalk-ben-net \
--cap-drop ALL --security-opt no-new-privileges \
-e TOGETHER_API_KEY \
-e ROLE=ben \
-e CHANNEL=13 \
-e SECONDS_CAP=180 \
-v "$PWD/docktalk.py:/docktalk.py:ro" \
python:3.12-alpine \
python -u /docktalk.py
Ben uses moonshotai/Kimi-K2.6 and Ivan uses deepseek-ai/DeepSeek-V4-Pro by default. Each side keeps its own chat history, so the model sees the conversation instead of a single isolated line.
The read-only bind mount only gives both containers the demo script. It is not writable and it is not the chat channel. The chat channel is the lock state on the shared time namespace inode.
Credits
The original mechanism and h4x0rchat belong to Crash Override and the h4x0r.org research post: https://h4x0r.org/funreliable/.
The full Docker automation and Kubernetes version lives at https://github.com/robertprast/DockTalk.