diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 98548f040816..f993a9c3cbd6 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -12,6 +12,8 @@ - [tranquil](https://tangled.org/tranquil.farm/tranquil-pds) is an ATProto PDS (personal data server) implementation in Rust. A featureful, spec conscious and community driven alternative to the Bluesky reference implementation PDS. Available as [services.tranquil-pds](#opt-services.tranquil-pds.enable). +- [FlapAlerted](https://github.com/Kioubit/FlapAlerted), detects BGP flapping events and provides statistics based on BGP update messages. Available as [services.flap-alerted](#opt-services.flap-alerted.enable). + ## Backward Incompatibilities {#sec-release-26.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index eb21ab718578..3ba8703f8555 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1017,6 +1017,7 @@ ./services/monitoring/das_watchdog.nix ./services/monitoring/datadog-agent.nix ./services/monitoring/do-agent.nix + ./services/monitoring/flap-alerted.nix ./services/monitoring/fluent-bit.nix ./services/monitoring/fusion-inventory.nix ./services/monitoring/gatus.nix diff --git a/nixos/modules/services/monitoring/flap-alerted.nix b/nixos/modules/services/monitoring/flap-alerted.nix new file mode 100644 index 000000000000..03c20f5b562b --- /dev/null +++ b/nixos/modules/services/monitoring/flap-alerted.nix @@ -0,0 +1,147 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.flap-alerted; + + settingsArgs = lib.pipe cfg.settings [ + (lib.mapAttrsToList ( + name: value: + if value == null || value == false then + [ ] + else if value == true then + [ "-${name}" ] + else + [ + "-${name}" + (toString value) + ] + )) + lib.concatLists + ]; +in + +{ + meta.maintainers = with lib.maintainers; [ defelo ]; + + options.services.flap-alerted = { + enable = lib.mkEnableOption "FlapAlerted"; + + package = lib.mkPackageOption pkgs "flap-alerted" { }; + + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + example = [ "/run/secrets/flap-alerted.env" ]; + description = '' + Files to load environment variables from. + This is useful to avoid putting secrets into the nix store. + See for a list of options. + ''; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + Extra command line arguments to pass to FlapAlerted. + See for a list of options. + ''; + default = [ ]; + }; + + settings = lib.mkOption { + description = '' + Configuration of FlapAlerted. + See for a list of options. + ''; + default = { }; + + type = lib.types.submodule { + freeformType = lib.types.attrsOf ( + lib.types.nullOr ( + lib.types.oneOf [ + lib.types.str + lib.types.int + lib.types.bool + ] + ) + ); + + options = { + asn = lib.mkOption { + type = lib.types.ints.u32; + description = "Your ASN number"; + }; + + bgpListenAddress = lib.mkOption { + type = lib.types.str; + description = "Address to listen on for incoming BGP connections"; + default = ":1790"; + }; + + debug = lib.mkOption { + type = lib.types.bool; + description = "Enable debug mode (produces a lot of output)"; + default = false; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.flap-alerted = { + wantedBy = [ "multi-user.target" ]; + + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + serviceConfig = { + User = "flap-alerted"; + Group = "flap-alerted"; + DynamicUser = true; + + EnvironmentFile = cfg.environmentFiles; + + ExecStart = lib.escapeShellArgs ([ (lib.getExe cfg.package) ] ++ settingsArgs ++ cfg.extraArgs); + + # Hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 663f65595661..1dea04b0c19a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -597,6 +597,7 @@ in firezone = runTest ./firezone/firezone.nix; fish = runTest ./fish.nix; flannel = runTestOn [ "x86_64-linux" ] ./flannel.nix; + flap-alerted = runTest ./flap-alerted.nix; flaresolverr = runTest ./flaresolverr.nix; flood = runTest ./flood.nix; fluent-bit = runTest ./fluent-bit.nix; diff --git a/nixos/tests/flap-alerted.nix b/nixos/tests/flap-alerted.nix new file mode 100644 index 000000000000..176de1be5157 --- /dev/null +++ b/nixos/tests/flap-alerted.nix @@ -0,0 +1,128 @@ +{ config, lib, ... }: + +{ + name = "flap-alerted"; + meta.maintainers = with lib.maintainers; [ defelo ]; + + nodes.machine = { + services.flap-alerted = { + enable = true; + settings = { + asn = 4213370001; + bgpListenAddress = ":1790"; + routeChangeCounter = 5; + overThresholdTarget = 1; + }; + }; + + services.bird = { + enable = true; + preCheckConfig = '' + mkdir -p /tmp/bird + touch /tmp/bird/routes.conf + ''; + config = '' + router id 192.168.1.1; + + protocol device { } + + protocol bgp flapalerted { + local 2001:db8:1::1 as 4213370001; + neighbor ::1 as 4213370001 port 1790; + + ipv4 { + add paths on; + export all; + import none; + extended next hop on; + }; + + ipv6 { + add paths on; + export all; + import none; + }; + } + + protocol static { + include "/tmp/bird/routes.conf"; + + ipv4 { + import all; + export none; + }; + } + ''; + }; + + systemd.services.bird.serviceConfig.BindReadOnlyPaths = [ "/tmp/bird" ]; + + systemd.tmpfiles.settings.bird-static-routes."/tmp/bird/routes.conf".f = { }; + }; + + interactive.sshBackdoor.enable = true; + interactive.defaults.virtualisation.graphics = false; + + interactive.nodes.machine = { + services.flap-alerted.settings.httpAPIListenAddress = ":8699"; + networking.firewall.allowedTCPPorts = [ 8699 ]; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 8699; + guest.port = 8699; + } + ]; + }; + + testScript = '' + import json + import random + import time + + machine.log(machine.succeed("systemd-analyze security flap-alerted.service --threshold=11 --no-pager")) + + machine.wait_for_unit("bird.service") + machine.wait_for_unit("flap-alerted.service") + machine.wait_for_open_port(1790) + machine.wait_for_open_port(8699) + + resp = json.loads(machine.succeed("curl localhost:8699/capabilities")) + expected_version = "v${config.nodes.machine.services.flap-alerted.package.version}" + assert resp["Version"] == expected_version + + for _ in range(10): + resp = json.loads(machine.succeed("curl localhost:8699/sessions")) + if len(resp) == 1: break + time.sleep(1) + else: + assert False, "failed to establish bgp session" + assert resp[0]["RouterID"] == "192.168.1.1" + + resp = json.loads(machine.succeed("curl localhost:8699/flaps/active/compact")) + assert resp == [] + + def flap(): + route = lambda idx, gw: f""" + route 10.0.{idx}.0/24 via 10.254.254.{gw} dev \"eth0\" onlink {{ + bgp_path.prepend(4213370002); + bgp_path.prepend({4213370002 + gw}); + }}; + """ + with open("routes.conf", "w") as f: + for i in range(1, 4): # stable routes + f.write(route(i, i)) + for i in range(4, 7): # flappy routes + f.write(route(i, random.randint(1, 254))) + machine.copy_from_host("routes.conf", "/tmp/bird/routes.conf") + machine.succeed("birdc configure") + + t = time.time() + while time.time() - t < 70: + flap() + time.sleep(1) + + resp = json.loads(machine.succeed("curl localhost:8699/flaps/active/compact")) + assert sorted(x["Prefix"] for x in resp) == ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] + ''; +} diff --git a/pkgs/by-name/fl/flap-alerted/package.nix b/pkgs/by-name/fl/flap-alerted/package.nix new file mode 100644 index 000000000000..8430e2cfb3e6 --- /dev/null +++ b/pkgs/by-name/fl/flap-alerted/package.nix @@ -0,0 +1,66 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, + versionCheckHook, + nix-update-script, + nixosTests, + + # modules (https://github.com/Kioubit/FlapAlerted#module-documentation) + withHttpApi ? true, + withLog ? true, + withScript ? true, + withWebhook ? true, + withCollector ? true, + withHistory ? true, + withRoaFilter ? false, +}: + +buildGoModule (finalAttrs: { + pname = "flap-alerted"; + version = "4.5.0"; + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "Kioubit"; + repo = "FlapAlerted"; + tag = "v${finalAttrs.version}"; + hash = "sha256-D4+FLAMt/cHXCks4GQI33ymbZIHzBajpvKU6QQntofk="; + }; + + vendorHash = null; + + env.CGO_ENABLED = 0; + + ldflags = [ + "-s" + "-w" + "-X main.Version=v${finalAttrs.version}" + ]; + + tags = + lib.optionals (!withHttpApi) [ "disable_mod_httpAPI" ] + ++ lib.optionals (!withLog) [ "disable_mod_log" ] + ++ lib.optionals (!withScript) [ "disable_mod_script" ] + ++ lib.optionals (!withWebhook) [ "disable_mod_webhook" ] + ++ lib.optionals (!withCollector) [ "disable_mod_collector" ] + ++ lib.optionals (!withHistory) [ "disable_mod_history" ] + ++ lib.optionals withRoaFilter [ "mod_roaFilter" ]; + + nativeInstallCheckInputs = [ versionCheckHook ]; + doInstallCheck = true; + + passthru = { + updateScript = nix-update-script { }; + tests = { inherit (nixosTests) flap-alerted; }; + }; + + meta = { + description = "BGP Update based flap detection & statistics"; + homepage = "https://github.com/Kioubit/FlapAlerted"; + changelog = "https://github.com/Kioubit/FlapAlerted/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.agpl3Plus; + maintainers = with lib.maintainers; [ defelo ]; + mainProgram = "FlapAlerted"; + }; +})