> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nullclaw/nullclaw/llms.txt
> Use this file to discover all available pages before exploring further.

# Tunnel Configuration

> Expose NullClaw gateway to the internet with tunnel providers

Tunnels expose your local NullClaw gateway to the internet without port forwarding or firewall configuration.

## Why Tunnels?

Webhook-based channels (Telegram, Discord, Slack) require a public HTTPS URL to receive events. Tunnels provide this by:

1. Establishing an outbound connection from your gateway to a tunnel server
2. The tunnel server provides a public HTTPS URL
3. Incoming webhook requests are forwarded through the tunnel to your local gateway

```
Telegram → https://abc123.ngrok.io → Tunnel Server → Your Gateway (localhost:3000)
```

## Supported Providers

NullClaw supports multiple tunnel providers:

<CardGroup cols={2}>
  <Card title="Cloudflare Tunnel" icon="cloud">
    Free, unlimited bandwidth, zero-trust security
  </Card>

  <Card title="ngrok" icon="globe">
    Popular, reliable, generous free tier
  </Card>

  <Card title="Tailscale Funnel" icon="network-wired">
    Tailnet-based, secure, funnel mode for public access
  </Card>

  <Card title="Custom" icon="code">
    Bring your own tunnel (bore, localtunnel, etc.)
  </Card>
</CardGroup>

## Configuration

Add tunnel configuration to `~/.nullclaw/config.json`:

```json theme={null}
{
  "tunnel": {
    "provider": "cloudflare",
    "cloudflare": {
      "token": "your-cloudflare-tunnel-token"
    }
  }
}
```

## Cloudflare Tunnel

Cloudflare Tunnel (formerly Argo Tunnel) provides free, unlimited bandwidth with zero-trust security.

### Prerequisites

1. Cloudflare account (free)
2. Install `cloudflared` CLI:

```bash theme={null}
brew install cloudflared  # macOS
# or
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
```

### Setup

<Steps>
  <Step title="Login to Cloudflare">
    ```bash theme={null}
    cloudflared tunnel login
    ```

    This opens a browser to authenticate with Cloudflare.
  </Step>

  <Step title="Create tunnel">
    ```bash theme={null}
    cloudflared tunnel create nullclaw
    ```

    This generates a tunnel token.
  </Step>

  <Step title="Get tunnel token">
    ```bash theme={null}
    cloudflared tunnel token nullclaw
    ```

    Copy the token (starts with `eyJ...`).
  </Step>

  <Step title="Add to config">
    ```json theme={null}
    {
      "tunnel": {
        "provider": "cloudflare",
        "cloudflare": {
          "token": "eyJ..."
        }
      }
    }
    ```
  </Step>
</Steps>

### How It Works

NullClaw spawns `cloudflared` as a child process:

```bash theme={null}
cloudflared tunnel --no-autoupdate run --token TOKEN --url http://localhost:3000
```

The tunnel URL is extracted from stderr output:

```
https://abc123.trycloudflare.com
```

### Implementation

From `src/tunnel.zig`:

```zig theme={null}
pub const CloudflareTunnel = struct {
    token: []const u8,
    allocator: std.mem.Allocator,
    state: TunnelState = .stopped,
    url: ?[]const u8 = null,
    child: ?std.process.Child = null,

    pub fn start(self: *CloudflareTunnel, local_port: u16) ![]const u8 {
        self.state = .starting;
        
        const local_url = try std.fmt.allocPrint(
            self.allocator,
            "http://localhost:{d}",
            .{local_port},
        );
        defer self.allocator.free(local_url);

        var child = std.process.Child.init(
            &.{ "cloudflared", "tunnel", "--no-autoupdate", "run",
                "--token", self.token, "--url", local_url },
            self.allocator,
        );
        child.spawn() catch return error.ProcessSpawnFailed;
        
        // Extract URL from stderr...
        self.state = .running;
        return self.url.?;
    }
};
```

## ngrok

ngrok is a popular tunnel service with a generous free tier.

### Prerequisites

1. ngrok account (free): [https://ngrok.com/signup](https://ngrok.com/signup)
2. Install ngrok CLI:

```bash theme={null}
brew install ngrok  # macOS
# or
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok
```

### Setup

<Steps>
  <Step title="Get auth token">
    Login to ngrok dashboard and copy your auth token from:
    [https://dashboard.ngrok.com/get-started/your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)
  </Step>

  <Step title="Add to config">
    ```json theme={null}
    {
      "tunnel": {
        "provider": "ngrok",
        "ngrok": {
          "auth_token": "your-ngrok-auth-token",
          "domain": "my-app.ngrok.io"  // optional, requires paid plan
        }
      }
    }
    ```
  </Step>
</Steps>

### How It Works

NullClaw spawns ngrok:

```bash theme={null}
ngrok http 3000 --authtoken TOKEN --log stdout --log-format logfmt
```

The tunnel URL is extracted from stdout:

```
url=https://abc123.ngrok.io
```

### Custom Domain

With a paid ngrok plan, you can use a custom domain:

```json theme={null}
{
  "tunnel": {
    "provider": "ngrok",
    "ngrok": {
      "auth_token": "your-token",
      "domain": "nullclaw.example.com"
    }
  }
}
```

## Tailscale Funnel

Tailscale Funnel exposes a service on your tailnet to the public internet.

### Prerequisites

1. Tailscale account (free): [https://tailscale.com/](https://tailscale.com/)
2. Install Tailscale:

```bash theme={null}
brew install tailscale  # macOS
# or
curl -fsSL https://tailscale.com/install.sh | sh
```

3. Login to Tailscale:

```bash theme={null}
tailscale up
```

### Setup

<Steps>
  <Step title="Enable funnel mode">
    ```json theme={null}
    {
      "tunnel": {
        "provider": "tailscale",
        "tailscale": {
          "funnel": true,
          "hostname": "myhost.tail-scale.ts.net"  // optional
        }
      }
    }
    ```
  </Step>
</Steps>

### Funnel vs Serve

* **`tailscale serve`**: Exposes to your tailnet only (private)
* **`tailscale funnel`**: Exposes to public internet (requires funnel mode)

Set `"funnel": true` for public webhooks.

### How It Works

NullClaw spawns:

```bash theme={null}
tailscale funnel 3000  # public
# or
tailscale serve 3000  # tailnet-only
```

The URL is constructed from your Tailscale hostname:

```
https://myhost.tail-scale.ts.net
```

## Custom Tunnels

Bring your own tunnel provider (bore, localtunnel, etc.).

### Configuration

```json theme={null}
{
  "tunnel": {
    "provider": "custom",
    "custom": {
      "start_command": "bore local {port} --to bore.pub",
      "health_url": "http://localhost:4040/api/tunnels",  // optional
      "url_pattern": "https://.*\\.bore\\.pub"  // optional
    }
  }
}
```

### Placeholders

* `{port}`: Replaced with gateway port (e.g., `3000`)
* `{host}`: Replaced with `localhost`

### How It Works

NullClaw:

1. Replaces placeholders in `start_command`
2. Spawns the command as a child process
3. Reads stdout for a URL matching `https://...`

Example with bore:

```bash theme={null}
bore local 3000 --to bore.pub
```

Output:

```
Listening on https://abc123.bore.pub
```

NullClaw extracts `https://abc123.bore.pub`.

## No Tunnel (Local Only)

Set `provider` to `"none"` to disable tunnels:

```json theme={null}
{
  "tunnel": {
    "provider": "none"
  }
}
```

The gateway only accepts connections on `localhost`. Webhooks will not work.

## Tunnel State

NullClaw tracks tunnel state:

```zig theme={null}
pub const TunnelState = enum {
    stopped,
    starting,
    running,
    error_state,
};
```

Check tunnel status:

```bash theme={null}
nullclaw gateway status
```

## Security Considerations

<Warning>
  **Tunnels expose your gateway to the internet.** Ensure you:

  * Use webhook secret tokens (Telegram, Discord, Slack)
  * Enable pairing mode for interactive sessions
  * Review security policies in `~/.nullclaw/config.json`
  * Monitor tunnel logs for unauthorized access
</Warning>

### Webhook Secrets

Always configure webhook secret tokens:

**Telegram:**

```bash theme={null}
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/telegram",
    "secret_token": "your-secret-token"
  }'
```

**Discord:**

```json theme={null}
{
  "channels": {
    "discord": {
      "verification_key": "your-discord-public-key"
    }
  }
}
```

## Tunnel Lifecycle

### Start

```zig theme={null}
var tunnel = try createTunnel(config);
const public_url = try tunnel.start("localhost", 3000);
std.debug.print("Tunnel URL: {s}\n", .{public_url});
```

### Stop

```zig theme={null}
tunnel.stop();
```

This kills the child process and cleans up resources.

### URL Extraction

NullClaw scans tunnel process output for HTTPS URLs:

```zig theme={null}
fn extractUrl(output: []const u8) ?[]const u8 {
    var start: usize = 0;
    while (start < output.len) {
        if (std.mem.indexOfPos(u8, output, start, "https://")) |idx| {
            const end = blk: {
                var e = idx;
                while (e < output.len and 
                       output[e] != ' ' and 
                       output[e] != '\n' and 
                       output[e] != '\r') : (e += 1) {}
                break :blk e;
            };
            if (end > idx + 8) return output[idx..end];
            start = end;
        } else break;
    }
    return null;
}
```

## Troubleshooting

### Tunnel binary not found

Ensure the tunnel CLI is in your PATH:

```bash theme={null}
which cloudflared
which ngrok
which tailscale
```

Install the missing binary:

```bash theme={null}
brew install cloudflared ngrok tailscale
```

### URL not extracted

NullClaw reads tunnel process output to find the URL. If extraction fails:

1. Check tunnel process logs
2. Verify the tunnel is actually starting
3. Ensure output contains `https://` URL

### Tunnel disconnects

Free tunnel plans may have:

* Idle timeouts
* Bandwidth limits
* Connection limits

Consider upgrading to a paid plan for production use.

### Port already in use

If the gateway port is already bound:

```bash theme={null}
lsof -ti:3000 | xargs kill
```

Or change the gateway port:

```json theme={null}
{
  "gateway": {
    "port": 3001
  }
}
```

## Implementation Reference

The tunnel system is implemented in `src/tunnel.zig` with a vtable-based architecture:

```zig theme={null}
pub const TunnelAdapter = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        start: *const fn (ptr: *anyopaque, local_port: u16) TunnelError![]const u8,
        stop: *const fn (ptr: *anyopaque) void,
        public_url: *const fn (ptr: *anyopaque) ?[]const u8,
        provider_name: *const fn (ptr: *anyopaque) []const u8,
        is_running: *const fn (ptr: *anyopaque) bool,
    };
};
```

Each provider implements this interface:

* `NoneTunnel` (no-op)
* `CloudflareTunnel`
* `NgrokTunnel`
* `TailscaleTunnel`
* `CustomTunnel`

See `~/workspace/source/src/tunnel.zig` for full implementation.
