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
- add the following to
[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
- add the following to
[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