Compare commits

...

9 commits

Author SHA1 Message Date
b1c9279c63 nixos: services: add thelounge
All checks were successful
ci/woodpecker/push/check Pipeline was successful
2025-08-05 15:53:21 +02:00
30348a1e19 nixos: services: matrix: add Facebook bridge
I probably won't actually use it, after test-driving it quickly.

But now that the effort has been expanded, might as well keep it if only
as a future reference.
2025-08-05 15:53:21 +02:00
7a85a44407 nixos: services: matrix: remove obsolete comment 2025-08-05 15:53:21 +02:00
9cadbe6256 nixos: services: matrix: add admin interface 2025-08-05 15:53:21 +02:00
fa7b4910f5 nixos: services: matrix: fix proxy to synapse
I want to make use of the `/_synapse/matrix/` sub-path, so just proxy
the whole of `/_synapse/`.
2025-08-05 15:53:21 +02:00
1e31b2dfea nixos: services: matrix: simplify VHost 2025-08-05 15:53:21 +02:00
bd019258cb nixos: services: matrix: simplify listeners 2025-08-05 15:53:21 +02:00
0792e8c7cb nixos: services: matrix: fix element-web config 2025-08-05 15:53:21 +02:00
f1d7da7fcb nixos: services: matrix: refactor well-knowns 2025-08-05 15:53:21 +02:00
4 changed files with 283 additions and 107 deletions

View file

@ -38,6 +38,7 @@
./servarr ./servarr
./ssh-server ./ssh-server
./tandoor-recipes ./tandoor-recipes
./thelounge
./tlp ./tlp
./transmission ./transmission
./vikunja ./vikunja

View file

@ -0,0 +1,143 @@
# Matrix bridges for some services I use
{ config, lib, ... }:
let
cfg = config.my.services.matrix.bridges;
synapseCfg = config.services.matrix-synapse;
domain = config.networking.domain;
serverName = synapseCfg.settings.server_name;
mkBridgeOption = n: lib.mkEnableOption "${n} bridge" // { default = cfg.enable; };
mkPortOption = n: default: lib.mkOption {
type = lib.types.port;
inherit default;
example = 8080;
description = "${n} bridge port";
};
mkEnvironmentFileOption = n: lib.mkOption {
type = lib.types.str;
example = "/run/secret/matrix/${lib.toLower n}-bridge-secrets.env";
description = ''
Path to a file which should contain the secret values for ${n} bridge.
Using through the following format:
```
MATRIX_APPSERVICE_AS_TOKEN=<the_as_value>
MATRIX_APPSERVICE_HS_TOKEN=<the_hs_value>
```
Each bridge should use a different set of secrets, as they each register
their own independent double-puppetting appservice.
'';
};
in
{
options.my.services.matrix.bridges = with lib; {
enable = mkEnableOption "bridges configuration";
admin = mkOption {
type = types.str;
default = "ambroisie";
example = "admin";
description = "Local username for the admin";
};
facebook = {
enable = mkBridgeOption "Facebook";
port = mkPortOption "Facebook" 29321;
environmentFile = mkEnvironmentFileOption "Facebook";
};
};
config = lib.mkMerge [
(lib.mkIf cfg.facebook.enable {
services.mautrix-meta.instances.facebook = {
enable = true;
# Automatically register the bridge with synapse
registerToSynapse = true;
# Provide `AS_TOKEN`, `HS_TOKEN`
inherit (cfg.facebook) environmentFile;
settings = {
homeserver = {
domain = serverName;
address = "http://localhost:${toString config.my.services.matrix.port}";
};
appservice = {
hostname = "localhost";
inherit (cfg.facebook) port;
address = "http://localhost:${toString cfg.facebook.port}";
public_address = "https://facebook-bridge.${domain}";
as_token = "$MATRIX_APPSERVICE_AS_TOKEN";
hs_token = "$MATRIX_APPSERVICE_HS_TOKEN";
bot = {
username = "fbbot";
};
};
backfill = {
enabled = true;
};
bridge = {
delivery_receipts = true;
permissions = {
"*" = "relay";
${serverName} = "user";
"@${cfg.admin}:${serverName}" = "admin";
};
};
database = {
type = "postgres";
uri = "postgres:///mautrix-meta-facebook?host=/var/run/postgresql/";
};
double_puppet = {
secrets = {
${serverName} = "as_token:$MATRIX_APPSERVICE_AS_TOKEN";
};
};
network = {
# Don't be picky on Facebook/Messenger
allow_messenger_com_on_fb = true;
displayname_template = ''{{or .DisplayName .Username "Unknown user"}} (FB)'';
};
provisioning = {
shared_secret = "disable";
};
};
};
services.postgresql = {
enable = true;
ensureDatabases = [ "mautrix-meta-facebook" ];
ensureUsers = [{
name = "mautrix-meta-facebook";
ensureDBOwnership = true;
}];
};
systemd.services.mautrix-meta-facebook = {
wants = [ "postgres.service" ];
after = [ "postgres.service" ];
};
my.services.nginx.virtualHosts = {
# Proxy to the bridge
"facebook-bridge" = {
inherit (cfg.facebook) port;
};
};
})
];
}

View file

@ -1,24 +1,49 @@
# Matrix homeserver setup, using different endpoints for federation and client # Matrix homeserver setup.
# traffic. The main trick for this is defining two nginx servers endpoints for
# matrix.domain.com, each listening on different ports.
#
# Configuration shamelessly stolen from [1]
#
# [1]: https://github.com/alarsyo/nixos-config/blob/main/services/matrix.nix
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.my.services.matrix; cfg = config.my.services.matrix;
federationPort = { public = 8448; private = 11338; }; adminPkg = pkgs.synapse-admin-etkecc;
clientPort = { public = 443; private = 11339; };
domain = config.networking.domain; domain = config.networking.domain;
matrixDomain = "matrix.${domain}"; matrixDomain = "matrix.${domain}";
serverConfig = {
"m.server" = "${matrixDomain}:443";
};
clientConfig = {
"m.homeserver" = {
"base_url" = "https://${matrixDomain}";
"server_name" = domain;
};
"m.identity_server" = {
"base_url" = "https://vector.im";
};
};
# ACAO required to allow element-web on any URL to request this json file
mkWellKnown = data: ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON data}';
'';
in in
{ {
imports = [
./bridges.nix
];
options.my.services.matrix = with lib; { options.my.services.matrix = with lib; {
enable = mkEnableOption "Matrix Synapse"; enable = mkEnableOption "Matrix Synapse";
port = mkOption {
type = types.port;
default = 8448;
example = 8008;
description = "Internal port for listeners";
};
secretFile = mkOption { secretFile = mkOption {
type = with types; nullOr str; type = with types; nullOr str;
default = null; default = null;
@ -58,22 +83,22 @@ in
enable_registration = false; enable_registration = false;
listeners = [ listeners = [
# Federation
{ {
inherit (cfg) port;
bind_addresses = [ "::1" ]; bind_addresses = [ "::1" ];
port = federationPort.private; type = "http";
tls = false; # Terminated by nginx. tls = false;
x_forwarded = true; x_forwarded = true;
resources = [{ names = [ "federation" ]; compress = false; }]; resources = [
} {
names = [ "client" ];
# Client compress = true;
{ }
bind_addresses = [ "::1" ]; {
port = clientPort.private; names = [ "federation" ];
tls = false; # Terminated by nginx. compress = false;
x_forwarded = true; }
resources = [{ names = [ "client" ]; compress = false; }]; ];
} }
]; ];
@ -96,19 +121,12 @@ in
chat = { chat = {
root = pkgs.element-web.override { root = pkgs.element-web.override {
conf = { conf = {
default_server_config = { default_server_config = clientConfig;
"m.homeserver" = { show_labs_settings = true;
"base_url" = "https://${matrixDomain}"; default_country_code = "FR"; # cocorico
"server_name" = domain; room_directory = {
};
"m.identity_server" = {
"base_url" = "https://vector.im";
};
};
showLabsSettings = true;
defaultCountryCode = "FR"; # cocorico
roomDirectory = {
"servers" = [ "servers" = [
domain
"matrix.org" "matrix.org"
"mozilla.org" "mozilla.org"
]; ];
@ -116,99 +134,54 @@ in
}; };
}; };
}; };
# Dummy VHosts for port collision detection matrix = {
matrix-federation = { # Somewhat unused, but necessary for port collision detection
port = federationPort.private; inherit (cfg) port;
};
matrix-client = {
port = clientPort.private;
};
};
# Those are too complicated to use my wrapper... extraConfig = {
services.nginx.virtualHosts = { locations = {
${matrixDomain} = {
onlySSL = true;
useACMEHost = domain;
locations =
let
proxyToClientPort = {
proxyPass = "http://[::1]:${toString clientPort.private}";
};
in
{
# Or do a redirect instead of the 404, or whatever is appropriate # Or do a redirect instead of the 404, or whatever is appropriate
# for you. But do not put a Matrix Web client here! See the # for you. But do not put a Matrix Web client here! See the
# Element web section below. # Element web section below.
"/".return = "404"; "/".return = "404";
"/_matrix" = proxyToClientPort; "/_matrix".proxyPass = "http://[::1]:${toString cfg.port}";
"/_synapse/client" = proxyToClientPort; "/_synapse".proxyPass = "http://[::1]:${toString cfg.port}";
"= /admin".return = "307 /admin/";
"/admin/" = {
alias = "${adminPkg}/";
priority = 500;
tryFiles = "$uri $uri/ /index.html";
};
"~ ^/admin/.*\\.(?:css|js|jpg|jpeg|gif|png|svg|ico|woff|woff2|ttf|eot|webp)$" = {
priority = 400;
root = adminPkg;
extraConfig = ''
rewrite ^/admin/(.*)$ /$1 break;
expires 30d;
more_set_headers "Cache-Control: public";
'';
};
}; };
listen = [
{ addr = "0.0.0.0"; port = clientPort.public; ssl = true; }
{ addr = "[::]"; port = clientPort.public; ssl = true; }
];
};
# same as above, but listening on the federation port
"${matrixDomain}_federation" = {
onlySSL = true;
serverName = matrixDomain;
useACMEHost = domain;
locations."/".return = "404";
locations."/_matrix" = {
proxyPass = "http://[::1]:${toString federationPort.private}";
}; };
listen = [
{ addr = "0.0.0.0"; port = federationPort.public; ssl = true; }
{ addr = "[::]"; port = federationPort.public; ssl = true; }
];
}; };
};
# Those are too complicated to use my wrapper...
services.nginx.virtualHosts = {
"${domain}" = { "${domain}" = {
forceSSL = true; forceSSL = true;
useACMEHost = domain; useACMEHost = domain;
locations."= /.well-known/matrix/server".extraConfig = locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
let locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
server = { "m.server" = "${matrixDomain}:${toString federationPort.public}"; };
in
''
add_header Content-Type application/json;
return 200 '${builtins.toJSON server}';
'';
locations."= /.well-known/matrix/client".extraConfig =
let
client = {
"m.homeserver" = { "base_url" = "https://${matrixDomain}"; };
"m.identity_server" = { "base_url" = "https://vector.im"; };
};
# ACAO required to allow element-web on any URL to request this json file
in
''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}';
'';
}; };
}; };
# For administration tools. # For administration tools.
environment.systemPackages = [ pkgs.matrix-synapse ]; environment.systemPackages = [ pkgs.matrix-synapse ];
networking.firewall.allowedTCPPorts = [
clientPort.public
federationPort.public
];
my.services.backup = { my.services.backup = {
paths = [ paths = [
config.services.matrix-synapse.dataDir config.services.matrix-synapse.dataDir

View file

@ -0,0 +1,59 @@
# Web IRC client
{ config, lib, ... }:
let
cfg = config.my.services.thelounge;
in
{
options.my.services.thelounge = with lib; {
enable = mkEnableOption "The Lounge, a self-hosted web IRC client";
port = mkOption {
type = types.port;
default = 9050;
example = 4242;
description = "The port on which The Lounge will listen for incoming HTTP traffic.";
};
};
config = lib.mkIf cfg.enable {
services.thelounge = {
enable = true;
inherit (cfg) port;
extraConfig = {
reverseProxy = true;
};
};
my.services.nginx.virtualHosts = {
irc = {
inherit (cfg) port;
# Proxy websockets for RPC
websocketsLocations = [ "/" ];
extraConfig = {
locations."/".extraConfig = ''
proxy_read_timeout 1d;
'';
};
};
};
services.fail2ban.jails = {
thelounge = ''
enabled = true
filter = thelounge
port = http,https
'';
};
environment.etc = {
"fail2ban/filter.d/thelounge.conf".text = ''
[Definition]
failregex = Authentication failed for user .* from <HOST>$
Authentication for non existing user attempted from <HOST>$
journalmatch = _SYSTEMD_UNIT=thelounge.service
'';
};
};
}