---
name: fabric-vpn
description: Set up Tailscale mesh VPN and SSH tunnels for secure access to FABRIC nodes
allowed-tools:
  - Read
  - Grep
  - Glob
  - Write
  - Edit
  - Bash
---

# Instructions

When invoked, generate code for VPN and tunneling on FABRIC. Two main capabilities:

1. **Tailscale** — Zero-trust mesh VPN for direct access to FABRIC nodes from your laptop
2. **SSH Tunnels** — Port forwarding through FABRIC's bastion host for accessing services

Help the user choose the right approach and generate complete setup code.

# Patterns

## Tailscale Mesh VPN

```python
from fabrictestbed_extensions.fablib.fablib import FablibManager

fablib = FablibManager()

slice = fablib.new_slice(name="tailscale-experiment")

node1 = slice.add_node(name="node1", site="TACC", cores=4, ram=16, disk=50)
node1.add_fabnet()

node2 = slice.add_node(name="node2", site="UCSD", cores=4, ram=16, disk=50)
node2.add_fabnet()

slice.submit()
```

```python
# Tailscale auth key from https://login.tailscale.com/admin/settings/keys
ts_key = "tskey-auth-XXXXXXXX"

# Install and start Tailscale on all nodes
for node in slice.get_nodes():
    node.execute(
        f"curl -fsSL https://tailscale.com/install.sh | sh && "
        f"sudo tailscale up --authkey {ts_key}"
    )

# Verify Tailscale status
for node in slice.get_nodes():
    stdout, _ = node.execute("tailscale status")
    print(f"{node.get_name()}:\n{stdout}")
```

```python
# Access services directly from your laptop via Tailscale hostname
# Example: Start a file browser on node1
node1 = slice.get_node(name="node1")
node1.execute(
    "docker run -d --name filebrowser -p 8989:8989 "
    f"-v /home/{node1.get_username()}:/data "
    "-e FB_ROOT=/data -e FB_PORT=8989 -e FB_NOAUTH=noauth "
    "filebrowser/filebrowser"
)

# From your laptop (connected to same Tailscale network):
# http://node1:8989
```

```python
# IMPORTANT: Logout before deleting slice
for node in slice.get_nodes():
    node.execute("sudo tailscale logout")

slice.delete()
```

## SSH Tunnels for Service Access

```python
slice = fablib.new_slice(name="tunnel-experiment")

node = slice.add_node(
    name="service-node",
    site=fablib.get_random_site(),
    cores=4,
    ram=16,
    disk=100,
    image="docker_rocky_8",
)

# Enable Docker
node.add_post_boot_upload_directory('node_tools', '.')
node.add_post_boot_execute('node_tools/enable_docker.sh {{ _self_.image }}')

slice.submit()
```

```python
# Start a service bound to localhost
node = slice.get_node(name="service-node")
node.execute(
    "docker run -d --name filebrowser -p 127.0.0.1:5555:5555 "
    f"-v /home/{node.get_username()}/data:/data "
    "-e FB_BASEURL=/filebrowser -e FB_ROOT=/data "
    "-e FB_PORT=5555 -e FB_NOAUTH=noauth "
    "filebrowser/filebrowser"
)
```

```python
# Generate SSH tunnel config
fablib.create_ssh_tunnel_config(overwrite=True)

# Print tunnel command
import os

local_port = "5555"
target_host = f"{node.get_username()}@{node.get_management_ip()}"
key_file = os.path.basename(fablib.get_default_slice_public_key_file())[:-4]

print(
    f"ssh -L 127.0.0.1:{local_port}:127.0.0.1:{local_port} "
    f"-i {key_file} -F ssh_config {target_host}"
)
print(f"\nThen browse: http://127.0.0.1:{local_port}/filebrowser")
```

## SSH Tunnel to Jupyter/Grafana

```python
# For any web service running on a FABRIC node:
node = slice.get_node(name="monitor-node")

# Get management IP for tunnel
mgmt_ip = node.get_management_ip()
username = node.get_username()

# Generic tunnel pattern
service_port = "3000"  # e.g., Grafana
print(f"Run on your laptop:")
print(
    f"ssh -L 127.0.0.1:{service_port}:127.0.0.1:{service_port} "
    f"-i <slice_key> -F ssh_config {username}@{mgmt_ip}"
)
print(f"Then browse: http://127.0.0.1:{service_port}")
```

## Multiple Tunnels

```python
# Forward multiple ports in a single SSH command
ports = {"Grafana": 3000, "Prometheus": 9090, "App": 8080}

tunnel_flags = " ".join(
    f"-L 127.0.0.1:{port}:127.0.0.1:{port}" for port in ports.values()
)

print(f"ssh {tunnel_flags} -i <key> -F ssh_config {username}@{mgmt_ip}")

for name, port in ports.items():
    print(f"  {name}: http://127.0.0.1:{port}")
```

## Important Notes

- **Tailscale auth key**: Generate at https://login.tailscale.com/admin/settings/keys
- **Tailscale logout**: Always run `tailscale logout` before deleting slices to clean up devices
- **SSH tunnels**: Use `fablib.create_ssh_tunnel_config()` for bastion-aware SSH config
- **Bind to localhost**: Always bind services to `127.0.0.1` for security, not `0.0.0.0`
- **ssh_config**: Generated config handles FABRIC bastion host automatically
