Tap 0.4.1
dotnet tool install --global Tap --version 0.4.1
dotnet new tool-manifest
dotnet tool install --local Tap --version 0.4.1
#tool dotnet:?package=Tap&version=0.4.1
nuke :add-package Tap --version 0.4.1
<div align="center"> <p> <img src="assets/tap-logo.svg" alt="Tap" width="150"> </p>
<picture> <source srcset="assets/tap-hero-dark.png" media="(prefers-color-scheme: dark)"> <img src="assets/tap-hero.png" alt="Tap tunnel and HTTP inspector illustration" width="620"> </picture>
<p><strong>Easy tunneling with an HTTP inspector built in.</strong> Test mobile app hooks, webhook deliveries, auth callbacks, partner integrations, and temporary demos from your local machine without changing the app you are building.</p>
<p> <a href="https://philbir.github.io/tap/"><strong>Landing page and docs</strong></a> </p>
<p> <a href="https://philbir.github.io/tap/"><img alt="Docs" src="https://img.shields.io/badge/docs-GitHub%20Pages-14945f"></a> <img alt=".NET" src="https://img.shields.io/badge/.NET-10-512bd4?logo=dotnet"> <img alt="Aspire" src="https://img.shields.io/badge/Aspire-ready-7b2ff7"> <img alt="Cloudflare Tunnel" src="https://img.shields.io/badge/Cloudflare-Tunnel-f38020?logo=cloudflare"> <img alt="Tailscale Funnel" src="https://img.shields.io/badge/Tailscale-Funnel-5e64f4?logo=tailscale"> <img alt="UI" src="https://img.shields.io/badge/UI-React%2019-14945f?logo=react"> </p> </div>
Tap is for the local-development moment when you need a real public URL and a clear view of what hit it. Mobile app development hooks, webhook deliveries, third-party OAuth redirects, auth provider callbacks, partner integrations, and "can you hit my laptop for a minute?" demos all need the same thing: a tunnel that is quick to bring up and a request log that tells you what actually happened.
Tap gives you both. Run it directly from the tap CLI when you want an ad hoc tunnel for one upstream, or add it to a .NET Aspire AppHost when tunnel wiring should live beside the rest of your distributed app.
Quick tunnels are free with TryCloudflare and do not need a Cloudflare account. If you want stable hostnames, use a free Cloudflare account with a domain you control; a .dev domain is a nice fit for developer projects and is usually inexpensive depending on registrar. Tap itself is meant to feel like tap water: free, useful, and available whenever you need another glass.
Tap makes local services reachable through public URLs. Treat exposed endpoints as internet-facing. Prefer short-lived TryCloudflare tunnels for quick demos, use Cloudflare Access or Tap's inspector auth options for sensitive services, and never tunnel a privileged local admin endpoint without an explicit access boundary.
Public tunnels are scanned within minutes. The moment a public hostname's TLS cert appears in a CT log (which happens immediately when you bring up Cloudflare Tunnel or Tailscale Funnel), opportunistic scanners hit it looking for admin endpoints, debug routes, and known-CVE banners. Always pair public tunnels with auth or edge controls. Tap's auth options (header / CIDR / country / OIDC) gate the proxy port before traffic reaches your upstream; for Cloudflare hostnames, Cloudflare Access and WAF rules are another good outer layer. Those attempts show up directly in the Inspector request log, often seconds after the tunnel is reachable. For Tailscale, prefer WithTailscaleServe(...) (tailnet-only) over WithTailscaleFunnel(...) (public) unless you actually need internet exposure.
Run Modes
| When to use | |
|---|---|
| CLI | You want to point Tap at an upstream URL now: tap run http://localhost:3000 --quick. |
| Aspire | You want tunnels and inspectors modeled in your AppHost with generated resource URLs. |
| Standalone inspector | You want a local capture proxy without Cloudflare. |
| Quick tunnel | You need a throwaway *.trycloudflare.com URL with no Cloudflare account or DNS setup. |
| Existing tunnel | You already manage a tunnel in the Cloudflare dashboard and want Tap to run cloudflared --token against it. |
| API-managed tunnel | You want the AppHost to look up or create a named tunnel, write local credentials, and manage DNS. |
| Dynamic hostname | You want fresh per-run hostnames such as api-1a2b3c4d-tap.example.com for demos or parallel dev loops. |
| Tailscale Serve (default) | Tailnet-only: reachable from your other tailnet devices but not the public internet. The safe default for Tailscale. |
| Tailscale Funnel (public, opt-in) | Public URL via your tailnet node — pair with auth. |
| Tailscale (ephemeral) | AppHost / CLI: spin up a per-session userspace tailscaled from an auth key (Process or Docker). Node disappears when the run stops. |
Use Cases
| Use case | Why Tap helps |
|---|---|
| Mobile app callbacks | Point native or emulator builds at a public URL while still serving from localhost. The inspector's QR tab (or http://localhost:<uiPort>/#qr) lets you scan the public URL straight onto your phone. |
| Webhook development | See the raw headers, body, status code, and replay path for every provider delivery. |
| Auth callbacks | Test OAuth/OIDC redirect URIs against a real HTTPS hostname. |
| Streaming protocols | Tap proxies and live-captures Server-Sent Events (text/event-stream) and WebSockets end-to-end. The inspector UI renders dedicated SSE and WS tabs with a live frame/event timeline (direction, payload, timestamps) — open while the connection is in flight and watch messages append in real time. |
| Partner demos | Share a temporary URL to work running on your machine, then tear it down. |
| Aspire demos | Put the same tunnel and inspector wiring in the AppHost so the whole team gets it. |
Install
Pick whichever fits — all three install the same tap CLI.
.NET global tool
Needs the .NET 10 SDK on PATH. Cross-platform.
dotnet tool install -g Tap
dotnet tool update -g Tap
dotnet tool uninstall -g Tap
Make sure ~/.dotnet/tools (Linux/macOS) or %USERPROFILE%\.dotnet\tools (Windows) is on your PATH.
Self-contained binary (Linux/macOS)
No .NET install required. Downloads the latest release for your platform, verifies the SHA256 checksum, and writes a launcher to ~/.local/bin/tap.
curl -fsSL https://raw.githubusercontent.com/philbir/tap/main/install.sh | sh
Pin a version with TAP_VERSION=0.1.0 ..., override paths with TAP_INSTALL_DIR / TAP_BIN_DIR. To uninstall: rm -rf ~/.local/share/tap ~/.local/bin/tap.
Archives are also available directly from the GitHub Releases page as tap-<version>-<rid>.tar.gz, with a SHA256SUMS file alongside.
Windows one-liner
Wraps the .NET global-tool install — needs the .NET 10 SDK on PATH.
irm https://raw.githubusercontent.com/philbir/tap/main/install.ps1 | iex
Pin a version with $env:TAP_VERSION = "0.2.3" before running. To uninstall: dotnet tool uninstall -g Tap.
cloudflared
Cloudflare-tunnel features need cloudflared on PATH. Install it once with brew install cloudflared, winget install Cloudflare.cloudflared, or run tap install-cloudflared after Tap is installed.
Tailscale
Tailscale support can use tailscale serve for private tailnet-only access or tailscale funnel for public internet access. Host-process modes need the tailscale CLI on PATH; Docker mode runs the official tailscale/tailscale image instead. One-time tailnet setup:
- Install Tailscale (
brew install tailscaleon macOS,tailscale.com/download/linuxon Linux, or the GUI installer on Windows) and sign in withtailscale upwhen using system mode. - In the admin console, enable HTTPS Certificates under DNS. This is required for both
serveandfunnel. - For public Funnel only, add a
nodeAttrsrule to your tailnet ACL granting thefunnelcapability:
{
"nodeAttrs": [
{ "target": ["*"], "attr": ["funnel"] }
]
}
Verify with tailscale status --json | grep -i funnel — "funnel" should appear in your node's CapMap. Funnel only listens on ports 443, 8443, and 10000. tailscale serve is the Tap default and stays private to your tailnet.
Quick Start
CLI
tap run http://localhost:3000
That starts the inspector with a local proxy on http://localhost:4444 and the UI on http://localhost:4445.
Add a quick TryCloudflare tunnel:
tap run http://localhost:3000 --quick
Use an existing dashboard-managed tunnel token:
tap run http://localhost:3000 \
--token "$CLOUDFLARE_TUNNEL_TOKEN" \
--hostname api-local.example.com
Use Cloudflare API-managed DNS and a fresh dynamic hostname:
tap run http://localhost:3000 \
--api-token "$CLOUDFLARE_API_TOKEN" \
--account "$CLOUDFLARE_ACCOUNT_ID" \
--api-managed tap-cli \
--dynamic example.com
If cloudflared is not installed, run:
tap install-cloudflared
Use Tailscale (system tailscaled — requires the Tailscale CLI on PATH and a tailnet you're signed in on):
# Tailnet-only (safe default — reachable from your other tailnet devices, not the public internet):
tap run http://localhost:3000 --tailscale
# Public Funnel (URL is on the internet — pair with auth):
tap run http://localhost:3000 --tailscale --tailscale-public \
--auth-header "X-Tap-Key=$TAP_KEY"
Or run a per-session userspace tailscaled with an auth key (no system Tailscale install needed beyond the CLI):
export TAILSCALE_AUTHKEY=tskey-... # or pass --tailscale-authkey
tap run http://localhost:3000 --tailscale
The CLI spawns tailscaled --tun=userspace-networking under a temp state dir, runs tailscale up --authkey ..., configures tailscale serve (or funnel with --tailscale-public), and tears everything down (including the state dir) on Ctrl+C. macOS/Linux only — on Windows pair the auth key with --docker to use the tailscale/tailscale container.
Don't have a host tailscaled binary? Run the official tailscale/tailscale Docker image instead — works on any host with Docker, including macOS where the GUI Tailscale client doesn't expose tailscaled:
export TAILSCALE_AUTHKEY=tskey-...
tap run http://localhost:3000 --tailscale --docker
The same --docker flag controls Cloudflare and Tailscale: with --tailscale it runs tailscale/tailscale; without, it runs cloudflare/cloudflared. For Tailscale, tap starts the container with TS_USERSPACE=true and drives funnel config via docker exec (bind-mounted unix sockets don't survive macOS Docker Desktop's VM boundary). The container reaches the inspector through Docker's host.docker.internal host-gateway alias (auto on Docker Desktop; --add-host is added on Linux). Container is --rm and force-removed on shutdown.
CLI options can also come from environment variables and an optional tap.config file. Command-line flags win, then environment variables, then config file defaults.
{
"upstream": "http://localhost:3000"
}
Aspire: standalone inspection
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Sample_Api>("api");
var tap = builder.AddTap<Projects.Tap_Server>();
api.WithTap(tap);
builder.Build().Run();
Open http://localhost:5198 for the inspector UI. Send traffic through http://localhost:5199 and Tap records the request, response, headers, status, timing, and supported bodies before forwarding to the upstream service. WebSocket upgrades and Server-Sent Events are forwarded through the same proxy port; their frames/events stream live in the inspector's WS and SSE tabs.
Aspire: quick public tunnel
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Sample_Api>("api");
var tap = builder.AddTap<Projects.Tap_Server>(
name: "tap-quick",
proxyPort: 5307,
uiPort: 5306)
.WithQuickTunnel();
api.WithTap(tap);
builder.Build().Run();
cloudflared assigns a random TryCloudflare URL at startup. Tap watches the tunnel logs, surfaces the public URL on the tap, and routes Cloudflare traffic through the tap before it reaches api.
Aspire: existing Cloudflare tunnel
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Sample_Api>("api");
var tap = builder.AddTap<Projects.Tap_Server>()
.WithTunnel("tap-tunnel", t =>
t.WithExistingTunnel(builder.Configuration["Cloudflare:TunnelToken"]));
api.WithTap(tap, "api-local.example.com");
builder.Build().Run();
Configure the token with user-secrets:
dotnet user-secrets set Cloudflare:TunnelToken "<token>" \
--project samples/Sample.AppHost
WithExistingTunnel expects a Cloudflare Tunnel you have already created. Create the tunnel in the Cloudflare dashboard first, copy its connector token, and pass that token to Tap. Tap will run cloudflared tunnel run --token ...; it will not create or reconfigure that dashboard-managed tunnel.
Aspire: Tailscale (private by default)
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Sample_Api>("api");
// Tailnet-only — reachable only from other devices on your tailnet (the safe default).
var tap = builder.AddTap<Projects.Tap_Server>(mode: "tunnel")
.WithTailscaleServe("tap-serve", t => t.WithSystemDaemon());
api.WithTap(tap);
builder.Build().Run();
For a public URL on the internet (pair with auth!):
var tap = builder.AddTap<Projects.Tap_Server>(mode: "tunnel")
.WithTailscaleFunnel("tap-funnel", t => t.WithSystemDaemon())
.WithHeaderAuth("X-Tap-Key", builder.Configuration["Tap:Key"]!);
api.WithTap(tap);
For a per-session userspace daemon (clean tailnet membership, throw-away node):
var tap = builder.AddTap<Projects.Tap_Server>(mode: "tunnel")
.WithTailscaleFunnel("tap-funnel", t => t
.WithEphemeralDaemon(builder.Configuration["Tailscale:AuthKey"]!)
.WithFunnelPort(8443)); // 443 (default), 8443, or 10000
api.WithTap(tap);
Or run the userspace daemon in Docker (tailscale/tailscale image — useful on macOS where the GUI client doesn't expose a tailscaled binary):
var tap = builder.AddTap<Projects.Tap_Server>(mode: "tunnel")
.WithTailscaleFunnel("tap-funnel", t => t
.WithEphemeralDaemon(builder.Configuration["Tailscale:AuthKey"]!),
hostMode: TailscaleHostMode.Docker);
api.WithTap(tap);
In Docker mode the funnel target is auto-rewritten from localhost:<port> to host.docker.internal:<port> so the container can reach the inspector on the host. The companion tailscaled Aspire resource shows up in the dashboard as a docker run child of the funnel resource — its logs are the container's logs, and Aspire's shutdown kills the docker process which --rms the container.
Funnel exposes one URL per tailnet node, so each WithTailscaleFunnel(...) is bound to exactly one upstream — register multiple funnels for multiple upstreams. Tap shells out to tailscale funnel and parses MagicDNS for the public URL; the TailscaleLifecycleHook provisions everything before the funnel resource starts and removes only the path-specific rule on shutdown so other funnel/serve rules survive.
Aspire: API-managed tunnel and DNS
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Sample_Api>("api");
var tap = builder.AddTap<Projects.Tap_Server>()
.WithTunnel("tap-tunnel", t => t
.WithApiManagedTunnel(
builder.Configuration["Cloudflare:ApiToken"]!,
builder.Configuration["Cloudflare:AccountId"]!,
tunnelName: "tap-dev")
.WithDynamicHostname("example.com", prefix: "api-", suffix: "-tap"));
api.WithTap(tap);
builder.Build().Run();
The lifecycle hook runs before cloudflared starts. It looks up or creates the named tunnel, writes a temporary credentials file, resolves the Cloudflare zone, mints hostnames when needed, ensures CNAME records, and then starts cloudflared with a local ingress config.
CLI Reference
| Option | Purpose |
|---|---|
<upstream> |
Target URL to inspect, for example http://localhost:3000. |
--proxy-port |
Captured traffic port. Default 4444. |
--ui-port |
Inspector UI/API port. Default 4445. |
--quick |
Start a TryCloudflare quick tunnel. |
--token |
Connector token for an existing Cloudflare Tunnel. |
--hostname |
Public hostname for token or API-managed mode. |
--api-token |
Cloudflare API token for managed tunnel/DNS operations. |
--account |
Cloudflare account id. |
--api-managed |
Named tunnel to create or reuse. |
--dynamic |
Zone where Tap should mint a fresh hostname. |
--docker |
Run the active provider in Docker. With --tailscale: tailscale/tailscale (ephemeral, userspace networking). Without: cloudflare/cloudflared. |
--auto-install |
Install cloudflared if missing. |
--tailscale |
Route through Tailscale (system tailscaled by default — tailnet-only via tailscale serve; pair with --tailscale-public for tailscale funnel). |
--tailscale-public |
Switch from serve (tailnet-only, default) to funnel (public internet). Pair with auth flags. |
--tailscale-port |
Funnel/serve port. Allowed: 443 (default), 8443, 10000. |
--tailscale-authkey |
Auth key. Switches to ephemeral mode — the CLI spawns a userspace tailscaled per session and joins the tailnet with the key. Env: TAILSCALE_AUTHKEY. |
--tailscale-system |
Force system mode even when an auth key is present (CLI flag, env, or profile). Use when TAILSCALE_AUTHKEY is exported globally but you want one run on the host's existing node. |
--tailscale-login-server |
Override Tailscale coordination server (Headscale, etc.). Env: TAILSCALE_LOGIN_SERVER. |
--config |
Load defaults from a JSON tap.config file. |
Useful environment variables:
| Variable | Purpose |
|---|---|
TAP_UPSTREAM |
Upstream URL when omitted from the command line. |
CLOUDFLARE_TUNNEL_TOKEN |
Token tunnel connector token. |
CLOUDFLARE_API_TOKEN |
API-managed tunnel token. |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account id. |
TAILSCALE_AUTHKEY |
Tailscale auth key — picked up by --tailscale to enable ephemeral mode. |
TAILSCALE_LOGIN_SERVER |
Override Tailscale coordination server (Headscale, etc.). |
Tailscale Setup
Default to tailscale serve (tailnet-only). Only switch to tailscale funnel (public) when you actually need internet exposure, and always pair public tunnels with auth — opportunistic scanners hit new public hostnames within minutes.
System mode (CLI + AppHost):
- Install Tailscale and run
tailscale upso the node is authenticated. - Enable HTTPS Certificates in the admin console (one-time per tailnet — needed for both
serveandfunnel). - For Funnel only: grant the
funnelcapability via tailnet ACLnodeAttrs(see the install section above).servemode doesn't need this.
Ephemeral mode (CLI + AppHost):
- Generate a reusable auth key in the admin console under Settings → Keys and apply tags that grant the
funnelcapability. - CLI: pass
--tailscale-authkey, setTAILSCALE_AUTHKEY, or save the key in a profile. Tap spawnstailscaled --tun=userspace-networkingfor the run, then tears it down on Ctrl+C. - AppHost: stash it in user-secrets with
dotnet user-secrets set Tailscale:AuthKey "tskey-..." --project samples/Sample.AppHost, then useWithEphemeralDaemon(authKey). - Windows ephemeral process mode is not supported; pair the auth key with
--dockerin the CLI orhostMode: TailscaleHostMode.Dockerin Aspire.
The inspector dialog (Tunnel chip in the Inspector header) shows live daemon state — backend state, MagicDNS name, tailnet, version — and a table of every active tailscale funnel / serve rule on the node.
Cloudflare Setup
For token mode:
- In Cloudflare Zero Trust, create a Cloudflare Tunnel.
- Copy the
cloudflared tunnel run --token ...connector command. - Use only the token value with
tap run --tokenorWithExistingTunnel(...). - Route the hostname you pass to Tap to that tunnel in Cloudflare.
For API-managed mode:
- Create a Cloudflare API token with account-level Cloudflare Tunnel edit permission.
- Add DNS edit permission for the zone Tap will manage.
- Provide
Cloudflare:ApiTokenandCloudflare:AccountIdthrough user-secrets, environment variables, or normal .NET configuration. - Use
WithApiManagedTunnel(...); addWithDynamicHostname(...)when Tap should mint hostnames and DNS CNAMEs.
Cloudflare references: tunnel tokens and API token permissions.
Authentication
Tap auth gates the proxy branch before traffic reaches the upstream. The inspector UI port stays local and is not gated by these checks.
CLI static checks:
tap run http://localhost:3000 --quick \
--auth-header "X-Tap-Key=$TAP_KEY" \
--auth-cidr "203.0.113.0/24" \
--auth-country "CH"
CLI OIDC:
tap run http://localhost:3000 --quick \
--auth-oidc-authority "https://issuer.example.com" \
--auth-oidc-client-id "$OIDC_CLIENT_ID" \
--auth-oidc-client-secret "$OIDC_CLIENT_SECRET"
Aspire auth:
var tap = builder.AddTap<Projects.Tap_Server>()
.WithHeaderAuth("X-Tap-Key", builder.Configuration["Tap:Key"]!)
.WithIpAllowList("203.0.113.0/24")
.WithCountryAllowList("CH")
.WithOidcAuth(
authority: builder.Configuration["Auth:Authority"]!,
clientId: builder.Configuration["Auth:ClientId"]!,
clientSecret: builder.Configuration["Auth:ClientSecret"]);
api.WithTap(tap);
Enabled checks are combined. If header auth, CIDR allowlist, country allowlist, and OIDC are all configured, every request must satisfy every configured check.
Packages
| Package | Purpose |
|---|---|
Tap.Hosting |
Aspire AppHost extensions: AddTap, AddTapContainer, WithTap, tap.WithTunnel, tap.WithQuickTunnel, tap.WithTailscaleServe (tailnet-only, default), tap.WithTailscaleFunnel (public, opt-in), WithExistingTunnel, WithApiManagedTunnel, WithDynamicHostname, WithSystemDaemon/WithEphemeralDaemon/WithFunnelPort. |
Tap.Server |
ASP.NET Core capture server: YARP reverse proxy, capture middleware, WebSocket-terminating proxy, SSE event parser, REST API, /api/stream push channel, and bundled React UI with live WS and SSE message timelines. |
Tap.Cli |
Local command host that reuses the same inspector server code. Tailscale system-mode profiles run from the CLI; ephemeral mode requires the AppHost. |
Both entry points use the same Tap.Server host internally. The CLI builds TapInspectorOptions from command-line flags, environment variables, and optional tap.config; Aspire writes the same options through project environment variables.
Consumer AppHost projects must reference both Tap.Hosting and Tap.Server. Tap.Server supplies the generated Projects.Tap_Server metadata type used by AddTap<TTapServer>(); Tap.Hosting should be referenced with IsAspireProjectResource="false" because it is a library, not a launchable resource.
<ProjectReference Include="..\..\src\Tap.Hosting\Tap.Hosting.csproj"
IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Tap.Server\Tap.Server.csproj" />
Configuration
AppHost Cloudflare settings
| Key | Purpose |
|---|---|
Cloudflare:TunnelToken |
Connector token for dashboard-managed token tunnels. |
Cloudflare:ApiToken |
API token for API-managed tunnels, DNS, and tunnel details. |
Cloudflare:AccountId |
Cloudflare account id used with Cloudflare:ApiToken. |
Cloudflare:Zone |
Default zone used by the sample AppHost. |
Cloudflare:Hostnames:* |
Optional sample hostnames for token and managed scenarios. |
For API-managed DNS, the Cloudflare token needs tunnel edit permission on the account and DNS edit permission on the relevant zone.
AppHost Tailscale settings
| Key | Purpose |
|---|---|
Tailscale:AuthKey |
Auth key used by WithEphemeralDaemon(authKey) to spawn a userspace tailscaled per AppHost run. Reusable keys are recommended. |
Tailscale:UseSystem |
Sample AppHost only: set to true to enable the system-daemon Tailscale scenario. |
Tailscale:UseDocker |
Sample AppHost only: set to true (with Tailscale:AuthKey) to enable the Tailscale + Docker scenario. |
The sample AppHost can be filtered by provider:
dotnet run --project samples/Sample.AppHost # all scenarios (default)
dotnet run --project samples/Sample.AppHost -- --scenarios tailscale # standalone + ts-* only
dotnet run --project samples/Sample.AppHost -- --scenarios cloudflare # standalone + cf-* only
Inspector server settings
Tap.Hosting writes these for you when running under Aspire. The CLI maps its flags to the same server options.
| Variable | Purpose |
|---|---|
Inspector__ProxyPort |
Port that receives proxied app traffic. Default 5199. |
Inspector__UiPort |
Port for the local inspector UI and API. Default 5198. |
Inspector__Mode |
standalone or tunnel. |
Inspector__Provider |
cloudflare or tailscale. Gates provider-specific UI panes and API endpoints. |
Inspector__Ingress |
JSON array of { hostname, upstream, tunnelMode, tunnelName, publicUrl }. |
Inspector__Tunnel__* |
Optional tunnel context surfaced by /api/tunnel/details. |
Inspector__Tunnel__SocketPath |
Tailscale daemon socket path (set automatically in ephemeral mode). |
Inspector__Auth__* |
Optional proxy-side auth gate: header, CIDR, country, and OIDC settings. |
Development
dotnet restore Tap.slnx
dotnet build Tap.slnx
dotnet run --project samples/Sample.AppHost
UI source lives in ui/ and is built into src/Tap.Server/wwwroot/ during server builds:
cd ui
yarn
yarn dev
yarn build
Use -p:SkipTapUiBuild=true when iterating on C# only.
Docs site
cd docs-site
yarn
yarn build
yarn preview
The docs site is a static Vite app configured with base: "./" so the built dist/ directory can be deployed under GitHub Pages project paths.
Architecture
At runtime Tap splits traffic across two ports:
Internet -> Cloudflare -> cloudflared -> Tap proxy port -> upstream app
-> Tailscale Funnel -> tailscaled -> Tap proxy port -> upstream app
\-> Tap UI port -> inspector UI/API
The proxy branch captures request and response data, stores the latest records in a bounded in-memory ring, and publishes new records over server-sent events. WebSocket upgrade requests are intercepted by the capture middleware and re-originated against the upstream so that every text and binary frame can be recorded in both directions; the inspector renders them in a dedicated WS tab alongside the existing SSE view. The UI branch serves the React inspector and exposes REST endpoints for request history, replay, ingress, and tunnel details. With Cloudflare credentials configured the UI can show and update tunnel ingress rules; with Tailscale it shows live daemon state and active funnel/serve rules read from tailscale status --json and tailscale serve status --json.
For the deeper technical background, see docs/ARCHITECTURE.md.
Layout
assets/ README logo and hero assets
docs/ Technical documentation
src/Tap.Core/ Shared auth and Cloudflare/cloudflared primitives
src/Tap.Hosting/ Aspire integration and lifecycle hook
src/Tap.Server/ Capture server, YARP proxy, SSE API, bundled UI host
src/Tap.Cli/ CLI host for the inspector server
ui/ Vite + React inspector source
samples/ Sample AppHost and upstream API
License
TBD.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.1 | 93 | 5/13/2026 |
| 0.4.0 | 117 | 5/10/2026 |
| 0.3.0-beta.1 | 81 | 5/9/2026 |
| 0.2.3 | 108 | 5/7/2026 |
| 0.2.2 | 90 | 5/7/2026 |
| 0.2.1 | 102 | 5/6/2026 |
| 0.2.0 | 101 | 5/5/2026 |
| 0.1.0-alpha.2 | 59 | 5/4/2026 |