plugin-tunnel
| Placement | compiled-in (in-process) |
| Source | github.com/opencharly/charly/candy/plugin-tunnel |
| Version | 2026.182.1200 |
| Candy | plugin-tunnel |
This plugin is listed in charly/charly.yml’s compiled_plugins:, so its providers are compiled into the charly binary and register in-process.
Providers
Section titled “Providers”The reserved words this plugin serves:
tunnel— verb class
What it does
Section titled “What it does”OUT-OF-TREE charly plugin serving the tunnel VERB (verb:tunnel) — the
externalized tailscale/cloudflare TUNNEL EXECUTION LEG (core-externalization
cutover C16b). It is DUAL-PLACEMENT: its importable provider package
(NewProvider/NewMeta, serving verb:tunnel + its self-contained CUE schema over
the SDK Describe channel) is COMPILED INTO charly when listed in charly.yml
compiled_plugins: (the default — registered in-process via
registerCompiledPlugin), and the SAME provider is served OUT-OF-PROCESS over
go-plugin gRPC by the cmd/serve shim (host-built + connected via LocalTransport)
when it is not. Placement is invisible above the provider registry.
The RESOLUTION half of the tunnel subsystem lives in sdk/deploykit/tunnel_resolve.go (ResolveTunnelConfig / TunnelConfigFromMetadata + the tunnelConfigPath the systemd unit references; the pure schemeTarget/ tailscaleFlag/isTCPFamily helpers the quadlet emitter shares, FLOOR-SLIM mechanical batch — charly/tunnel.go, the former core home, carried nothing but dead aliases and was deleted). Only the EXECUTION leg lives HERE: the pod-lifecycle plugins that resolve a TunnelConfig via the pod-config-container-tunnel / pod-config-tunnel-resolve host-build seams (candy/plugin-deploy-pod for start/stop, candy/plugin-pod for remove — Cutover B unit 2) drive this verb’s start/stop/setup methods directly over InvokeProvider, wrapped in the {method, config} Invoke envelope; the plugin runs the actual tailscale serve/funnel and cloudflared lifecycle, stopping at the exec/auth boundary. There is no remaining core-side tunnel EXECUTION dispatch — every caller reaches this verb directly.
verb:tunnel ALSO carries a benign plan (dry-run) method: given a TunnelConfig
it returns the EXACT tailscale/cloudflared argv it WOULD run WITHOUT exec, so a
disposable bed proves the registry dispatch + the TunnelConfig wire round-trip +
the moved command-building logic with ZERO tailscale/cloudflare credentials. The
R10 consumer is box/fedora’s check-tunnel-pod bed (a plugin: tunnel
method:plan step asserting the built argv).
Parameter schema
Section titled “Parameter schema”The CUE schema below is the authoritative grammar for this plugin’s input. It is the same single source that generates the plugin’s Go parameter types and answers the runtime Describe RPC, so this page cannot disagree with either.
schema/tunnel.cue
Section titled “schema/tunnel.cue”// The OUT-OF-TREE plugin-tunnel's OWN CUE schema — the typed params for the// `tunnel` VERB (verb:tunnel), the externalized tailscale/cloudflare TUNNEL// EXECUTION LEG. It is the SINGLE SOURCE for this plugin's params, used two ways// (the same contract core `spec` + the reference examplerunverb use)://// 1. GENERATE the Go param struct — `cue exp gengotypes` (driven by task cue:gen,// which wraps this with `package params` + `@go(params)`) emits// ../params/cue_types_gen.go, so the provider decodes plugin_input into a TYPED// struct (params.TunnelInput / TunnelConfig / TunnelPort), never a hand-parsed map.// 2. VALIDATE authored input AT RUNTIME — the plugin SERVES this source over the// Describe channel (sdk.BuildCapabilities → schema_cue); the host splices it onto// its base (base ++ plugin) and validates every authored `plugin: tunnel` step's// plugin_input against #TunnelInput. The host NEVER reads this file from disk — the// schema travels with the plugin.//// verb:tunnel is NOT a check verb in the usual sense — it is the externalized TUNNEL// EXECUTION backend. The pod-lifecycle plugins (candy/plugin-deploy-pod, candy/plugin-pod)// drive its start/stop/setup methods directly over this verb's Invoke envelope// ({method,config}) via InvokeProvider. It ALSO// carries a benign `plan` (dry-run) method — a `plugin: tunnel` check step returns the// EXACT tailscale/cloudflared argv it WOULD run WITHOUT exec, so a disposable bed proves// the registry dispatch + the TunnelConfig wire round-trip + the moved command-building// logic with ZERO tailscale/cloudflare credentials.//// SELF-CONTAINED: #TunnelInput references only its sibling defs (#TunnelConfig /// #TunnelPort) WITHIN this schema, NO base def, so it compiles standalone (gengotypes +// the SDK serve-side check) AND splices onto the base — the splice exists to detect a// def-name collision with the base, not to resolve base references.
// #TunnelInput — the plugin_input shape for the `tunnel` verb.#TunnelInput: { // method — the tunnel operation: start | stop | setup | plan. `plan` is the // creds-free dry-run that returns the argv the operation WOULD run (no exec). method: string & !="" @go(Method) // config — the resolved tunnel configuration to act on (byte-compatible with the // core's TunnelConfig, sent by the calling pod-lifecycle plugin over the Invoke envelope). config?: #TunnelConfig @go(Config) // expect — plan only: the expected argv command lines (space-joined). When set, the // plan method compares the built argv against it (FAIL on mismatch); empty ⇒ the // step passes as long as dispatch + round-trip succeed, echoing the built argv. expect?: [...string] @go(Expect)}
// #TunnelConfig — the resolved, ready-to-execute tunnel configuration.#TunnelConfig: { // provider — "tailscale" or "cloudflare". provider?: string @go(Provider) // tunnel_name — cloudflare: tunnel name. tunnel_name?: string @go(TunnelName) // hostname — cloudflare: default hostname (from the image dns field). hostname?: string @go(Hostname) // box_name — for PID file naming / cloudflare tunnel-name default. box_name?: string @go(BoxName) // ports — all tunneled ports with their access scope. ports?: [...#TunnelPort] @go(Ports)}
// #TunnelPort — a single port to tunnel with its protocol and access scope.#TunnelPort: { // port — the tailscale HTTPS listen port (must be a valid serve/funnel port). port?: int @go(Port) // backend_port — the localhost backend port (0 means same as port). backend_port?: int @go(BackendPort) // protocol — backend scheme: http | https | https+insecure | tcp | // tls-terminated-tcp | ssh | rdp | smb (udp is skipped, never tunneled). protocol?: string @go(Protocol) // public — true = internet-accessible (funnel), false = private (tailnet-only serve). public?: bool @go(Public) // hostname — cloudflare: per-port hostname (from the map form). hostname?: string @go(Hostname)}See also the candy reference for this candy’s install surface.