1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 # Background information: FastNetMon requires a MongoDB to start. This is because
10 # it uses MongoDB to store its configuration. That is, in a normal setup there is
11 # one collection with one document.
12 # To provide declarative configuration in our NixOS module, this database is
13 # completely emptied and replaced on each boot by the fastnetmon-setup service
14 # using the configuration backup functionality.
15
16 cfg = config.services.fastnetmon-advanced;
17 settingsFormat = pkgs.formats.yaml { };
18
19 # obtain the default configs by starting up ferretdb and fcli in a derivation
20 default_configs =
21 pkgs.runCommand "default-configs"
22 {
23 nativeBuildInputs = [
24 pkgs.ferretdb
25 pkgs.fastnetmon-advanced # for fcli
26 pkgs.proot
27 ];
28 }
29 ''
30 mkdir ferretdb fastnetmon $out
31 FERRETDB_TELEMETRY="disable" FERRETDB_HANDLER="sqlite" FERRETDB_STATE_DIR="$PWD/ferretdb" FERRETDB_SQLITE_URL="file:$PWD/ferretdb/" ferretdb &
32
33 cat << EOF > fastnetmon/fastnetmon.conf
34 ${builtins.toJSON {
35 mongodb_username = "";
36 }}
37 EOF
38 proot -b fastnetmon:/etc/fastnetmon -0 fcli create_configuration
39 proot -b fastnetmon:/etc/fastnetmon -0 fcli set bgp default
40 proot -b fastnetmon:/etc/fastnetmon -0 fcli export_configuration backup.tar
41 tar -C $out --no-same-owner -xvf backup.tar
42 '';
43
44 # merge the user configs into the default configs
45 config_tar =
46 pkgs.runCommand "fastnetmon-config.tar"
47 {
48 nativeBuildInputs = with pkgs; [ jq ];
49 }
50 ''
51 jq -s add ${default_configs}/main.json ${pkgs.writeText "main-add.json" (builtins.toJSON cfg.settings)} > main.json
52 mkdir hostgroup
53 ${lib.concatImapStringsSep "\n" (pos: hostgroup: ''
54 jq -s add ${default_configs}/hostgroup/0.json ${pkgs.writeText "hostgroup-${toString (pos - 1)}-add.json" (builtins.toJSON hostgroup)} > hostgroup/${toString (pos - 1)}.json
55 '') hostgroups}
56 mkdir bgp
57 ${lib.concatImapStringsSep "\n" (pos: bgp: ''
58 jq -s add ${default_configs}/bgp/0.json ${pkgs.writeText "bgp-${toString (pos - 1)}-add.json" (builtins.toJSON bgp)} > bgp/${toString (pos - 1)}.json
59 '') bgpPeers}
60 tar -cf $out main.json ${
61 lib.concatImapStringsSep " " (pos: _: "hostgroup/${toString (pos - 1)}.json") hostgroups
62 } ${lib.concatImapStringsSep " " (pos: _: "bgp/${toString (pos - 1)}.json") bgpPeers}
63 '';
64
65 hostgroups = lib.mapAttrsToList (name: hostgroup: { inherit name; } // hostgroup) cfg.hostgroups;
66 bgpPeers = lib.mapAttrsToList (name: bgpPeer: { inherit name; } // bgpPeer) cfg.bgpPeers;
67
68in
69{
70 options.services.fastnetmon-advanced = with lib; {
71 enable = mkEnableOption "the fastnetmon-advanced DDoS Protection daemon";
72
73 settings = mkOption {
74 description = ''
75 Extra configuration options to declaratively load into FastNetMon Advanced.
76
77 See the [FastNetMon Advanced Configuration options reference](https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-configuration-options/) for more details.
78 '';
79 type = settingsFormat.type;
80 default = { };
81 example = literalExpression ''
82 {
83 networks_list = [ "192.0.2.0/24" ];
84 gobgp = true;
85 gobgp_flow_spec_announces = true;
86 }
87 '';
88 };
89 hostgroups = mkOption {
90 description = "Hostgroups to declaratively load into FastNetMon Advanced";
91 type = types.attrsOf settingsFormat.type;
92 default = { };
93 };
94 bgpPeers = mkOption {
95 description = "BGP Peers to declaratively load into FastNetMon Advanced";
96 type = types.attrsOf settingsFormat.type;
97 default = { };
98 };
99
100 enableAdvancedTrafficPersistence = mkOption {
101 description = "Store historical flow data in clickhouse";
102 type = types.bool;
103 default = false;
104 };
105
106 traffic_db.settings = mkOption {
107 type = settingsFormat.type;
108 description = "Additional settings for /etc/fastnetmon/traffic_db.conf";
109 };
110 };
111
112 config = lib.mkMerge [
113 (lib.mkIf cfg.enable {
114 environment.systemPackages = with pkgs; [
115 fastnetmon-advanced # for fcli
116 ];
117
118 environment.etc."fastnetmon/license.lic".source = "/var/lib/fastnetmon/license.lic";
119 environment.etc."fastnetmon/gobgpd.conf".source = "/run/fastnetmon/gobgpd.conf";
120 environment.etc."fastnetmon/fastnetmon.conf".source = pkgs.writeText "fastnetmon.conf" (
121 builtins.toJSON {
122 mongodb_username = "";
123 }
124 );
125
126 services.ferretdb.enable = true;
127
128 systemd.services.fastnetmon-setup = {
129 wantedBy = [ "multi-user.target" ];
130 after = [ "ferretdb.service" ];
131 path = with pkgs; [
132 fastnetmon-advanced
133 config.systemd.package
134 ];
135 script = ''
136 fcli create_configuration
137 fcli delete hostgroup global
138 fcli import_configuration ${config_tar}
139 systemctl --no-block try-restart fastnetmon
140 '';
141 serviceConfig.Type = "oneshot";
142 };
143
144 systemd.services.fastnetmon = {
145 wantedBy = [ "multi-user.target" ];
146 after = [
147 "ferretdb.service"
148 "fastnetmon-setup.service"
149 "polkit.service"
150 ];
151 path = with pkgs; [ iproute2 ];
152 unitConfig = {
153 # Disable logic which shuts service when we do too many restarts
154 # We do restarts from sudo fcli commit and it's expected that we may have many restarts
155 # Details: https://github.com/systemd/systemd/issues/2416
156 StartLimitInterval = 0;
157 };
158 serviceConfig = {
159 ExecStart = "${pkgs.fastnetmon-advanced}/bin/fastnetmon --log_to_console";
160
161 LimitNOFILE = 65535;
162 # Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
163 Restart = "on-failure";
164 RestartSec = "5s";
165
166 DynamicUser = true;
167 CacheDirectory = "fastnetmon";
168 RuntimeDirectory = "fastnetmon"; # for gobgpd config
169 StateDirectory = "fastnetmon"; # for license file
170 };
171 };
172
173 security.polkit.enable = true;
174 security.polkit.extraConfig = ''
175 polkit.addRule(function(action, subject) {
176 if (action.id == "org.freedesktop.systemd1.manage-units" &&
177 subject.isInGroup("fastnetmon")) {
178 if (action.lookup("unit") == "gobgp.service") {
179 var verb = action.lookup("verb");
180 if (verb == "start" || verb == "stop" || verb == "restart") {
181 return polkit.Result.YES;
182 }
183 }
184 }
185 });
186 '';
187 # dbus/polkit with DynamicUser is broken with the default implementation
188 services.dbus.implementation = "broker";
189
190 # We don't use the existing gobgp NixOS module and package, because the gobgp
191 # version might not be compatible with fastnetmon. Also, the service name
192 # _must_ be 'gobgp' and not 'gobgpd', so that fastnetmon can reload the config.
193 systemd.services.gobgp = {
194 wantedBy = [ "multi-user.target" ];
195 after = [ "network.target" ];
196 description = "GoBGP Routing Daemon";
197 unitConfig = {
198 ConditionPathExists = "/run/fastnetmon/gobgpd.conf";
199 };
200 serviceConfig = {
201 Type = "notify";
202 ExecStartPre = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf -d";
203 SupplementaryGroups = [ "fastnetmon" ];
204 ExecStart = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf --sdnotify";
205 ExecReload = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -r";
206 DynamicUser = true;
207 AmbientCapabilities = "cap_net_bind_service";
208 };
209 };
210 })
211
212 (lib.mkIf (cfg.enable && cfg.enableAdvancedTrafficPersistence) {
213 ## Advanced Traffic persistence
214 ## https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-traffic-persistency/
215
216 services.clickhouse.enable = true;
217
218 services.fastnetmon-advanced.settings.traffic_db = true;
219
220 services.fastnetmon-advanced.traffic_db.settings = {
221 clickhouse_batch_size = lib.mkDefault 1000;
222 clickhouse_batch_delay = lib.mkDefault 1;
223 traffic_db_host = lib.mkDefault "127.0.0.1";
224 traffic_db_port = lib.mkDefault 8100;
225 clickhouse_host = lib.mkDefault "127.0.0.1";
226 clickhouse_port = lib.mkDefault 9000;
227 clickhouse_user = lib.mkDefault "default";
228 clickhouse_password = lib.mkDefault "";
229 };
230 environment.etc."fastnetmon/traffic_db.conf".text = builtins.toJSON cfg.traffic_db.settings;
231
232 systemd.services.traffic_db = {
233 wantedBy = [ "multi-user.target" ];
234 after = [ "network.target" ];
235 serviceConfig = {
236 ExecStart = "${pkgs.fastnetmon-advanced}/bin/traffic_db";
237 # Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
238 Restart = "on-failure";
239 RestartSec = "5s";
240
241 DynamicUser = true;
242 };
243 };
244
245 })
246 ];
247
248 meta.maintainers = lib.teams.wdz.members;
249}