Cloudflare's TURN Service as signaling server of Pairdrop deploying on QNAP


Pairdrop is a popular file sharing app based on web browser. You can send files and text messages to devices on local network or even over the Internet. The data transfers point-to-point, but NAT cross ability rely on a signaling server(信号服务器)that running TURN service.

I installed Pairdrop docker on my QNAP(威联通)NAS for private file transfering for long time, here I’ll take a note of experiences to make Internet Transfer happen without heavy effort.

Based on the offical document, we have 3 options to make use of TURN server:

  1. Install coturn on NAS directly or deploy through docker compose together with Pairdrop. Doc
  2. Specify off-the-shelf signaling server from public pairdrop service. This way, devices visiting to different domain can discover each other. Doc
  3. Use a free, pre-configured TURN server, such as OpenRelay, Google and Cloudflare.

While following the first process, I found it a tough job to install coturn under QNAP. The container station stucked for long time and fail with the message

[Container Station] Failed to create application “pairdrop”. Error message: operateApp action [–project-name pairdrop up -d –remove-orphans] failed: exit status 1: Network pairdrop_default Creating
Network pairdrop_default Created
Container pairdrop-coturn_server-1 Creating
Container pairdrop Creating
Container pairdrop Created
Container pairdrop-coturn_server-1 Created
Container pairdrop Starting
Container pairdrop-coturn_server-1 Starting
Container pairdrop Started
Error response from daemon: driver failed programming external connectivity on endpoint pairdrop-coturn_server-1 (9d64f6b67960878a2cc22fffc7ad11e7faff279d163fe49953ba1577eae7941e): listen udp4 0.0.0.0:13773: bind: address already in use

Instead, I installed coturn separately and finally success. The network mode was set to host for convenience. Compare to cloudflare, self-hosted TURN server seems more stable.

As the second process, The Pairdrop instance is running with webRTC enabled, but it can’t connect to pairdrop.net TURN server for unknown reason.

So I try to register OpenRelay account to get a free plan credential. Unfortunately, It also fail to connect most probability due to the network restriction in China. Finally, I’ve found Cloudflare Calls TURN Service which is available since May 2024. Any account has a free tier of 1,000 GB before $0.05 per GB of data used. That’s enough for personal usage.

Here’s the docker parameter I used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
services:
pairdrop:
image: "lscr.io/linuxserver/pairdrop:latest"
container_name: pairdrop
restart: unless-stopped
volumes:
- /share/Container/pairdrop/rtc_config.json:/home/node/app/rtc_config.json
environment:
- PUID=1000 # UID to run the application as
- PGID=100 # GID to run the application as
- WS_FALLBACK=true # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
- RATE_LIMIT=true # Set to true to limit clients to 1000 requests per 5 min.
- RTC_CONFIG=/home/node/app/rtc_config.json # Set to the path of a file that specifies the STUN/TURN servers.
- DEBUG_MODE=false # Set to true to debug container and peer connections.
- TZ=Asia/Shanghai # Time Zone
ports:
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`

Note: WS_FALLBACK and RTC_CONFIG cannot be used together with SIGNALING_SERVER

And RTC_CONFIG is like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"sdpSemantics": "unified-plan",
"iceServers": [
{
"urls": "stun:stun.cloudflare.com:3478"
},
{
"urls": "turn:turn.cloudflare.com:3478?transport=udp",
"username": "xxx",
"credential": "yyy"
},
{
"urls": "turn:turn.cloudflare.com:3478?transport=tcp",
"username": "xxx",
"credential": "yyy"
},
{
"urls": "turns:turn.cloudflare.com:5349?transport=tcp",
"username": "xxx",
"credential": "yyy"
}
]
}

The credential is not persistence, so I use Cronicle to setup a cron job to update rtc_config.json and restart Pairdrop periodly:

#!/usr/bin/env bash

cd /share/Container/pairdrop
cf=$(curl -s -X POST -H "Authorization: Bearer xxxxxxxxx" -H "Content-Type: application/json" -d '{"ttl": 604800}' https://rtc.live.cloudflare.com/v1/turn/keys/xxxxxxxxx/credentials/generate)
if jq -e . >/dev/null 2>&1 <<<"$cf"; then
jq --argjson cf "$cf" '.iceServers[] |= . + { username: $cf.iceServers.username, credential: $cf.iceServers.credential }' rtc_config.json > rtc_config.tmp && mv rtc_config.tmp rtc_config.json
/share/CACHEDEV1_DATA/.qpkg/container-station/bin/docker restart pairdrop-1
else
echo "Failed to parse JSON"
fi

Also, don’t forget to append Apache reverse proxy rule with websocket enabled:

1
2
3
4
5
6
7
8
9
10
11
<VirtualHost *:5049>
ServerName <DOMAIN>
SSLEngine on
SSLCipherSuite EECDH+CHACHA20:EECDH+AES
SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCertificateFile "<path to crt>"
SSLCertificateKeyFile "<path to key>"
ProxyPreserveHost on
ProxyPass / http://localhost:3000/ upgrade=websocket connectiontimeout=60 timeout=60
ProxyPassReverse / http://localhost:3000/
</VirtualHost>

Now, I can make two devices visible each other on the self-hosted Pairdrop app, such as mobile phone under 5G and PC under Wifi. However, the file transfer process is not quit stable, especially for large files. and the speed is also slow, only 500kbps on my side. Not sure if it relates to cloudflare or Pairdrop. And the maintainer are workin on the issue now, the preview next version improve a lot.

Anyway, it canbe used as online chatroom or send small file quickly and temporarily, not only on local network.

FastSend and pp快传 are similar tool build on webRTC. In my mind, the UI are more simple and practical than Pairdrop. But the connection is also unstable. Hope more powerful tool appears to make sharing directly simple and easy. No app, No intermediate server, Web-based and stable. For now, only bluetooth meet the goals, but it’s slow, very slow.