diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 781734a189a8..72fee8d16d02 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -40,6 +40,14 @@
services.sourcehut.
+
+
+ ucarp,
+ an userspace implementation of the Common Address Redundancy
+ Protocol (CARP). Available as
+ networking.ucarp.
+
+
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 3a29b29f1063..1aa22b3a4b6f 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -18,6 +18,10 @@ In addition to numerous new and upgraded packages, this release has the followin
development. Available as
[services.sourcehut](options.html#opt-services.sourcehut.enable).
+* [ucarp](https://download.pureftpd.org/pub/ucarp/README), an userspace
+ implementation of the Common Address Redundancy Protocol (CARP). Available as
+ [networking.ucarp](options.html#opt-networking.ucarp.enable).
+
## Backward Incompatibilities
* The `staticjinja` package has been upgraded from 1.0.4 to 2.0.0
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 2938f79fb16a..1a4c2fb719dc 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -839,6 +839,7 @@
./services/networking/tox-node.nix
./services/networking/toxvpn.nix
./services/networking/tvheadend.nix
+ ./services/networking/ucarp.nix
./services/networking/unbound.nix
./services/networking/unifi.nix
./services/networking/v2ray.nix
diff --git a/nixos/modules/services/networking/ucarp.nix b/nixos/modules/services/networking/ucarp.nix
new file mode 100644
index 000000000000..9b19a19687bc
--- /dev/null
+++ b/nixos/modules/services/networking/ucarp.nix
@@ -0,0 +1,183 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.networking.ucarp;
+
+ ucarpExec = concatStringsSep " " (
+ [
+ "${cfg.package}/bin/ucarp"
+ "--interface=${cfg.interface}"
+ "--srcip=${cfg.srcIp}"
+ "--vhid=${toString cfg.vhId}"
+ "--passfile=${cfg.passwordFile}"
+ "--addr=${cfg.addr}"
+ "--advbase=${toString cfg.advBase}"
+ "--advskew=${toString cfg.advSkew}"
+ "--upscript=${cfg.upscript}"
+ "--downscript=${cfg.downscript}"
+ "--deadratio=${toString cfg.deadratio}"
+ ]
+ ++ (optional cfg.preempt "--preempt")
+ ++ (optional cfg.neutral "--neutral")
+ ++ (optional cfg.shutdown "--shutdown")
+ ++ (optional cfg.ignoreIfState "--ignoreifstate")
+ ++ (optional cfg.noMcast "--nomcast")
+ ++ (optional (cfg.extraParam != null) "--xparam=${cfg.extraParam}")
+ );
+in {
+ options.networking.ucarp = {
+ enable = mkEnableOption "ucarp, userspace implementation of CARP";
+
+ interface = mkOption {
+ type = types.str;
+ description = "Network interface to bind to.";
+ example = "eth0";
+ };
+
+ srcIp = mkOption {
+ type = types.str;
+ description = "Source (real) IP address of this host.";
+ };
+
+ vhId = mkOption {
+ type = types.ints.between 1 255;
+ description = "Virtual IP identifier shared between CARP hosts.";
+ example = 1;
+ };
+
+ passwordFile = mkOption {
+ type = types.str;
+ description = "File containing shared password between CARP hosts.";
+ example = "/run/keys/ucarp-password";
+ };
+
+ preempt = mkOption {
+ type = types.bool;
+ description = ''
+ Enable preemptive failover.
+ Thus, this host becomes the CARP master as soon as possible.
+ '';
+ default = false;
+ };
+
+ neutral = mkOption {
+ type = types.bool;
+ description = "Do not run downscript at start if the host is the backup.";
+ default = false;
+ };
+
+ addr = mkOption {
+ type = types.str;
+ description = "Virtual shared IP address.";
+ };
+
+ advBase = mkOption {
+ type = types.ints.unsigned;
+ description = "Advertisement frequency in seconds.";
+ default = 1;
+ };
+
+ advSkew = mkOption {
+ type = types.ints.unsigned;
+ description = "Advertisement skew in seconds.";
+ default = 0;
+ };
+
+ upscript = mkOption {
+ type = types.path;
+ description = ''
+ Command to run after become master, the interface name, virtual address
+ and optional extra parameters are passed as arguments.
+ '';
+ example = ''
+ pkgs.writeScript "upscript" '''
+ #!/bin/sh
+ $\{pkgs.iproute2\}/bin/ip addr add "$2"/24 dev "$1"
+ ''';
+ '';
+ };
+
+ downscript = mkOption {
+ type = types.path;
+ description = ''
+ Command to run after become backup, the interface name, virtual address
+ and optional extra parameters are passed as arguments.
+ '';
+ example = ''
+ pkgs.writeScript "downscript" '''
+ #!/bin/sh
+ $\{pkgs.iproute2\}/bin/ip addr del "$2"/24 dev "$1"
+ ''';
+ '';
+ };
+
+ deadratio = mkOption {
+ type = types.ints.unsigned;
+ description = "Ratio to consider a host as dead.";
+ default = 3;
+ };
+
+ shutdown = mkOption {
+ type = types.bool;
+ description = "Call downscript at exit.";
+ default = false;
+ };
+
+ ignoreIfState = mkOption {
+ type = types.bool;
+ description = "Ignore interface state, e.g., down or no carrier.";
+ default = false;
+ };
+
+ noMcast = mkOption {
+ type = types.bool;
+ description = "Use broadcast instead of multicast advertisements.";
+ default = false;
+ };
+
+ extraParam = mkOption {
+ type = types.nullOr types.str;
+ description = "Extra parameter to pass to the up/down scripts.";
+ default = null;
+ };
+
+ package = mkOption {
+ type = types.package;
+ description = ''
+ Package that should be used for ucarp.
+
+ Please note that the default package, pkgs.ucarp, has not received any
+ upstream updates for a long time and can be considered as unmaintained.
+ '';
+ default = pkgs.ucarp;
+ defaultText = "pkgs.ucarp";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.ucarp = {
+ description = "ucarp, userspace implementation of CARP";
+
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ Type = "exec";
+ ExecStart = ucarpExec;
+
+ ProtectSystem = "strict";
+ ProtectHome = true;
+ PrivateTmp = true;
+ ProtectClock = true;
+ ProtectKernelModules = true;
+ ProtectControlGroups = true;
+ MemoryDenyWriteExecute = true;
+ RestrictRealtime = true;
+ };
+ };
+ };
+
+ meta.maintainers = with lib.maintainers; [ oxzi ];
+}