Unverified Commit e46b352a authored by Janik's avatar Janik Committed by GitHub
Browse files

Merge pull request #231065 from yu-re-ka/fnm-advanced-module

nixos/fastnetmon-advanced: init
parents 6d3cf22d 30dcccea
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -107,6 +107,8 @@

- [NNCP](http://www.nncpgo.org/). Added nncp-daemon and nncp-caller services. Configuration is set with [programs.nncp.settings](#opt-programs.nncp.settings) and the daemons are enabled at [services.nncp](#opt-services.nncp.caller.enable).

- [FastNetMon Advanced](https://fastnetmon.com/product-overview/), a commercial high performance DDoS detector / sensor. Available as [services.fastnetmon-advanced](#opt-services.fastnetmon-advanced.enable).

- [tuxedo-rs](https://github.com/AaronErhardt/tuxedo-rs), Rust utilities for interacting with hardware from TUXEDO Computers.

- [audiobookshelf](https://github.com/advplyr/audiobookshelf/), a self-hosted audiobook and podcast server. Available as [services.audiobookshelf](#opt-services.audiobookshelf.enable).
+1 −0
Original line number Diff line number Diff line
@@ -907,6 +907,7 @@
  ./services/networking/eternal-terminal.nix
  ./services/networking/expressvpn.nix
  ./services/networking/fakeroute.nix
  ./services/networking/fastnetmon-advanced.nix
  ./services/networking/ferm.nix
  ./services/networking/firefox-syncserver.nix
  ./services/networking/fireqos.nix
+222 −0
Original line number Diff line number Diff line
{ config, lib, pkgs, ... }:

let
  # Background information: FastNetMon requires a MongoDB to start. This is because
  # it uses MongoDB to store its configuration. That is, in a normal setup there is
  # one collection with one document.
  # To provide declarative configuration in our NixOS module, this database is
  # completely emptied and replaced on each boot by the fastnetmon-setup service
  # using the configuration backup functionality.

  cfg = config.services.fastnetmon-advanced;
  settingsFormat = pkgs.formats.yaml { };

  # obtain the default configs by starting up ferretdb and fcli in a derivation
  default_configs = pkgs.runCommand "default-configs" {
    nativeBuildInputs = [
      pkgs.ferretdb
      pkgs.fastnetmon-advanced # for fcli
      pkgs.proot
    ];
  } ''
    mkdir ferretdb fastnetmon $out
    FERRETDB_TELEMETRY="disable" FERRETDB_HANDLER="sqlite" FERRETDB_STATE_DIR="$PWD/ferretdb" FERRETDB_SQLITE_URL="file:$PWD/ferretdb/" ferretdb &

    cat << EOF > fastnetmon/fastnetmon.conf
    ${builtins.toJSON {
      mongodb_username = "";
    }}
    EOF
    proot -b fastnetmon:/etc/fastnetmon -0 fcli create_configuration
    proot -b fastnetmon:/etc/fastnetmon -0 fcli set bgp default
    proot -b fastnetmon:/etc/fastnetmon -0 fcli export_configuration backup.tar
    tar -C $out --no-same-owner -xvf backup.tar
  '';

  # merge the user configs into the default configs
  config_tar = pkgs.runCommand "fastnetmon-config.tar" {
    nativeBuildInputs = with pkgs; [ jq ];
  } ''
    jq -s add ${default_configs}/main.json ${pkgs.writeText "main-add.json" (builtins.toJSON cfg.settings)} > main.json
    mkdir hostgroup
    ${lib.concatImapStringsSep "\n" (pos: hostgroup: ''
      jq -s add ${default_configs}/hostgroup/0.json ${pkgs.writeText "hostgroup-${toString (pos - 1)}-add.json" (builtins.toJSON hostgroup)} > hostgroup/${toString (pos - 1)}.json
    '') hostgroups}
    mkdir bgp
    ${lib.concatImapStringsSep "\n" (pos: bgp: ''
      jq -s add ${default_configs}/bgp/0.json ${pkgs.writeText "bgp-${toString (pos - 1)}-add.json" (builtins.toJSON bgp)} > bgp/${toString (pos - 1)}.json
    '') bgpPeers}
    tar -cf $out main.json ${lib.concatImapStringsSep " " (pos: _: "hostgroup/${toString (pos - 1)}.json") hostgroups} ${lib.concatImapStringsSep " " (pos: _: "bgp/${toString (pos - 1)}.json") bgpPeers}
  '';

  hostgroups = lib.mapAttrsToList (name: hostgroup: { inherit name; } // hostgroup) cfg.hostgroups;
  bgpPeers = lib.mapAttrsToList (name: bgpPeer: { inherit name; } // bgpPeer) cfg.bgpPeers;

in {
  options.services.fastnetmon-advanced = with lib; {
    enable = mkEnableOption "the fastnetmon-advanced DDoS Protection daemon";

    settings = mkOption {
      description = ''
        Extra configuration options to declaratively load into FastNetMon Advanced.

        See the [FastNetMon Advanced Configuration options reference](https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-configuration-options/) for more details.
      '';
      type = settingsFormat.type;
      default = {};
      example = literalExpression ''
        {
          networks_list = [ "192.0.2.0/24" ];
          gobgp = true;
          gobgp_flow_spec_announces = true;
        }
      '';
    };
    hostgroups = mkOption {
      description = "Hostgroups to declaratively load into FastNetMon Advanced";
      type = types.attrsOf settingsFormat.type;
      default = {};
    };
    bgpPeers = mkOption {
      description = "BGP Peers to declaratively load into FastNetMon Advanced";
      type = types.attrsOf settingsFormat.type;
      default = {};
    };

    enableAdvancedTrafficPersistence = mkOption {
      description = "Store historical flow data in clickhouse";
      type = types.bool;
      default = false;
    };

    traffic_db.settings = mkOption {
      type = settingsFormat.type;
      description = "Additional settings for /etc/fastnetmon/traffic_db.conf";
    };
  };

  config = lib.mkMerge [ (lib.mkIf cfg.enable {
    environment.systemPackages = with pkgs; [
      fastnetmon-advanced # for fcli
    ];

    environment.etc."fastnetmon/license.lic".source = "/var/lib/fastnetmon/license.lic";
    environment.etc."fastnetmon/gobgpd.conf".source = "/run/fastnetmon/gobgpd.conf";
    environment.etc."fastnetmon/fastnetmon.conf".source = pkgs.writeText "fastnetmon.conf" (builtins.toJSON {
      mongodb_username = "";
    });

    services.ferretdb.enable = true;

    systemd.services.fastnetmon-setup = {
      wantedBy = [ "multi-user.target" ];
      after = [ "ferretdb.service" ];
      path = with pkgs; [ fastnetmon-advanced config.systemd.package ];
      script = ''
        fcli create_configuration
        fcli delete hostgroup global
        fcli import_configuration ${config_tar}
        systemctl --no-block try-restart fastnetmon
      '';
      serviceConfig.Type = "oneshot";
    };

    systemd.services.fastnetmon = {
      wantedBy = [ "multi-user.target" ];
      after = [ "ferretdb.service" "fastnetmon-setup.service" "polkit.service" ];
      path = with pkgs; [ iproute2 ];
      unitConfig = {
        # Disable logic which shuts service when we do too many restarts
        # We do restarts from sudo fcli commit and it's expected that we may have many restarts
        # Details: https://github.com/systemd/systemd/issues/2416
        StartLimitInterval = 0;
      };
      serviceConfig = {
        ExecStart = "${pkgs.fastnetmon-advanced}/bin/fastnetmon --log_to_console";

        LimitNOFILE = 65535;
        # Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
        Restart= "on-failure";
        RestartSec= "5s";

        DynamicUser = true;
        CacheDirectory = "fastnetmon";
        RuntimeDirectory = "fastnetmon"; # for gobgpd config
        StateDirectory = "fastnetmon"; # for license file
      };
    };

    security.polkit.enable = true;
    security.polkit.extraConfig = ''
      polkit.addRule(function(action, subject) {
        if (action.id == "org.freedesktop.systemd1.manage-units" &&
          subject.isInGroup("fastnetmon")) {
          if (action.lookup("unit") == "gobgp.service") {
            var verb = action.lookup("verb");
            if (verb == "start" || verb == "stop" || verb == "restart") {
              return polkit.Result.YES;
            }
          }
        }
      });
    '';

    # We don't use the existing gobgp NixOS module and package, because the gobgp
    # version might not be compatible with fastnetmon. Also, the service name
    # _must_ be 'gobgp' and not 'gobgpd', so that fastnetmon can reload the config.
    systemd.services.gobgp = {
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
      description = "GoBGP Routing Daemon";
      unitConfig = {
        ConditionPathExists = "/run/fastnetmon/gobgpd.conf";
      };
      serviceConfig = {
        Type = "notify";
        ExecStartPre = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf -d";
        SupplementaryGroups = [ "fastnetmon" ];
        ExecStart = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -f /run/fastnetmon/gobgpd.conf --sdnotify";
        ExecReload = "${pkgs.fastnetmon-advanced}/bin/fnm-gobgpd -r";
        DynamicUser = true;
        AmbientCapabilities = "cap_net_bind_service";
      };
    };
  })

  (lib.mkIf (cfg.enable && cfg.enableAdvancedTrafficPersistence) {
    ## Advanced Traffic persistence
    ## https://fastnetmon.com/docs-fnm-advanced/fastnetmon-advanced-traffic-persistency/

    services.clickhouse.enable = true;

    services.fastnetmon-advanced.settings.traffic_db = true;

    services.fastnetmon-advanced.traffic_db.settings = {
      clickhouse_batch_size = lib.mkDefault 1000;
      clickhouse_batch_delay = lib.mkDefault 1;
      traffic_db_host = lib.mkDefault "127.0.0.1";
      traffic_db_port = lib.mkDefault 8100;
      clickhouse_host = lib.mkDefault "127.0.0.1";
      clickhouse_port = lib.mkDefault 9000;
      clickhouse_user = lib.mkDefault "default";
      clickhouse_password = lib.mkDefault "";
    };
    environment.etc."fastnetmon/traffic_db.conf".text = builtins.toJSON cfg.traffic_db.settings;

    systemd.services.traffic_db = {
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
      serviceConfig = {
        ExecStart = "${pkgs.fastnetmon-advanced}/bin/traffic_db";
        # Restart service when it fails due to any reasons, we need to keep processing traffic no matter what happened
        Restart= "on-failure";
        RestartSec= "5s";

        DynamicUser = true;
      };
    };

  }) ];

  meta.maintainers = lib.teams.wdz.members;
}
+1 −0
Original line number Diff line number Diff line
@@ -248,6 +248,7 @@ in {
  ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
  ecryptfs = handleTest ./ecryptfs.nix {};
  fscrypt = handleTest ./fscrypt.nix {};
  fastnetmon-advanced = runTest ./fastnetmon-advanced.nix;
  ejabberd = handleTest ./xmpp/ejabberd.nix {};
  elk = handleTestOn ["x86_64-linux"] ./elk.nix {};
  emacs-daemon = handleTest ./emacs-daemon.nix {};
+65 −0
Original line number Diff line number Diff line
{ pkgs, lib, ... }:

{
  name = "fastnetmon-advanced";
  meta.maintainers = lib.teams.wdz.members;

  nodes = {
    bird = { ... }: {
      networking.firewall.allowedTCPPorts = [ 179 ];
      services.bird2 = {
        enable = true;
        config = ''
          router id 192.168.1.1;

          protocol bgp fnm {
            local 192.168.1.1 as 64513;
            neighbor 192.168.1.2 as 64514;
            multihop;
            ipv4 {
              import all;
              export none;
            };
          }
        '';
      };
    };
    fnm = { ... }: {
      networking.firewall.allowedTCPPorts = [ 179 ];
      services.fastnetmon-advanced = {
        enable = true;
        settings = {
          networks_list = [ "172.23.42.0/24" ];
          gobgp = true;
          gobgp_flow_spec_announces = true;
        };
        bgpPeers = {
          bird = {
            local_asn = 64514;
            remote_asn = 64513;
            local_address = "192.168.1.2";
            remote_address = "192.168.1.1";

            description = "Bird";
            ipv4_unicast = true;
            multihop = true;
            active = true;
          };
        };
      };
    };
  };

  testScript = { nodes, ... }: ''
    start_all()
    fnm.wait_for_unit("fastnetmon.service")
    bird.wait_for_unit("bird2.service")

    fnm.wait_until_succeeds('journalctl -eu fastnetmon.service | grep "BGP daemon restarted correctly"')
    fnm.wait_until_succeeds("journalctl -eu gobgp.service | grep BGP_FSM_OPENCONFIRM")
    bird.wait_until_succeeds("birdc show protocol fnm | grep Estab")
    fnm.wait_until_succeeds('journalctl -eu fastnetmon.service | grep "API server listening"')
    fnm.succeed("fcli set blackhole 172.23.42.123")
    bird.succeed("birdc show route | grep 172.23.42.123")
  '';
}
Loading