1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 concatStringsSep
11 isBool
12 mapAttrs
13 mkEnableOption
14 mkIf
15 mkOption
16 mkPackageOption
17 mkRenamedOptionModule
18 types
19 ;
20
21 cfg = config.services.redlib;
22
23 args = concatStringsSep " " ([
24 "--port ${toString cfg.port}"
25 "--address ${cfg.address}"
26 ]);
27
28 boolToString' = b: if b then "on" else "off";
29in
30{
31 imports = [
32 (mkRenamedOptionModule
33 [
34 "services"
35 "libreddit"
36 ]
37 [
38 "services"
39 "redlib"
40 ]
41 )
42 ];
43
44 options = {
45 services.redlib = {
46 enable = mkEnableOption "Private front-end for Reddit";
47
48 package = mkPackageOption pkgs "redlib" { };
49
50 address = mkOption {
51 default = "0.0.0.0";
52 example = "127.0.0.1";
53 type = types.str;
54 description = "The address to listen on";
55 };
56
57 port = mkOption {
58 default = 8080;
59 example = 8000;
60 type = types.port;
61 description = "The port to listen on";
62 };
63
64 openFirewall = mkOption {
65 type = types.bool;
66 default = false;
67 description = "Open ports in the firewall for the redlib web interface";
68 };
69
70 settings = lib.mkOption {
71 type = lib.types.submodule {
72 freeformType =
73 with types;
74 attrsOf (
75 nullOr (oneOf [
76 bool
77 int
78 str
79 ])
80 );
81 options = { };
82 };
83 default = { };
84 description = ''
85 See [GitHub](https://github.com/redlib-org/redlib/tree/main?tab=readme-ov-file#configuration) for available settings.
86 '';
87 };
88 };
89 };
90
91 config = mkIf cfg.enable {
92 systemd.packages = [ cfg.package ];
93 systemd.services.redlib = {
94 wantedBy = [ "default.target" ];
95 environment = mapAttrs (_: v: if isBool v then boolToString' v else toString v) cfg.settings;
96 serviceConfig = {
97 # Hardening
98 LockPersonality = true;
99 MemoryDenyWriteExecute = true;
100 NoNewPrivileges = true;
101 PrivateDevices = true;
102 PrivateIPC = true;
103 PrivateTmp = true;
104 ProcSubset = "pid";
105 ProtectClock = true;
106 ProtectControlGroups = true;
107 ProtectHome = true;
108 ProtectHostname = true;
109 ProtectKernelLogs = true;
110 ProtectKernelModules = true;
111 ProtectKernelTunables = true;
112 ProtectProc = "invisible";
113 ProtectSystem = "full";
114 RemoveIPC = true;
115 RestrictAddressFamilies = [
116 "AF_INET"
117 "AF_INET6"
118 ];
119 RestrictNamespaces = true;
120 RestrictRealtime = true;
121 RestrictSUIDSGID = true;
122 SystemCallArchitectures = "native";
123 SystemCallFilter = [
124 "~@mount"
125 "~@swap"
126 "~@resources"
127 "~@reboot"
128 "~@raw-io"
129 "~@obsolete"
130 "~@module"
131 "~@debug"
132 "~@cpu-emulation"
133 "~@clock"
134 "~@privileged"
135 ];
136 UMask = "0027";
137
138 ExecStart = [
139 ""
140 "${lib.getExe cfg.package} ${args}"
141 ];
142 }
143 // (
144 if (cfg.port < 1024) then
145 {
146 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
147 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
148 }
149 else
150 {
151 # A private user cannot have process capabilities on the host's user
152 # namespace and thus CAP_NET_BIND_SERVICE has no effect.
153 PrivateUsers = true;
154 CapabilityBoundingSet = false;
155 }
156 );
157 };
158
159 networking.firewall = mkIf cfg.openFirewall {
160 allowedTCPPorts = [ cfg.port ];
161 };
162 };
163
164 meta = {
165 maintainers = with lib.maintainers; [ Guanran928 ];
166 };
167}