<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Cradicle Explorer</title>
    <link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet">
    <style>
      .form-control-dark::placeholder {
          color: #aaa;
          opacity: 1;
      }
    </style>
    <link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
    <link rel="icon" type="image/png" href="/favicon.png">


                <link href="/css/dashboard.css" rel="stylesheet">
                </head>
                <body>
                <header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
                  <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="/">Cradicle Explorer</a>
                  <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                  </button>
                  <form method="get" action="/cgi-bin/main" style="width:100%;"><input class="form-control form-control-dark w-100 rounded-0 border-0" type="text" name="q" placeholder="Search repos" aria-label="Search"></form>
                  <div class="navbar-nav flex-row">
                    <div class="nav-item text-nowrap">
                      <a class="nav-link px-3 active" href="/cgi-bin/repo?id=zafWK8vuwJBJtynJUtgFjSFWZyGp">iroh-infra</a>
                    </div>
                  </div>
                </header>
                <div class="container-fluid">
                  <div class="row">
                    <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-dark sidebar collapse">
                      <div class="position-sticky pt-3 sidebar-sticky">
                        <ul class="nav flex-column">
                          <li class="nav-item">
                            <a class="nav-link active" href="/cgi-bin/repo?id=zafWK8vuwJBJtynJUtgFjSFWZyGp">
                              <i class="align-text-bottom fa-solid fa-info"></i>
                              Info
                            </a>
                          </li>
                          <li class="nav-item">
                            <a class="nav-link" href="/cgi-bin/repo?id=zafWK8vuwJBJtynJUtgFjSFWZyGp&issue=list">
                              <i class="align-text-bottom fa-solid fa-layer-group"></i>
                              Issues
                            </a>
                          </li>
                          <li class="nav-item">
                            <a class="nav-link" href="/cgi-bin/repo?id=zafWK8vuwJBJtynJUtgFjSFWZyGp&patch=list">
                              <i class="align-text-bottom fa-solid fa-vest-patches"></i>
                              Patches
                            </a>
                          </li>
                          <li class="nav-item">
                            <a class="nav-link" href="/cgi-bin/repo?id=zafWK8vuwJBJtynJUtgFjSFWZyGp&wallet=list">
                              <i class="align-text-bottom fa-solid fa-wallet"></i>
                              Wallets
                            </a>
                          </li>
                          <li class="nav-item">
                            <a class="nav-link" href="/cgi-bin/repo?id=zafWK8vuwJBJtynJUtgFjSFWZyGp&source=.">
                              <i class="align-text-bottom fa-solid fa-code"></i>
                              Source
                            </a>
                          </li>
                        <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
                          <span></span>
                        </h6>
                        <ul class="nav flex-column mb-2">
                        
                        </ul>
                      </div>
                    </nav>
                <main class="col-md-9 ms-sm-auto col-lg-10">
                  <div class="container px-1 py-3">
        

    <div class="list-group">
    <div class="list-group-item">
    <div style="font-size:1.3rem;">iroh-infra</div>
    <div class="repo-item">Ansible playbook to deploy iroh-relay and iroh-dns-server</div>
    <div>rad:zafWK8vuwJBJtynJUtgFjSFWZyGp</div>
    </div>
    <div class="list-group-item">
    <div>Visibility</div>
    <div class="repo-item">public</div>
    </div>
    <div class="list-group-item">
    <div>Delegates</div><div class="repo-item">did:key:z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm</div>
    </div>
    <div class="list-group-item">
    <div>Default branch</div>
    <div><span class="repo-item">main &#8594 1cd744f157c74e1465b55eefd5e0b0b092c0115b</span> (Thu Apr  9 12:43:04 2026)</div>
    </div>
    <div class="list-group-item">
    <div>Threshold</div>
    <div class="repo-item">1</div>
    </div>
    </div>
    
        <div class="list-group mt-3">
        <div class="list-group-item">
        <div class="mb-2" style="font-weight:bold;"><i class="fa-solid fa-book"></i> README.md</div>
        <pre style="margin:0; font-size:0.85rem; overflow-x:auto; color:#fafafa;"># radworks-iroh-infra

Ansible playbook to deploy [iroh-relay](https://github.com/n0-computer/iroh/tree/main/iroh-relay) and [iroh-dns-server](https://github.com/n0-computer/iroh/tree/main/iroh-dns-server) to a single host with Caddy as a TLS-terminating reverse proxy.

## Architecture

```
Internet
  |
  +-- TCP 80/443 --&gt; Caddy (auto Let&#x27;s Encrypt, reverse proxy by hostname)
  |                    |-- relay.example.org --&gt; iroh-relay :3340 (HTTP + WebSocket)
  |                    +-- dns.example.org   --&gt; iroh-dns-server :8080 (HTTP)
  |
  +-- UDP/TCP 53 --&gt; iroh-dns-server (DNS, direct)
  |
  +-- UDP 7842   --&gt; iroh-relay (QUIC address discovery, direct TLS)
```

### TLS termination

Caddy owns ports 80 and 443. It automatically provisions and renews Let&#x27;s Encrypt certificates for both domains and reverse-proxies HTTPS traffic to each service&#x27;s plain HTTP listener.

iroh-relay additionally needs its own TLS for QUIC address discovery on UDP port 7842. It uses the `Reloading` cert mode, which reads PEM certificates from disk and automatically picks up changes. A systemd timer (`sync-relay-certs`) copies Caddy&#x27;s certificates to iroh-relay&#x27;s cert directory every 12 hours.

The relay config sets `dangerous_http_only = true` in the `[tls]` section. This tells iroh-relay to serve its relay protocol over plain HTTP (so Caddy can proxy it) while still using the TLS configuration for the QUIC endpoint. This works because the QUIC config is built from the TLS settings before the `dangerous_http_only` flag strips TLS from the HTTP listener (see `iroh-relay/src/main.rs:624-696`).

### Port map

| Port | Protocol | Service | Description |
|------|----------|---------|-------------|
| 22 | TCP | sshd | SSH access |
| 53 | TCP+UDP | iroh-dns-server | DNS queries |
| 80 | TCP | Caddy | HTTP (redirects to HTTPS, ACME challenges) |
| 443 | TCP | Caddy | HTTPS reverse proxy to relay and dns-server |
| 3340 | TCP | iroh-relay | Internal HTTP listener (localhost only via Caddy) |
| 7842 | UDP | iroh-relay | QUIC address discovery (direct TLS, not proxied) |
| 8080 | TCP | iroh-dns-server | Internal HTTP listener (localhost only via Caddy) |

## Prerequisites

- A Scaleway (or any Ubuntu/Debian) host with a public IP
- DNS A records pointing to the host (see [DNS records](#dns-records))
- Ansible installed locally (`pip install ansible`)

## DNS records

Create these records at your DNS provider before running the playbook:

| Record | Type | Value |
|--------|------|-------|
| `relay.example.org` | A | server public IP |
| `dns.example.org` | A | server public IP |
| `ns1.dns.example.org` | A | server public IP |

The first two are needed for Caddy to obtain Let&#x27;s Encrypt certificates. The third is referenced in the DNS SOA and NS records served by iroh-dns-server.

## Usage

### First deploy

1. Edit `inventory/hosts.yml` and set your server IP:
   ```yaml
   iroh01:
     ansible_host: &quot;203.0.113.10&quot;
   ```

2. Run the playbook with your domain configuration:
   ```bash
   ansible-playbook playbook.yml \
     -e domain_relay=relay.example.org \
     -e domain_dns=dns.example.org \
     -e letsencrypt_email=admin@example.org
   ```

### Updating iroh version

Change `iroh_version` in `group_vars/all.yml` (or pass it as an extra var) and re-run the playbook. The new binaries will be downloaded and the services restarted.

```bash
ansible-playbook playbook.yml \
  -e iroh_version=v0.98.0 \
  -e domain_relay=relay.example.org \
  -e domain_dns=dns.example.org \
  -e letsencrypt_email=admin@example.org
```

## Configuration

All variables are defined in `group_vars/all.yml`. Override them with `-e` flags or by editing the file directly.

### Required variables

| Variable | Description | Example |
|----------|-------------|---------|
| `domain_relay` | Hostname for the relay server | `relay.example.org` |
| `domain_dns` | Hostname for the DNS server | `dns.example.org` |
| `letsencrypt_email` | Email for Let&#x27;s Encrypt registration | `admin@example.org` |

### Optional variables

| Variable | Default | Description |
|----------|---------|-------------|
| `iroh_version` | `v0.97.0` | GitHub release tag to download |
| `iroh_arch` | `x86_64-unknown-linux-musl` | Binary architecture (`aarch64-unknown-linux-musl` for ARM) |
| `iroh_relay_http_port` | `3340` | Internal HTTP port for iroh-relay |
| `iroh_relay_quic_port` | `7842` | QUIC address discovery port |
| `iroh_dns_server_http_port` | `8080` | Internal HTTP port for iroh-dns-server |
| `iroh_dns_server_dns_port` | `53` | DNS listener port |
| `server_ip` | auto-detected | Public IP used in DNS A records (falls back to `ansible_default_ipv4.address`) |

### Service configuration files

The service configs are Jinja2 templates rendered to TOML:

| Template | Rendered to | Docs |
|----------|-------------|------|
| `roles/iroh_relay/templates/iroh-relay.toml.j2` | `/etc/iroh/iroh-relay.toml` | [iroh-relay config](https://github.com/n0-computer/iroh/tree/main/iroh-relay) |
| `roles/iroh_dns_server/templates/iroh-dns-server.toml.j2` | `/etc/iroh/iroh-dns-server.toml` | [iroh-dns-server config](https://github.com/n0-computer/iroh/tree/main/iroh-dns-server) |
| `roles/caddy/templates/Caddyfile.j2` | `/etc/caddy/Caddyfile` | [Caddyfile docs](https://caddyserver.com/docs/caddyfile) |

To add options not exposed as Ansible variables (rate limiting, access control, mainline DHT, etc.), edit the template directly.

## Roles

| Role | What it does |
|------|-------------|
| `common` | Installs packages, creates `iroh` system user, downloads binaries from GitHub releases, disables `systemd-resolved` (frees port 53), configures UFW firewall |
| `caddy` | Installs Caddy from the official apt repo, templates the Caddyfile with reverse proxy rules for both domains |
| `iroh_relay` | Templates relay config and systemd unit, sets up cert-sync timer for QUIC TLS, waits for Caddy certs on first deploy |
| `iroh_dns_server` | Templates DNS server config and systemd unit with `CAP_NET_BIND_SERVICE` for port 53 |

## Systemd services

| Unit | Description |
|------|-------------|
| `caddy.service` | Caddy reverse proxy and TLS terminator |
| `iroh-relay.service` | Iroh relay server |
| `iroh-dns-server.service` | Iroh DNS server |
| `sync-relay-certs.timer` | Copies Caddy&#x27;s relay certs to iroh-relay every 12 hours |

Check status:
```bash
systemctl status caddy iroh-relay iroh-dns-server sync-relay-certs.timer
```

View logs:
```bash
journalctl -u iroh-relay -f
journalctl -u iroh-dns-server -f
journalctl -u caddy -f
```

## Verifying the deployment

```bash
# Relay responds to HTTPS (WebSocket upgrade expected)
curl -v https://relay.example.org/

# DNS-over-HTTPS
curl https://dns.example.org/dns-query

# Pkarr HTTP API
curl https://dns.example.org/pkarr

# DNS query
dig @dns.example.org example.test
```

</pre>
        </div>
        </div>

</div>
</main>
</div>
</div>


</body>
</html>

