跳转到内容

FreeBSD

此内容尚不支持你的语言。

The relay runs on FreeBSD 14 or newer. This page describes a hardened layout: the relay in a jail as an unprivileged user, pf forwarding public port 443 to it, and TLS terminated by the relay itself.

What the relay does on FreeBSD by itself: at startup it sets RLIMIT_CORE to 0 and disables tracing with procctl(PROC_TRACE_CTL), so it never writes a core file and another process of the same user cannot attach to it. It does not use Capsicum or MAC policies; isolation comes from the jail, the rchat user and the resource limits.

Inside the jail, as root, install.sh installs the release binary, the rchat user (home /var/db/rchat), /var/db/rchat (0700), /var/log/rchat (0750), /usr/local/etc/rchat and the rc script:

Terminal window
pkg install -y curl # not in the base system; install.sh keeps redirects on https with it
curl -fsSL https://assets.r-chat.net/install.sh | sh

To build from source instead, see Install (pkg install -y rust cmake provides the toolchain), then create the user and directories yourself:

Terminal window
pw useradd rchat -d /var/db/rchat -s /usr/sbin/nologin -c "rChat relay" -w no
install -o root -g wheel -m 0755 target/release/rchat-server /usr/local/bin/
install -d -o rchat -g rchat -m 0700 /var/db/rchat
install -d -o rchat -g rchat -m 0750 /var/log/rchat
install -d -o root -g rchat -m 0750 /usr/local/etc/rchat

/usr/local/etc/rchat/server.toml:

[server]
bind_addr = "127.0.1.1:8443"
websocket_path = "/CHANGE-ME-to-a-long-random-path"
relay_identity_path = "/var/db/rchat/relay_identity.key"
# Clients dial public 443 (pf rdr), not 8443: tells --print-invite.
public_port = 443
# pf forwards public :443 to this listener without terminating TLS,
# so built-in ACME (TLS-ALPN-01) works.
[server.acme]
enabled = true
domains = ["chat.example.com"]
cache_dir = "/var/db/rchat/acme"
# Optional: keep held messages across restarts, encrypted, with the key on
# the tmpfs that the rc.d script mounts. Leave persist = false for memory only.
[server.offline_queue]
ttl_secs = 259200
persist = true
persist_path = "/var/db/rchat/queue.rcq"
persist_key_path = "/var/run/rchat/queue.key"

The relay logs a reminder that Let’s Encrypt validates on public port 443; that is expected when pf forwards 443 to another port.

Create it once, as the service user, before the first start:

Terminal window
su -m rchat -c '/usr/local/bin/rchat-server --config /usr/local/etc/rchat/server.toml --init-relay-identity'
# prints: rk:1:… (version 0), the pin to give your users

See Relay identity for publishing and rotating it.

To invite people, print the relay invite (a link and a QR code with the address and the pin) as the service user:

Terminal window
su -m rchat -c '/usr/local/bin/rchat-server --config /usr/local/etc/rchat/server.toml --print-invite'

In the jail layout the relay binds 8443 but clients dial 443 through pf, so set public_port = 443 under [server] (or pass --public-port 443); otherwise the invite would carry :8443. The host comes from [server.decoy] host, else the first [server.acme] domain. See Inviting people to your relay.

Each release publishes the rc script that runs on a FreeBSD 15.1 relay as rchat.rc; install.sh installs it as /usr/local/etc/rc.d/rchat (the file name must match name="rchat"). Its contents:

#!/bin/sh
# PROVIDE: rchat
# REQUIRE: NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="rchat"
rcvar="rchat_enable"
load_rc_config $name
: ${rchat_enable:="NO"}
: ${rchat_runas:="rchat"} # not rchat_user: rc.subr would run daemon itself as that user
: ${rchat_config:="/usr/local/etc/rchat/server.toml"}
: ${rchat_log:="/var/log/rchat/server.log"}
: ${rchat_log_level:="warn"}
# daemon(8) supervises the relay: -r restarts it 5 s after it exits (an
# OOM kill included), -H reopens the log on SIGHUP for newsyslog. daemon
# runs as root, writes its pidfile (-P, the supervisor) in root-owned
# /var/run, then starts the relay as rchat_runas.
pidfile="/var/run/${name}.pid"
procname="/usr/sbin/daemon"
command="/usr/sbin/daemon"
command_args="-r -R 5 -H -o ${rchat_log} -P ${pidfile} -t rchat -u ${rchat_runas} /usr/local/bin/rchat-server --config ${rchat_config} --log-level ${rchat_log_level}"
run_rc_command "$1"
Terminal window
fetch -o rchat.rc https://assets.r-chat.net/1.1.0/rchat.rc # only if install.sh did not install it
install -o root -g wheel -m 0555 rchat.rc /usr/local/etc/rc.d/rchat
sysrc rchat_enable=YES
service rchat start

Why it looks like this:

  • The variable is rchat_runas, not rchat_user. rc.subr treats ${name}_user specially and runs the whole command, daemon(8) included, as that user. daemon(8) then cannot -u (“failed to set user environment”) and cannot write its pidfile in root-owned /var/run (“ppidfile … Permission denied”). With rchat_runas, daemon(8) starts as root, writes the pidfile, and then starts the relay as the service user. The pidfile stays in root-owned /var/run, never in a directory the service user owns.
  • -r -R 5 makes daemon(8) supervise the relay and restart it 5 seconds after it exits (an OOM kill included). -P is the supervisor’s pid, which service rchat stop signals.
  • -H -o /var/log/rchat/server.log writes a log file that daemon(8) reopens on SIGHUP, so newsyslog can rotate it.

For the jail layout with persist = true, add a start_precmd that creates the tmpfs queue key before each start (use ${rchat_runas} for the owner, and keep the pidfile out of /var/run/rchat):

Terminal window
start_precmd="${name}_prestart"
rchat_prestart()
{
rchat_gid=$(id -g ${rchat_runas})
install -d -o ${rchat_runas} -g ${rchat_gid} -m 0700 /var/run/rchat
if ! mount -p | awk '{print $2}' | grep -qx /var/run/rchat; then
mount -t tmpfs -o mode=0700,uid=$(id -u ${rchat_runas}),gid=${rchat_gid} tmpfs /var/run/rchat
fi
if [ ! -f /var/run/rchat/queue.key ]; then
(umask 077; dd if=/dev/random of=/var/run/rchat/queue.key bs=32 count=1 2>/dev/null)
chown ${rchat_runas}:${rchat_gid} /var/run/rchat/queue.key
chmod 0400 /var/run/rchat/queue.key
fi
}

The queue data lives on ZFS (/var/db) and the key on the tmpfs: different directories and filesystems, as the relay requires. Setting allow_persistent_key = true (for example to keep the key on a GELI-encrypted disk) skips the memory-filesystem check; such a relay can no longer say that its queue key is lost on reboot.

On a dedicated VM the relay can run directly on the host and listen on :443 itself, with no jail and no pf rdr. mac_portacl(4) lets exactly one uid, the rchat user, bind TCP 443. Each release publishes provision.sh, a script (run as root) that sets this up. Read it before you run it: it replaces /etc/sysctl.conf.local and sets net.inet.ip.portrange.reservedlow and reservedhigh to 0, so that only the mac_portacl rule grants port 443. It creates:

  • the rchat user (no login shell, home /var/db/rchat) and the directories /usr/local/etc/rchat, /var/db/rchat (0700) and /var/log/rchat (0750);
  • mac_portacl_load="YES" in /boot/loader.conf and security.mac.portacl.rules=uid:<rchat uid>:tcp:443 in /etc/sysctl.conf.local, with the hardening sysctls (kern.coredump=0, security.bsd.see_other_uids=0, and so on);
  • a newsyslog entry that rotates /var/log/rchat/server.log daily and keeps 14;
  • sendmail switched off, and /tmp cleared at boot.
Terminal window
fetch https://assets.r-chat.net/1.1.0/provision.sh https://assets.r-chat.net/1.1.0/server-2gb.toml
sh provision.sh
pkg install -y curl
curl -fsSL https://assets.r-chat.net/install.sh | sh # binary and rc script
install -o root -g rchat -m 0640 server-2gb.toml /usr/local/etc/rchat/server.toml
# edit: domains, and websocket_path (a long random secret)
su -m rchat -c '/usr/local/bin/rchat-server --config /usr/local/etc/rchat/server.toml --init-relay-identity'
sysrc rchat_enable=YES
service rchat start

server-2gb.toml is a config for a 2 GB host: it binds 0.0.0.0:443, uses built-in ACME ([server.acme]) and sizes the connection and buffer budgets for that much RAM. Its websocket_path is the placeholder /CHANGE-ME-to-a-long-random-path; the relay refuses to start until you replace it.

Gotchas:

  • With security.bsd.see_other_uids=0, ps and sockstat show the relay only under sudo. An empty non-root sockstat -l does not mean the relay is down; check sudo service rchat status and sudo sockstat -4 -l -p 443.
  • Let’s Encrypt staging first: start with staging = true under [server.acme] and confirm a certificate is issued. Then stop the relay, move the staging cache aside (mv /var/db/rchat/acme /var/db/rchat/acme.staging), set staging = false and start again, so the staging account and certificate are not reused.

Forward public TCP 443 to the jail with a plain rdr. This is a TCP forward, so TLS still terminates at the relay:

ext_if = "vtnet0"
rchat_jail = "127.0.1.1"
rdr on $ext_if proto tcp from any to any port 443 -> $rchat_jail port 8443
pass in quick on $ext_if proto tcp from any to any port 443 flags S/SA keep state

rdr keeps the client’s source address, so the relay’s per-address caps and per-source fair shares work as in a direct deployment. Do not put anything that terminates TLS in front of the relay: the protocol binds key authentication to the relay’s own TLS session.