Exposing Local Web-socket connection Securely with FRP & Caddy

Karan Singh
7 min readApr 20, 2023

A step-by-step implementation guide

Caddy and FRPC for securely exposing ports
Caddy and FRPC architecture for securely exposing ports

Understanding Caddy and FRP

What is Caddy

Caddy is simplistic , open-source web server and reverse proxy written in Go. It provides automatic HTTPS, automatic HTTP/2, and automatic TLS certificate issuance and renewal using the Let’s Encrypt CA. Caddy is designed to be fast and efficient with a small memory footprint. It comes with a user-friendly configuration file format that makes it easy to set up and manage web services. Caddy also includes many useful features such as virtual hosting, URL rewriting, proxying, rate limiting, logging, and more.

What is FRP

FrpFast Reverse Proxy” is a high-performance reverse proxy and network tunneling tool written in Go. It allows users to expose services behind NATs and firewalls to the internet securely and easily. frp uses a client-server architecture, where the client runs on the server behind the NAT or firewall, and the server runs on a public machine. The client connects to the server and creates a secure tunnel that allows traffic to flow between the server and the client. This allows users to access services such as web servers, SSH servers, and databases that would otherwise be inaccessible from the internet. frp is lightweight and easy to set up, with a simple configuration file format that allows users to customize its behaviour to their needs.

Both Caddy and frp are powerful tools for managing web services and providing secure access to services behind firewalls and NATs. Caddy adds an automatic TLS and HTTPS certificate layer to FRP’s network tunnels, combination of both Caddy and FRPs makes it a fantastic production grade solution.

Implementation

Step 1: Set up a Server with a Public IP and Install Docker and Docker-Compose, this will be used for both FRP and Caddy . In our case, we will be using an EC2 instance. Once you have a server, you need to install Docker and Docker-Compose. These tools will help us manage the containers we will be using.

Step 2: Create DNS Records for Your Domain

To create DNS records, you will need to have a registered domain and a DNS server. In our case, we will be using AWS Route53. Create two DNS A records for your domain:

  1. caddy.example.com → Instance Public IP
  2. *.caddy.example.com → Instance Public IP

These DNS records will allow traffic to route to our server.

Step 3: Create a Docker Compose File

In this step, you will create a Docker Compose file that will allow you to run both Caddy and frps containers. This file will define the configuration and settings for each container.

The docker-compose.yml file should look like this:

version: "3.7"
services:
caddy:
container_name: caddy
image: caddy:latest
restart: always
network_mode: "host"
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
frps:
container_name: frps
image: snowdreamtech/frps
restart: always
network_mode: "host"
volumes:
- $PWD/frps.ini:/etc/frp/frps.ini

This file defines two services: caddy and frps. The caddy service uses the caddy:latest Docker image and mounts the Caddyfile configuration file from the current directory to /etc/caddy/Caddyfile inside the container. The frps service uses the snowdreamtech/frps Docker image and mounts the frps.ini configuration file from the current directory to /etc/frp/frps.ini inside the container.

Step 4: Create a Caddyfile

Create a new file named Caddyfile and add the following contents:

dashboard.caddy.example.in {
reverse_proxy localhost:7500 {
transport http {
keepalive 60m
keepalive_interval 10s
}
}
}

client-30001.caddy.example.in {
reverse_proxy localhost:30001 {
transport http {
keepalive 60m
keepalive_interval 10s
}
}
}

This Caddyfile contains routes to our websocket connections. The dashboard route will allow us to access the frps dashboard. The client routes will allow us to route traffic to our local websocket connections.

Step-5: Create frps.ini with the following contents

The frps.ini file contains the configuration for the frp server. It specifies the common configuration for all the tunnels and the ports to allow. Here’s what each parameter does:

  • bind_port: This is the port that the frp server will listen on.
  • subdomain_host: This is the subdomain that you registered on your DNS provider.
  • tls_enable: This enables the use of TLS for secure communication.
  • token: This is the password that the frp client uses to authenticate with the frp server.
  • dashboard_addr: This specifies the IP address that the frp dashboard will listen on.
  • dashboard_port: This is the port that the frp dashboard will listen on.
  • dashboard_user and dashboard_pwd: These are the username and password for accessing the frp dashboard.
  • log_file: This is the file path for the frp server log file.
  • log_level: This is the logging level for the frp server.
  • allow_ports: This specifies the range of ports that the frp server will allow for tunneling.
  • max_ports_per_client: This specifies the maximum number of ports allowed per client.

Create a new file named frps.ini and paste the following contents:

[common]
bind_port = 7000
subdomain_host = caddy.example.in
tls_enable = true
token = IamPassword
dashboard_addr = 0.0.0.0
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = frps@admin@001
log_file = /var/log/frps.log
log_level = debug
log_max_days = 3
allow_ports = 30001-30025
max_ports_per_client = 2

Step-6: Using docker-compose start the containers and verify containers and the ports on the server

With the docker-compose.yml , Caddyfile and frps.in configurations in place, you can now start the containers using the following command:

docker-compose up -d

Once the containers are running, you can verify that they are up and listening on the correct ports using the following command:

docker ps
netstat -plunt

The docker command will display a list of all the running containers on your server. You should see two containers named caddy and frps. And the netstat command will display a list of all the open ports on your server. You should see ports 80 and 443 for Caddy and ports 7500 and 7000 for frps.

Step-7: Open ports 443,30001–30005

To allow external clients to connect to the frp server, you need to open the ports 443, 30001–30005 (or any other ports that you have allowed in your frps.ini file) on your server. The method for opening ports varies depending on your cloud provider and operating system.

Step-8: Verify that the server configuration is completed

Once you have opened the necessary ports, the server configuration is complete. You should now be able to securely expose a web-socket connection (or any other service) running locally or in a NAT environment using frpc (FRP Client)

Step-9: Create the frp client configuration

To connect to the frp server from a client machine, you need to create a frp client configuration file frpc.ini. This configuration file specifies the frp server address, port, and the tunnel configurations.

[common]
server_addr = caddy.example.in
server_port = 7000
tls_enable = true
token = IamPassword

[client-30001]
type = tcp
local_ip = 127.0.0.1
local_port = 8888
remote_port = 30001
use_encryption = true
use_compression = true

Step 10: Start the frpc client

Once you have created the frpc.ini file, it’s time to start the client. Navigate to the directory where you downloaded the frpc binary and execute the following command:

./frpc -c frpc.ini

This command starts the frpc client and connects it to the frps server using the configuration specified in the frpc.ini file. If everything goes well, you should see a message saying that the client has successfully connected to the server.

Step 11: Verify the client port

Now that the frpc client is connected to the frps server, it’s time to verify that the port we specified in the frpc.ini file is listening on the server. To do this, log in to your server and execute the following command:

netstat -plunt | grep -i 30001

If everything is working as expected, you should see a line of output similar to the following:

tcp        0      0 0.0.0.0:30001           0.0.0.0:*               LISTEN      1234/frps

This output shows that port 30001 is open and listening on the server, and it’s being handled by the frps process.

Step 12: Create a local WebSocket server

Before we can test our connection, we need to create a local WebSocket server that we can connect to. For this demonstration, we will create a simple WebSocket server using Python and the websockets library. Create a new file called websocket_server.py and add the following code:

import asyncio
import websockets

async def handler(websocket, path):
data = await websocket.recv()
reply = f"Data received as: {data}!"
await websocket.send(reply)

print("Server started")
start_server = websockets.serve(handler, "localhost", 8888)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

This code creates a simple WebSocket server that listens on localhost:8888. Whenever a client connects to the server, the handler function is called, which reads the incoming data, generates a response, and sends it back to the client.

Step 13: Run the local WebSocket server

To start the local WebSocket server, navigate to the directory where you saved the websocket_server.py file and execute the following command:

python3 websocket_server.py

This command starts the local WebSocket server, which is now ready to accept incoming connections.

Step 14: Test the local WebSocket server

Now that the local WebSocket server is running, it’s time to test it to make sure it’s working correctly. You can use a tool like Postman or any other WebSocket client to connect to the server. In this example, we will use the wscat command-line tool to connect to the server.

To install wscat, execute the following command:

wscat -c ws://localhost:8888

This command connects to the WebSocket server running on localhost:8888. Once the connection is established, you can send messages to the server and see the responses.

Step 15: Verify that the local WebSocket server running on localhost:8888 has been exposed to the internet securely by connecting to wss://client-30001.caddy.example.in using the WebSocket client. If this is connected successfully, then you’re all good!

Congratulations! You have successfully exposed a local WebSocket connection securely to the internet using frp and Caddy. With this setup, you can easily expose multiple local WebSocket connections or any other local server with ease and security.

Overall, this setup is pretty simple and secure. The frp and Caddy setup allows for easy and secure exposing of local WebSocket connections without the need for complicated configurations. This makes it an ideal solution for developers who want to expose their local WebSocket connections without compromising their security.

If you have any questions or comments about this setup, please feel free to leave them in the comments section below.

Happy Exposing Applications !

--

--

Karan Singh

Co-Founder & CTO @ Scogo ♦ I Love to solve problems using Tech