A Zig port of Piko's Go client SDK
(piko/client) — connect an outbound-only tunnel to a Piko server and accept
or forward TCP connections through it. Built on
zio for async I/O.
| Path | Responsibility |
|---|---|
src/yamux/ |
Standalone yamux multiplexing implementation (many logical streams over one connection). Transport-agnostic — used here over a WebSocket, but has no Piko-specific code. See src/yamux/README.md. |
src/piko/ws_conn.zig |
WebSocket(-over-TLS) transport (WsConn), exposed as a std.Io.Reader/Writer pair so it satisfies the same shape zio.net.Stream does — hands straight to yamux.ZioPeer.init. Built on a vendored, patched copy of karlseguin/websocket.zig (src/piko/vendor/ws_client/, see its own README for why it's vendored rather than a plain dependency). |
src/piko/backoff.zig |
Exponential backoff with jitter, direct port of piko/pkg/backoff. |
src/piko/upstream.zig |
Upstream: dials the Piko server and returns an established yamux session, retrying with backoff on transient failures (DNS/TCP/TLS/handshake failures, and retryable HTTP statuses). Port of piko/client/upstream.go. |
src/piko/listener.zig |
Listener: the net.Listener-shaped accept surface for incoming tunneled connections, reconnecting automatically if the underlying connection drops. Port of piko/client/listener.go. |
src/piko/forwarder.zig |
Forwarder: accepts from a Listener and forwards each connection to a local TCP address via a bidirectional copy. Port of piko/client/forwarder.go. |
src/piko/dialer.zig |
Dialer: opens a single, non-multiplexed connection to a Piko endpoint (no retry). Port of piko/client/dialer.go. |
src/piko.zig re-exports all of the above; src/yamux.zig re-exports the
yamux package. Both are re-exported again from src/root.zig as piko and
yamux.
Register a listener for an endpoint and forward every accepted connection to
a local address (mirrors Go's Upstream.Listen + Forwarder):
const piko_client = @import("piko_client");
const piko = piko_client.piko;
var runtime = try zio.Runtime.init(allocator, .{});
defer runtime.deinit();
const io = runtime.io();
const upstream: piko.Upstream = .{
.url = "https://piko.example.com",
.token = "...",
};
var listener = try piko.Listener.init(allocator, io, &upstream, "my-endpoint");
defer listener.deinit();
var forwarder = piko.Forwarder.init(allocator, io, &listener, "127.0.0.1:8080");
try forwarder.run(); // blocks; forwarder.close() from another task to stopOr accept streams directly (no local forwarding) using the same
yamux.StreamHandle (std.Io.Reader/Writer) the yamux package exposes:
var handle = try listener.accept(.{});
defer handle.deinit(allocator);
try handle.writer.writeAll("hello");
try handle.writer.flush();For a single non-multiplexed outbound connection instead (Go's Dialer):
const dialer: piko.Dialer = .{ .url = "https://piko.example.com" };
var conn = try dialer.dial(allocator, io, "my-endpoint");
defer conn.deinit();src/main.zig is a small manual smoke-test driver (zig build run -- <endpoint-id> [local-addr]) that connects to a real Piko server and forwards
accepted streams, logging every step — useful for exercising this against a
live deployment, since piko.Forwarder itself deliberately logs nothing (see
"Deliberately out of scope" below).
The yamux layer is complete and tested independently (see
src/yamux/README.md), including wire interop against a separate Go peer
binary. The Piko client port covers Upstream/Listener/Forwarder/Dialer
with reconnect-with-backoff, TLS (std.crypto.tls, no external TLS
dependency), and bidirectional forwarding — verified against real WebSocket
test servers and, for the connect/listen path, against a real Piko
deployment.
Not yet done: end-to-end verification of a full forwarded connection (dial → tunnel → local target → response) against a real Piko Go server; so far only the connect/listen half has been exercised live.
Deliberately out of scope (add if a real need shows up, not speculatively):
- No internal logging in
piko.Forwarder/piko.Listener— errors propagate through return values, same convention as the yamux layer; log at the call site instead (seesrc/main.zigfor an example wrapper that does). websocket/httpzinbuild.zig.zonare unused by this port (the WebSocket transport is the vendored, patched copy undersrc/piko/vendor/ws_client/instead) — flagged for removal, not removed unilaterally in case they're wanted for something else.
zig build test
Covers the frame codec and session/stream state machines (pure unit tests),
yamux's async behavior over real loopback TCP and Go-interop sockets, the
WebSocket transport (handshake, framing, TLS) over real loopback sockets, and
Upstream/Listener/Forwarder/Dialer against hand-rolled WebSocket test
servers.