Run Your Own Instant Messaging Service on FreeBSD
What if you could host your own instant messaging service for you and your friends, to communicate privately and securely, away from the prying eyes of big tech? Turns out you can, and it’s actually quite easy to do.
When I grew up instant messaging wasn’t really a thing, mainly because few people had internet connectivity at home. While the ability to communicate with others on the computer is as old as the 1960s, and the Internet Relay Chat dates back to the 1980s, instant messaging as we know it today only really took off in the late 90s. Back at the time, ICQ, AIM and Yahoo! Messenger were the most prominent names in that domain. I remember most of my friends and family being on ICQ, with only few (more technical) people running mIRC to hang out on Freenoe, Undernet, euIRC and the QuakeNet.
I’d say the late-1990s was the time when the instant messaging goldrush began, and while it dimmed down throughout the early 2000s and was in parts overtaken by SMS and – in the corporate world – by BBM, it really took off with smartphones becoming a thing in 2009. That’s also when WhatsApp launched their service, with others like Kik and Facebook Messenger following suit shortly after.
The IM market today looks vastly different from the one twenty years ago. I don’t know anyone using ICQ, even though the service has been re-launched (over and over again) and is still available to this day. And while AIM, MSN and other messengers have shared a similar fate, there’s one example that has successfully stood the test of time: XMPP, or Jabber.
XMPP isn’t a service, as for example AIM or the MSN Messeger have been, but an open protocol. It specifies the way individual implementations of XMPP are supposed to communicate with each other, similar to how SMTP specifies the communication between individual mail services. As with mail software, there are various proprietary implementations that use(d) XMPP for their service, like for example Google Talk, and there are a large number of open source implementations, allowing everyone to set up and run their own XMPP service. And with XMPP being federated, it allows individually run services to communicate with each other, making it possible for one user on service A to reach out to another user on service B – similarly to how an @hotmail.com user could write e-mails to an @gmail.com user, even though they were both using different services.
XMPP these days supports a wide variety of extensions to the original protocol, called XEPs, allowing it to implement and make use of modern features like video calling or end-to-end encryption, while still remaining backward compatible (to certain degrees).
With that said, let’s have a look at how we could set up our own instant messaging service that uses E2EE (end-to-end encryption) and could totally replace WhatsApp, Telegram or Signal for our family, friends and ourselves!
Server
I’m going to use a Vultr VPS instance with FreeBSD 13.1, as it offers a solid basis for this setup as well as an up to date version of ejabberd – the XMPP implementation that we’re going to use. While I would have preferred to use OpenBSD, there’s no package of the XMPP server available there.
After launching the instance and SSHing into it, we’re first going to update the base system:
root@msg:~ # freebsd-update fetch install
We’re also taking care to update all installed packages:
root@msg:~ # pkg update && pkg upgrade
Let’s also reboot the system to apply kernel updates:
root@msg:~ # reboot
When I set up the VM, Vultr only had the FreeBSD 12 image available, hence we can perform an upgrade to 13.1. This is only necessary if the current FreeBSD version is older than the one you’re intending to use (13.1 in this case):
root@msg:~ # freebsd-update -r 13.1-RELEASE upgrade
root@msg:~ # freebsd-update install
root@msg:~ # reboot
root@msg:~ # freebsd-update install
root@msg:~ # pkg-static upgrade -f pkg
root@msg:~ # pkg bootstrap -f
root@msg:~ # pkg update && pkg upgrade
root@msg:~ # freebsd-update install
Next, let’s install a couple of handy tools:
root@msg:~ # pkg install tmux neovim mosh rsync
We are now able to reconnect using mosh
instead of SSH, and use tmux to launch
multiple shells on a single connection. Let’s continue by installing and
configuring ejabberd:
root@msg:~ # pkg install ejabberd
We will have to set up the DNS first. I’ll be using the domain
msg.example.com
for our instant messaging service. Replace it with your
actual domain. Also, if you’re looking for further details on the ejabberd
configuration, please refer to the official documentation.
msg.example.com. 2400 IN A 44.78.114.101
conference.msg.example.com. 2400 IN A 44.78.114.101
proxy.msg.example.com. 2400 IN A 44.78.114.101
pubsub.msg.example.com. 2400 IN A 44.78.114.101
upload.msg.example.com. 2400 IN A 44.78.114.101
stun.msg.example.com. 2400 IN A 44.78.114.101
_stun._udp.msg.example.com. 2400 IN SRV 0 0 3478 stun.msg.example.com.
_stun._tcp.msg.example.com. 2400 IN SRV 0 0 3478 stun.msg.example.com.
_stuns._tcp.msg.example.com. 2400 IN SRV 0 0 5349 stun.msg.example.com.
turn.msg.example.com. 2400 IN A 44.78.114.101
_turn._udp.msg.example.com. 2400 IN SRV 0 0 3478 turn.msg.example.com.
_turn._tcp.msg.example.com. 2400 IN SRV 0 0 3478 turn.msg.example.com.
_turns._tcp.msg.example.com. 2400 IN SRV 0 0 5349 turn.msg.example.com.
The A records are pointing to our Vultr instance’s IPv4 address. If you have an IPv6 address also add the AAAA records in a similar fashion.
Some registrars won’t allow you to properly input the SRV entries due to bugs in their UIs. If that’s the case you can skip these entries for now, as those are only really necessary for voice/video calling, which we won’t dive into here. You can find more info on these topics here.
Next we’re going to take a look at the configuration of ejabberd. You can find
the yaml file under /usr/local/etc/ejabberd/ejabberd.yml
(including a
.example
config in the same folder). I’m assuming that you’ll be using an
ejabberd version >= 22.05 here. By default ejabberd already comes with a solid
and sane configuration, however, we need to adjust a few values to make them fit
our setup. First, change the hosts
value to contain the domain you’ll be using
the Jabber server under:
hosts:
- msg.example.com
Next, configure the database and auth method to use Erlang’s own mnesia database, so that we won’t have to deal with a dedicated database. Unless you’re looking to serve dozens of users, mnesia will work just fine:
default_db: mnesia
new_sql_schema: true
host_config:
msg.example.com:
auth_method: mnesia
Under listen:
we now have to make sure that the individual listeners are
available from outside. With the IPv4 setup here you can set the ip:
value to
0.0.0.0
for all listeners but ejabberd_http
as well as mod_mqtt
. For these
two, set the ip:
value to 127.0.0.1
in order to only have them listen on the
local interface.
Additionally, make sure to set tls: true
for all listeners that listen on
0.0.0.0
.
Next, let’s add an admin user that we name root
:
acl:
admin:
user: root
local:
user_regexp: ""
loopback:
ip:
- 127.0.0.0/8
- ::1/128
The access_rules
should already contain rules for admin users.
Last but not least, we’re going to configure mod_mam
as well as mod_push
for
now. For mod_mam
we set the following configuration:
mod_mam:
db_type: mnesia
For mod_push
we do the following, as we do not want message content to be sent
through push notifications. If you prefer not pushing the sender name either,
disable include_sender
:
mod_push:
include_body: "New message!"
include_sender: true
We’re basically done with the groundwork. Obviously there are plenty of other things to adjust and tune – see below – however, with these changes we should get ejabberd up and running for once.
Let’s enable ejabberd through the /etc/rc.conf
by appending the following
line:
ejabberd_enable="YES"
We also need to configure /usr/local/etc/relayd.conf
to forward requests on
port 80 to 5280, which is required for issuing LetsEncrypt certificates:
ipv4="44.78.114.101"
#ipv6=""
table <ejabberd_http> { 127.0.0.1 }
relay www4 {
listen on $ipv4 port http
forward to <ejabberd_http> port 5280
}
#relay www6 {
# listen on $ipv6 port http
#
# forward to <ejabberd_http> port 5280
#}
We are now able to (re)start relayd and launch ejabberd with the following commands:
root@msg:~ # service relayd restart
root@msg:~ # service ejabberd start
To see what’s going on, we can use the following command:
root@msg:~ # tail -f /var/log/ejabberd/ejabberd.log
Now let’s add the root
user:
root@msg:~ # ejabberdctl register root msg.example.com 'my_password_here'
With this user, you should now be able to login on
https://msg.example.com:5443/admin/
and use the web interface to create
additional users and check the status of ejabberd. You can/should restrict
access to the admin interface.
The ejabberd should now also be listening on msg.example.com:5222. After you’ve
created yourself a regular user through the admin interface you can try
connecting to it using an XMPP client, by entering <user>@msg.example.com
as
username and the password you’ve assigned to the user.
Before we dive into the client config though, I suggest adding the following
crontab
entry:
root@msg:~ # crontab -l
0 * * * * find /var/spool/ejabberd/upload -type f -cmin +60 -exec rm -rf {} \;
This crontab command checks the ejabberd’s upload
folder – that’s where all
files that are being transferred from one client to the other go – for files
that are older than 60 minutes and deletes them. This is a safety/privacy
enhancement that I would recommend doing, in order to not leave message
attachments (e.g. photos or videos that were sent from one client to the other)
on the server. XMPP clients usually download and cache attachments locally,
meaning that as soon as a message was picked up from the server, the client will
likely have the attachment cached locally, not requiring the file to be on the
server anymore. The downside is of course that, if your client won’t pick up the
message within 60 minutes, you won’t see the attachment anymore, only the
message itself. Feel free to fine-tune the rule to make it work for you. At the
end of the day, all attachments that are uploaded onto the XMPP will be
encrypted anyway, meaning that even if your server gets compromised, the data
might be of little use to the attacker.
Clients
There are a large number of clients to pick from and I won’t be able to cover every client in existence here. However, here’s a short list of my favorites:
- Profanity: TUI client for *n.x, macOS, Windows, and Android (via Termux)
- Dino: GUI client for *n.x
- Gajim: GUI client for *n.x, macOS, and Windows
- Conversations: GUI client for Android (no GSF/GCM/FCM needed)
- Beagle IM: GUI client for macOS
- Siskin IM: GUI client for iOS (has Push Notifications)
I have tested all of these clients with ejabberd and can confirm that messaging
works extremely well. Each of these clients supports OMEMO encryption, which is
a must for this sort of setup. In addition, the Siskin IM iOS client even
supports push notifications, which however are sent through a third party
server – which is why we adjusted the mod_push
configuration to not send
message content.
Noteworthy clients that I haven’t tested/used in a long time are:
Configuration of any of these clients should be pretty straightforward. Usually
simply logging in with user@msg.example.com
and the password is sufficient for
the client to know what to do.
Integrations
With XMPP being an open protocol with plenty client libraries around, there are a large number of integrations – like bridges and bots – available. matterbridge is a well-known example in that regard.
In case you’re using the Pushover service, you might find value in one of my own projects: pushover-to-xmpp, a lightweight bridge that forwards Pushover notifications to XMPP users.
Important ToDos
Obviously this is a super basic setup that should help you get going without having to invest too much time right away. This setup can be used to play around with XMPP, invite a few friends and see whether this might work as an alternative solution to the privacy-invasive instant messengers you might otherwise be using.
In order to turn this into a reliable and secure messaging platform however, there are some more things that need to be done:
- Disable SSH access for root, set different port
- Set up log monitoring, fail2ban
pkg install lynis
, runlynis audit system -Q
and follow hints- Restrict access to the admin console
- Limit S2S connectivity to only servers you want to intentionally peer with
(
access_rules.s2s
) - Set up a firewall, configure anti-spamming/-brute-forcing rules
- Fine-tune ejabberd configuration, disable not needed features, set up an actual database backend if higher load is expected – Vultr offers managed Postgres
- Set up Tor connectivity for increased privacy
- Add redundancy, in case downtimes are intolerable
- Dive into
security(7)
and the handbook
As implementing these things require an understanding of how the service will be used, it makes sense to start lightweight and continuously iterate/extend the setup over time.
Have fun messaging!
Enjoyed this? Support me via Monero, Bitcoin, Lightning, or Ethereum! More info.