September 14, 2024

ngrok alternative²: frp + Caddy + Lets Encrypt

Update of ngrok alternative: localtunnel + Caddy + Lets Encrypt but using frp - fast reverse proxy.

In addition to the default setup, we are adding multiuser auth support using frp server plugin.

Setup

frps - server

  • prepare dedicated user and folders:
    • sudo useradd -m -s /bin/bash -b /var/lib -r frp
    • sudo install -d -o frp -g frp -m 700 /etc/frp
  • fetch binary from frp releases. Example:
release=0.60.0
curl -sfL "https://github.com/fatedier/frp/releases/download/v${release}/frp_${release}_linux_amd64.tar.gz" | sudo tar -xzf - -C /usr/local/bin --strip-components=1 "frp_${release}_linux_amd64/frps"
  • create config file /etc/frp/frps.toml according to your needs. I’ll use this example for now:
bindPort = 7000
vhostHTTPPort = 7001
webServer.port = 7500
subDomainHost = "your-subdomain.example.com"

[[httpPlugins]]
name = "user-login"
addr = "127.0.0.1:8000"
path = "/login"
ops = ["Login"]
  • create systemd unit:
    • add the following to /etc/systemd/system/frp-server.service
[Unit]
Description=frp (fast reverse proxy) - server
After=network.target
Documentation=https://github.com/fatedier/frp

[Service]
ExecStart=/usr/local/bin/frps --config /etc/frp/frps.toml
User=frp
Group=frp
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
  • start and enable:
    • systemctl enable --now frp-server.service

Caddy

We will also setup protection for the wildcard subdomains to only issue certificates for subdomains registered with frp using a custom ask endpoint.

  • add subdomain and wildcard domain (w/ on demand tls) to your Caddyfile
{
    on_demand_tls {
        ask http://127.0.0.1:8000/ask/
    }
}

your-subdomain.example.com {
  reverse_proxy http://127.0.0.1:7001
}

*.your-subdomain.example.com {
  reverse_proxy http://127.0.0.1:7001
  tls {
    on_demand
  }
}

on-demand-tls custom ask endpoint

First, we will add a python virtualenv to the frp user and install the Sanic framework.

sudo -u frp -H sh -c 'python3 -m venv $HOME/.venv'
sudo -u frp -H sh -c '$HOME/.venv/bin/pip install sanic httpx jmespath PyYAML'

Then, create the app in /var/lib/frp/validate.py.

install -o frp -g frp -m 640 /dev/null /var/lib/frp/validate.py
from httpx import AsyncClient, ConnectError
from jmespath import search
from sanic import Sanic
from sanic.exceptions import Forbidden, NotFound
from sanic.response import json
from yaml import safe_load

app = Sanic("frp-server-validate", env_prefix="FRPSV_")
app.config.FALLBACK_ERROR_FORMAT = "json"

try:
    with open("users.yaml", "r") as f:
        app.config.users = safe_load(f)
except FileNotFoundError:
    app.config.users = {}


async def frps_api(frps):
    async with AsyncClient() as client:
        try:
            result = await client.get(f"{frps}/api/proxy/http")
            return result.json()
        except ConnectError:
            return {}


@app.get("/ask")
async def ask(request):
    frps = "http://localhost:7500"
    if "FRPS" in app.config:
        frps = app.config.FRPS
    result = await frps_api(frps)
    domains = search(
        expression="proxies[*].conf.customDomains[]",
        data=result,
    )
    domain = request.args.get("domain")
    if domains and domain and domain in domains:
        return json({"domain": domain})
    raise NotFound


@app.post("/login")
async def login(request):
    if request.args.get("op") != "Login":
        raise Forbidden
    user = request.json.get("content", {}).get("user")
    token = request.json.get("content", {}).get("metas", {}).get("token")
    if token == app.config.users.get(user, {}).get("token"):
        return json({"reject": False, "unchange": True})
    raise Forbidden
  • create systemd unit:
    • add the following to /etc/systemd/system/frp-server-validate.service
[Unit]
Description=frp (fast reverse proxy) - validator
After=network.target

[Service]
WorkingDirectory=/var/lib/frp/
ExecStart=/var/lib/frp/.venv/bin/sanic validate:app
User=frp
Group=frp
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
  • create a user file w/ tokens in /var/lib/frp/users.yaml
foo:
  token: some-secret-token-for-user-foo
bar:
  token: also-some-secret-token-for-user-bar
  • start and enable:
    • systemctl enable --now frp-validate.service

Iptables

Add a rule which allows incoming TCP traffic for the server port specified (e.g. 7000).

Example:

iptables -A INPUT -p tcp --dport 7000 -j ACCEPT