Nicolas Martyanoff — Brain dump About

Replacing Ngrok With Frp

I have been using Ngrok for almost two years in order to test webhooks during development. It works really well, true; but even the personal plan costs $96 a year if you want a fixed URI. I recently came across Frp, a reverse proxy packed with useful features. This is how I used it to replace Ngrok entirely.

Of course you will need a server running somewhere. It does not have to be powerful, even the smallest VPS will do the work just fine. Personally I use FreeBSD, but Frp is written in Go and should run anywhere.

Configuring the server

Frp server

Server configuration turned out to be more complicated than expected. In the current state, the FreeBSD package runs Frp as the nobody user, meaning that it cannot read external files owned by root such as a TLS key. In a perfect world, Frp would start as root, would read the TLS key and certificate then would switch to a non-privileged user. Infortunately Frp does not support that.

Of course we could let Frp run as root, but there is a much better solution. Since I already use NGINX, what about simply letting it handle TLS? This way Frp can run as a non-privileged user without any issue.

Let’s make it happen! First we configure the Frp server; the configuration (located at /usr/local/etc/frps.ini on FreeBSD) is minimal:

[common]
bind_addr = 127.0.0.1
bind_port = 6001

proxy_bind_addr = 127.0.0.1
vhost_http_port = 8042

authentication_method = token
token = 12345678

We bind both the main interface and the proxy interface to a local address since we are going to use NGINX as front. We also require token authentication from any Frp client. Of course do not forget to replace 12345678 by a proper random password.

You may also want to patch the rc script at /usr/local/etc/rc.d/frps to run Frps as a dedicated user, and make sure that the configuration file is owned by it with permissions 0600.

NGINX

For NGINX, we need a more complex configuration.

We first need a block to define a HTTPS interface which will be used to access what is running on our workstation:

https {
    server {
        listen 443 ssl;
        listen [::]:443 ssl;

        server_name dev.example.com;

        ssl_certificate /etc/ssl/certs/dev.example.com.crt;
        ssl_certificate_key /etc/ssl/private/dev.example.com.key;

        access_log /var/log/nginx/access-dev.log combined;

        location / {
            proxy_pass http://localhost:8042;

            proxy_set_header Host $host;
            proxy_set_header X-Request-Id $request_id;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Obviously you will have to adjust the server name and SSL certificate file paths (my certificate comes from Let’s Encrypt and is generated by Lego).

Then we need a TCP interface to connect to the control port of the Frps server. NGINX can do just that with the stream module. On FreeBSD it is compiled as a dynamic library, so we have to load it first.

load_module /usr/local/libexec/nginx/ngx_stream_module.so;

stream {
    upstream frps {
        server 127.0.0.1:6001;
    }

    server {
        listen 6000 ssl;

        ssl_certificate /etc/ssl/certs/dev.example.com.crt;
        ssl_certificate_key /etc/ssl/private/dev.example.com.key;

        proxy_pass frps;
    }
}

I was positively surprised by how simple it is. Again, NGINX saves the day.

The resulting architecture is a bit convoluted, but it works.

Network diagram

Configuring the client

As Ngrok, Frp requires a client to run on your workstation. Fortunately it is already available on AUR for Archlinux, so installing it was trivial:

git clone https://aur.archlinux.org/frp.git
cd frp
makepkg
sudo pacman -U ./frpc-0.48.0-1-x86_64.pkg.tar.zst

The default path for the configuration file is ./frpc.ini which is a really bad idea. Ideally you would want a standard absolute location such as ~/.frpc.ini or ~/.config/frpc.ini. But we work we what we have.

I went with a small script to run the client, and stored it at ~/bin/frpc since ~/bin/ is my PATH environment variable.

#!/bin/sh

exec /usr/bin/frpc --config $HOME/.frpc/frpc.ini "$@"

This way I can run frpc without having to worry about the path to the configuration file. And in the future, if I end up needing several configurations, I can easily add options to the script to select the right file.

The configuration itself is quite simple:

[common]
server_addr = server.example.com
server_port = 6000

tls_enable = true
disable_custom_tls_first_byte = true

authentication_method = token
token = 12345678

[web]
type = http
local_port = 8080
custom_domains = dev.example.com

In the common section, we indicate how to contact the Frp server and how to authenticate. You will notice the strange disable_custom_tls_first_byte option. Frp uses a single byte sent by the client to identify whether the connection is going to use TLS or not, allowing it to support both TLS and non-TLS connections on the same port. Since we use NGINX as TLS interface, we need to instruct the client not to send this first byte.

In the web section, we declare our web proxy: we will access our local application on dev.example.com, and we expect it to listen locally on port 8080. Obviously you will have to choose a domain you control and make sure it points to the Frp server.

Done!

At this point, the only thing left is to start the Frp client on your workstation. It will connect to the Frp server, and you will then be able to publicly access your local application from the domain specified.

Communication between the Frp client and server is secured with TLS, and the application is exposed using HTTPS, both thanks to NGINX. All good!

You should also have a look at the Frp readme because it can do much more than just proxying HTTP traffic. OIDC authentication, builtin admin interface, Prometheus metrics, load balancing, you probably will find something useful to you.

Share the word!

Liked my article? Follow me on Twitter or on Mastodon to see what I'm up to.