/ module.nix
module.nix
  1  { lib, ... }:
  2  
  3  let
  4    indentation = n: lib.strings.replicate n "  ";
  5    indent =
  6      n:
  7      lib.flip lib.pipe [
  8        (lib.splitString "\n")
  9        (map (s: lib.optionalString (lib.match " *" s == null) (indentation n + s)))
 10        (lib.concatStringsSep "\n")
 11      ];
 12  
 13    nestedListOf =
 14      t:
 15      lib.types.listOf (lib.types.either t (nestedListOf t))
 16      // {
 17        description = "nested ${(lib.types.listOf t).description}";
 18      };
 19  
 20    chainSubmodule =
 21      { name, config, ... }:
 22      {
 23        options = {
 24          enable = lib.mkOption {
 25            type = lib.types.bool;
 26            description = "Enable this chain.";
 27            default = true;
 28          };
 29  
 30          name = lib.mkOption {
 31            type = lib.types.str;
 32            description = "Chain name. Defaults to the attribute name.";
 33          };
 34  
 35          type = lib.mkOption {
 36            type = lib.types.nullOr (
 37              lib.types.enum [
 38                "filter"
 39                "nat"
 40                "route"
 41              ]
 42            );
 43            description = "Chain type.";
 44            default = null;
 45          };
 46  
 47          hook = lib.mkOption {
 48            type = lib.types.nullOr (
 49              lib.types.enum [
 50                "ingress"
 51                "prerouting"
 52                "forward"
 53                "input"
 54                "output"
 55                "postrouting"
 56                "egress"
 57              ]
 58            );
 59            description = "Chain hook.";
 60            default = null;
 61          };
 62  
 63          priority = lib.mkOption {
 64            type = lib.types.nullOr lib.types.int;
 65            description = "Chain priority.";
 66            default = null;
 67          };
 68  
 69          policy = lib.mkOption {
 70            type = lib.types.nullOr (
 71              lib.types.enum [
 72                "accept"
 73                "drop"
 74              ]
 75            );
 76            description = "Chain policy.";
 77            default = null;
 78          };
 79  
 80          rules = lib.mkOption {
 81            type = nestedListOf lib.types.str;
 82            apply = lib.flatten;
 83            description = "Chain rules.";
 84            default = [ ];
 85          };
 86  
 87          header = lib.mkOption {
 88            type = lib.types.nullOr lib.types.str;
 89            description = "Chain header. Constructed by other options if not overridden.";
 90            default = null;
 91            example = "type filter hook input priority 0; policy drop;";
 92          };
 93  
 94          content = lib.mkOption {
 95            type = lib.types.lines;
 96            description = "Chain content. Constructed by other options if not overridden.";
 97          };
 98  
 99          defaultRules = {
100            enable = lib.mkEnableOption "default rules depending on the chain type and hook";
101  
102            ct_state = lib.mkOption {
103              type = lib.types.bool;
104              description = "Accept related/established, drop invalid. Applies if type = filter.";
105            };
106  
107            icmpv6 = lib.mkOption {
108              type = lib.types.bool;
109              description = "Accept basic ICMPv6 request types. Applies if type = filter && hook = input.";
110            };
111  
112            icmp_pings = lib.mkOption {
113              type = lib.types.bool;
114              description = "Accept ICMP echo requests (\"pings\").";
115              default = false;
116            };
117  
118            lo = lib.mkOption {
119              type = lib.types.bool;
120              description = "Accept traffic from `lo` (loopback). Applies if type = filter && hook = input.";
121            };
122          };
123        };
124  
125        config = {
126          name = lib.mkDefault name;
127  
128          header = lib.mkIf (config.type != null || config.hook != null) (
129            "type ${config.type} hook ${config.hook} priority ${toString (lib.defaultTo 0 config.priority)};"
130            + lib.optionalString (config.policy != null) " policy ${config.policy};"
131          );
132  
133          content = lib.concatStringsSep "\n" (
134            lib.optional (config.header != null) config.header
135            ++ config.rules
136            ++ lib.optional (config.policy != null && config.header == null) config.policy
137          );
138  
139          defaultRules = lib.mapAttrs (_: x: lib.mkDefault (config.defaultRules.enable && x)) {
140            ct_state =
141              config.type == "filter"
142              && lib.elem config.hook [
143                "input"
144                "forward"
145                "output"
146              ];
147  
148            icmpv6 = config.type == "filter" && config.hook == "input";
149  
150            lo = config.type == "filter" && config.hook == "input";
151          };
152  
153          rules = lib.mkBefore (
154            lib.optionals config.defaultRules.ct_state [
155              "ct state vmap { related : accept, established : accept, invalid : drop }"
156            ]
157            ++ lib.optionals config.defaultRules.icmpv6 [
158              "icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept"
159            ]
160            ++ lib.optionals config.defaultRules.icmp_pings [
161              "icmp type echo-request accept"
162              "icmpv6 type echo-request accept"
163            ]
164            ++ lib.optionals config.defaultRules.lo [ "iif lo accept" ]
165          );
166        };
167      };
168  
169    tableSubmodule =
170      { config, ... }:
171      {
172        options = {
173          chains = lib.mkOption {
174            type = lib.types.attrsOf (lib.types.submodule chainSubmodule);
175            description = "Chains to be added to the table.";
176            default = { };
177          };
178        };
179  
180        config.content = lib.mkIf (config.chains != { }) (
181          lib.pipe config.chains [
182            lib.attrValues
183            (lib.filter (chain: chain.enable))
184            (lib.concatMapStringsSep "\n" (chain: ''
185              chain ${chain.name} {
186              ${indent 1 chain.content}
187              }
188            ''))
189            (indent 1)
190            (lib.removePrefix (indentation 1))
191            (lib.removeSuffix "\n")
192          ]
193        );
194      };
195  in
196  
197  {
198    options.networking.nftables.tables = lib.mkOption {
199      type = lib.types.attrsOf (lib.types.submodule tableSubmodule);
200    };
201  }