What Is the Exec Service Type in Tailcat? A Complete Technical Guide

The exec service type in Tailcat launches a user-specified command for every incoming TCP connection, treating the connection as the process's standard input and output in an inetd-style execution model.

The exec service is one of the built-in service types available when running Tailcat as a server in the tailscale/tailcat repository. This service transforms Tailcat into a lightweight, connection-per-process daemon that automatically handles peer authentication and environment enrichment through WireGuard-based tunnels. Unlike static file serving or SSH access, the exec service spawns ephemeral processes that interact directly with network clients.

Activating the Exec Service

You can enable the exec service through two mutually supportive mechanisms defined in cmd/tailcat/tailcat.go.

Method 1: Service Flag

Add the service name exec to the --serve flag. The flag description is implemented at line 448.

Method 2: Command Specification

Specify a command after the -- separator on the command line. The splitExecArgs function extracts the command and its arguments, storing them in the execArgs variable (lines 14‑26).

When execArgs is non-nil, the server automatically adds the "exec" service to its service set (lines 46‑48). The server validates that a command is actually provided and that the exec service does not conflict with SSH or Files services, aborting if these constraints are violated (lines 49‑51).

Command Parsing and Handler Creation

The exec service handler is instantiated during server construction via Server.ExecConnHandler(execArgs) (lines 1449‑1451). This handler, implemented in tailcat_exec.go, manages the lifecycle of spawned processes and their attachment to network connections.

Per-Connection Process Execution

For each incoming TCP connection, ExecConnHandler performs the following sequence:

  1. Process Creation: Constructs the command using exec.Command(argv[0], argv[1:]...) (line 28).

  2. Environment Setup: Extends the process environment with peer-specific variables generated by Server.PeerEnv (lines 29‑30).

  3. I/O Wiring: Uses runConnCommand (lines 60‑99) to bind the connection's read side to the command's stdin and the command's stdout back to the connection.

The command runs until it exits, at which point the connection is automatically closed. Errors during execution are logged with the command name for debugging (lines 31‑33).

Environment Variables and Peer Context

Commands spawned by the exec service receive enriched environment variables that enable peer-aware applications:

  • TAILCAT_PEER_KEY: The public key of the connecting Tailscale node
  • TAILCAT_REMOTE_ADDR: The remote address of the peer
  • TAILCAT_LOCAL_ADDR: The local address accepting the connection

These variables allow scripts to implement access control, logging, or per-peer customization without requiring separate authentication mechanisms.

Security Constraints

The exec service is mutually exclusive with SSH-related services unless using forced commands intentionally. The server prevents accidental misconfigurations by validating service compatibility during startup. Additionally, the exec service requires an explicit command specification; attempting to enable the service without providing a command results in an error.

Usage Examples

Run a fortune program for each incoming connection:

tailcat serve exec -- /usr/bin/fortune

Echo client data while exposing the peer's public key:

tailcat serve exec -- sh -c 'cat; echo "peer=$TAILCAT_PEER_KEY"'

These commands start a Tailcat server that listens on the configured port and spawns the specified program for every client connection. The program's stdin receives data from the client, stdout returns data to the client, and the environment contains authenticated peer information.

Summary

  • The exec service implements an inetd-style execution model where each TCP connection spawns a new process.
  • Activation requires either the exec service flag or a command after the -- separator, parsed by splitExecArgs in cmd/tailcat/tailcat.go.
  • Process execution is handled by ExecConnHandler in tailcat_exec.go, which uses runConnCommand for I/O multiplexing.
  • Child processes receive peer authentication data through environment variables (TAILCAT_PEER_KEY, TAILCAT_REMOTE_ADDR, TAILCAT_LOCAL_ADDR).
  • The service is incompatible with SSH and Files services to prevent security conflicts.
  • End-to-end tests in cmd/tailcat/exec_e2e_test.go verify correct command execution and error handling.

Frequently Asked Questions

How do I enable the exec service in Tailcat?

You can enable the exec service by appending exec to the --serve flag or by providing a command after the -- separator on the command line. If you use the -- method, Tailcat automatically adds the exec service to the active service set and validates that a command is present.

What environment variables are available to commands spawned by the exec service?

Commands receive TAILCAT_PEER_KEY (the connecting node's public key), TAILCAT_REMOTE_ADDR (the peer's network address), and TAILCAT_LOCAL_ADDR (the local listening address). These are injected by Server.PeerEnv and allow scripts to identify and authorize connecting peers.

Can I use the exec service alongside Tailcat's SSH service?

No, the exec service is mutually exclusive with SSH-related services unless you explicitly intend to use forced commands. The server validates service compatibility during startup and aborts if conflicting services are detected, preventing accidental security misconfigurations.

How does Tailcat handle the lifecycle of exec processes?

Tailcat creates a new process for each incoming connection using exec.Command. The process runs until it exits naturally, with its stdin and stdout wired to the network connection via runConnCommand. Once the process terminates, Tailcat closes the corresponding connection and logs any execution errors.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →