peer-to-peer overlay for remote targets with limited capability tickets
Portl lets you share a machine, VM, or container over a peer-to-peer transport without opening inbound ports or relying on a central account system. The main workflow is a persistent terminal session: start a named workspace on a target, detach, and let another device or collaborator attach to the same workspace later.
Portl is built on iroh. Discovery uses iroh's DNS, Pkarr, mDNS, and optional Mainline DHT support; relay fallback is used for NAT traversal when direct paths are unavailable.
v0.13.0 — Apple embedding release. Tagged releases now include a
PortlFFI-<tag>-apple.xcframework.zip asset for iOS device, iOS Simulator,
and macOS arm64 app embedding, with a C ABI for identity creation, saved
access import, peer invites, shell opening, session attach, input, resize,
close, and callback event delivery. Portl also has SSH-style TCP, UDP, and
Unix socket forwarding with grouped startup summaries and lifecycle logs,
plus shared -L/-R forwarding flags on portl shell, portl attach,
portl ssh, and portl ssh-proxy. Persistent terminal sessions remain
available via portl/session/v1, provider discovery, Herdr bridge attach,
zmx-control support, tmux -CC compatibility, PORTL-S-* short codes for
importing shared session access through portl accept, and stable
host-suffixed labels for paired machines and saved access.
OSC and escape-sequence leaks across attach, reload, and
multi-attach scenarios are mitigated by four defense layers: a
server-side QueryStripper wired into every persistent-session
provider (zmx, tmux, ghostty, raw), the client HostOutputSanitizer
with a query-response extension, the client StdinResponseFilter that
drops echoed terminal-query replies on the input path, and a reload
pipeline with a stateful sanitizer, UTF-8 boundary snapping, and
DECAWM preservation across the reload window. The two-host PTY fixture
under crates/portl-cli/tests/tuistory_attach.rs is the canonical
end-to-end surface for these defenses and exercises per-provider
attach, reload, multi-attach, DoS fallback, and cross-area replacement
paths.
The current CLI vocabulary is:
target = something Portl can dial: peer label, adapter alias, ticket, or endpoint_id
peer = a saved trust-store entry from `portl peer ls`
ticket = a bounded permission token
session = a named persistent terminal workspace on a target
provider = how the target keeps sessions alive, currently zmx or tmux
Run portl --help for the grouped command map.
# install or upgrade; preserves the current client/agent mode
curl -fsSL \
https://raw.githubusercontent.com/KnickKnackLabs/portl/main/install.sh \
| bash
# install or upgrade and make this machine shareable
curl -fsSL \
https://raw.githubusercontent.com/KnickKnackLabs/portl/main/install.sh \
| PORTL_AGENT=1 bashThe installer is idempotent. Re-run it to upgrade; by default it preserves
whether this machine was already configured as a client or agent. Set
PORTL_VERSION=0.13.0 to pin a release. Use --agent=off to disable the
service, or --uninstall to remove binaries and service while keeping
$PORTL_HOME. By default, Portl stores local state under ~/.portl on
all operating systems (config/, data/, state/, and run/ subdirs).
Release artifacts cover macOS and Linux on arm64 / x86_64.
Apple app embedding releases also include
PortlFFI-<tag>-apple.xcframework.zip with arm64 iOS device, arm64 iOS
Simulator, and arm64 macOS slices.
Daemon lifecycle commands live on portl-agent:
portl-agent status # service + process + IPC status
portl-agent status --json # script-friendly status
portl-agent up # install/enable/start service
portl-agent restart # restart installed service
portl-agent down # stop/disable service, keeping state# mise
mise use -g github:KnickKnackLabs/portl@0.13.0
# mise only shims `portl`; run install.sh with PORTL_AGENT=1 if this machine should be shared.
# cargo
cargo install --git https://github.com/KnickKnackLabs/portl --locked portl
# source checkout
git clone https://github.com/KnickKnackLabs/portl
cd portl
cargo install --path crates/portl-cliOn the machine you want to share:
# Install the daemon if you did not use install.sh --agent.
portl install --apply --yes
# Create your local identity and run diagnostics.
portl init
portl doctor
# Optional local checks for persistent-session providers.
tmux -V # compatibility provider
zmx control --probe # optimized provider, when zmx is installedFor persistent sessions, install at least one provider on the shared machine:
- tmux works as the compatibility provider via
tmux -CC. - zmx is the optimized provider when its
zmx-control/v1path is available.
After another device has a peer label or ticket for this machine, it can ask Portl which providers are available:
portl session providers <shared-machine-label>If no persistent provider is available, portl shell and portl exec
still work, but portl session attach will not provide a reconnectable
workspace.
On the shared machine:
portl invite --ttl 1h --for shared-boxOn the other device:
portl accept PORTLINV-...
portl attach pair --target shared-boxThe session name (pair above) is the rendezvous point. Anyone with
permission to the target can attach to the same named session. For repeated
work on one machine, set a default target:
PORTL_TARGET=shared-box portl attach pairDetach by closing the local terminal; the provider session stays alive. Destroy it explicitly when finished:
portl ls --target shared-box
portl kill pair --target shared-boxWhen the recipient should get short-lived session access without a full
pairing, keep a sender command running and send them the printed
PORTL-S-* code:
portl session share pair --target shared-box --label shared-box --ttl 10m --access-ttl 2h
# prints PORTL-S-...On the recipient machine:
portl accept PORTL-S-...
portl attach shared-box/pairportl accept saves the imported access as a local ticket label. Pass
--label <name> while accepting if the suggested label conflicts. The
sender stays online only for the rendezvous; the saved ticket controls
how long access remains valid.
For short-lived access, mint a bounded ticket on the shared machine:
portl ticket issue session --ttl 2hSend the printed portl... ticket string. The recipient can attach
directly:
portl attach pair --target 'portl...'Or save it under a local label first:
portl ticket save shared-box 'portl...'
portl attach pair --target shared-boxUse portl ticket issue dev --ttl 2h for a broader development ticket
that also includes shell, exec, TCP/UDP, and metadata conveniences.
Docker and Slicer adapters create target aliases that use the same
<TARGET> argument as peers and tickets.
portl init
# Spin up a container target and save its ticket under the alias `demo`.
portl docker run alpine:3.20 --name demo
# One-shot commands.
portl exec demo -- echo "it works"
portl shell demo
# Persistent session, when the target has zmx or tmux.
portl session providers demo
portl session attach demo dev
# TCP / UDP forwards.
portl tcp demo -L 127.0.0.1:18080:127.0.0.1:80
portl udp demo -L 60000:127.0.0.1:60000
# No-sshd OpenSSH compatibility via Portl's native shell/exec lanes.
mkdir -p ~/.portl/ssh
portl ssh-config demo >> ~/.ssh/config
ssh demo 'hostname'
ssh -T demo <<'EOF'
printf 'raw stdin works on %s\n' "$(hostname)"
EOF
ssh -N -D 127.0.0.1:1080 demo
ssh -N -R 127.0.0.1:8022:127.0.0.1:22 demo
# OpenSSH ProxyCommand passthrough when a real sshd runs on the target.
portl ssh-config --mode sshd-proxy demo --host demo-sshd
ssh demo-sshd
portl docker rm demo --forceNative portl ssh --stdio config is intended for OpenSSH clients such as VS
Code Remote-SSH. It accepts OpenSSH auth, maps User to the Portl remote user
when generated with User, supports raw no-PTY shells (ssh -T), normal exec,
PTY shell/exec, agent forwarding, and local/dynamic TCP forwarding (-L/-D)
through Portl TCP capabilities. Remote TCP forwarding (ssh -R) uses
portl/tcp/v2 when the target advertises it; generated stdio tickets permit
loopback binds by default, while non-loopback binds require explicit ticket
rules. Direct Unix socket forwarding (direct-streamlocal@openssh.com) is
supported when the ticket grants the exact Unix connect path. SFTP is rejected
because portl/fs/v1 is still deferred; remote Unix socket forwarding and
OpenSSH PTY terminal modes are not implemented yet.
To require zmx provisioning for a Docker target:
PORTL_ZMX_BINARY=/path/to/zmx \
portl docker run alpine:3.20 --name dev --session-provider zmxTop-level help is grouped by task:
Setup init, doctor, install, config, whoami
Trust peer, invite
Pairing accept
Connect status, shell, session, exec, ssh, ssh-proxy, ssh-config, tcp, udp, socket
Permissions ticket
Integrations docker, slicer, gateway
Utility completions, man, help
Connection commands use <TARGET> because they accept any value that
resolves through Portl's connection cascade: inline ticket, peer label,
saved ticket, adapter alias, then endpoint id. Commands under
portl peer ... use peer vocabulary because they operate on the local
peer store specifically.
portl/ticket/v1— ticket handshake and capability validation.portl/meta/v1— ping, info, and revocation publication.portl/shell/v1— one-shot PTY shell and exact-argv exec.portl/session/v1— persistent terminal sessions via providers.portl/tcp/v1— one stream per forwarded TCP connection.portl/udp/v1— QUIC-datagram UDP with session linger for roaming clients such as mosh.
Full wire details live in docs/specs/040-protocols.md,
with the shipped persistent-session baseline in
docs/specs/200-persistent-sessions.md
and the v0.5.0 control-provider work in
docs/specs/210-session-control-lanes.md.
- Docker —
portl docker run/attachprovisions a container target with aportl-agentand saved alias. - Slicer —
portl slicer runprovisions a Slicer VM and can route throughportl-gateway. - zmx — optimized persistent-session provider when
zmx-control/v1is available; falls back to legacy attach behavior where needed. - tmux — compatibility persistent-session provider via PTY-backed
tmux -CC.
portl doctor
portl status
portl status <TARGET>
portl session providers <TARGET>The agent exposes OpenMetrics on $PORTL_HOME/run/metrics.sock when the
local service is running:
curl --unix-socket "${PORTL_HOME:-$HOME/.portl}/run/metrics.sock" http://metrics/Start with docs/specs/README.md. The numbered
specs are a mix of live design references and historical release records;
the index marks which is which.
Single-branch development on main. MIT licensed. Copyright
"KnickKnackLabs and portl contributors". Open an issue or discussion
before starting a large change.
MIT.