Compare commits

..

1 Commits

@ -1,40 +1,5 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1727060013,
"narHash": "sha256-/fC5YlJy4IoAW9GhkJiwyzk0K/gQd9Qi4rRcoweyG9E=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6b40cc876c929bfe1e3a24bf538ce3b5622646ba",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1727196810,
"narHash": "sha256-xQzgXRlczZoFfrUdA4nD5qojCQVqpiIk82aYINQZd+U=",
"owner": "nix-community",
"repo": "disko",
"rev": "6d42596a35d34918a905e8539a44d3fc91f42b5b",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@ -42,11 +7,11 @@
]
},
"locked": {
"lastModified": 1726989464,
"narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=",
"lastModified": 1720042825,
"narHash": "sha256-A0vrUB6x82/jvf17qPCpxaM+ulJnD8YZwH9Ci0BsAzE=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176",
"rev": "e1391fb22e18a36f57e6999c7a9f966dc80ac073",
"type": "github"
},
"original": {
@ -58,11 +23,11 @@
},
"impermanence": {
"locked": {
"lastModified": 1727198257,
"narHash": "sha256-/qMVI+SG9zvhLbQFOnqb4y4BH6DdK3DQHZU5qGptehc=",
"lastModified": 1719091691,
"narHash": "sha256-AxaLX5cBEcGtE02PeGsfscSb/fWMnyS7zMWBXQWDKbE=",
"owner": "nix-community",
"repo": "impermanence",
"rev": "8514fff0f048557723021ffeb31ca55f69b67de3",
"rev": "23c1f06316b67cb5dabdfe2973da3785cfe9c34a",
"type": "github"
},
"original": {
@ -73,11 +38,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1727040444,
"narHash": "sha256-19FNN5QT9Z11ZUMfftRplyNN+2PgcHKb3oq8KMW/hDA=",
"lastModified": 1723310128,
"narHash": "sha256-IiH8jG6PpR4h9TxSGMYh+2/gQiJW9MwehFvheSb5rPc=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "d0cb432a9d28218df11cbd77d984a2a46caeb5ac",
"rev": "c54cf53e022b0b3c1d3b8207aa0f9b194c24f0cf",
"type": "github"
},
"original": {
@ -89,11 +54,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1726969270,
"narHash": "sha256-8fnFlXBgM/uSvBlLWjZ0Z0sOdRBesyNdH0+esxqizGc=",
"lastModified": 1723688146,
"narHash": "sha256-sqLwJcHYeWLOeP/XoLwAtYjr01TISlkOfz+NG82pbdg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "23cbb250f3bf4f516a2d0bf03c51a30900848075",
"rev": "c3d4ac725177c030b1e289015989da2ad9d56af0",
"type": "github"
},
"original": {
@ -105,11 +70,11 @@
},
"nixpkgs_unstable": {
"locked": {
"lastModified": 1726937504,
"narHash": "sha256-bvGoiQBvponpZh8ClUcmJ6QnsNKw0EMrCQJARK3bI1c=",
"lastModified": 1723637854,
"narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9357f4f23713673f310988025d9dc261c20e70c6",
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
"type": "github"
},
"original": {
@ -121,15 +86,13 @@
},
"root": {
"inputs": {
"crane": "crane",
"disko": "disko",
"home-manager": "home-manager",
"impermanence": "impermanence",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixpkgs_unstable": "nixpkgs_unstable",
"secrix": "secrix",
"unattended-installer": "unattended-installer"
"secrix_unstable": "secrix_unstable"
}
},
"secrix": {
@ -139,40 +102,36 @@
]
},
"locked": {
"lastModified": 1724154123,
"narHash": "sha256-K0vRXtBoBbaONVUo9aRp5MfNLw5NQCB+qtkzzOF0bjk=",
"owner": "Zocker1999NET",
"lastModified": 1718983551,
"narHash": "sha256-JOX1quPQEHyzWfeZvI2ZtNPJ5zPP9vMuSO4W+/AawT0=",
"owner": "Platonic-Systems",
"repo": "secrix",
"rev": "754f316db237065109fb10137a7fc351425eaf32",
"rev": "59bdb737fac787278b80069dad48ba90ace42bfc",
"type": "github"
},
"original": {
"owner": "Zocker1999NET",
"ref": "release-bnet",
"owner": "Platonic-Systems",
"repo": "secrix",
"type": "github"
}
},
"unattended-installer": {
"secrix_unstable": {
"inputs": {
"disko": [
"disko"
],
"nixpkgs": [
"nixpkgs"
"nixpkgs_unstable"
]
},
"locked": {
"lastModified": 1724352083,
"narHash": "sha256-GP8YzfLFOFcSSRqGkzG7HGu/hT749saBgRUMSClkqwU=",
"owner": "chrillefkr",
"repo": "nixos-unattended-installer",
"rev": "25936f740d57d43a05adbb0e667deabd641d9ebc",
"lastModified": 1718983551,
"narHash": "sha256-JOX1quPQEHyzWfeZvI2ZtNPJ5zPP9vMuSO4W+/AawT0=",
"owner": "Platonic-Systems",
"repo": "secrix",
"rev": "59bdb737fac787278b80069dad48ba90ace42bfc",
"type": "github"
},
"original": {
"owner": "chrillefkr",
"repo": "nixos-unattended-installer",
"owner": "Platonic-Systems",
"repo": "secrix",
"type": "github"
}
}

@ -1,6 +1,7 @@
{
description = "banananet.work Server & Deployment Controller environment";
inputs = {
# packages repositories
@ -8,108 +9,205 @@
nixpkgs_unstable.url = "github:nixos/nixpkgs/nixos-unstable";
# required submodules
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager/release-24.05";
inputs.nixpkgs.follows = "nixpkgs";
};
impermanence.url = "github:nix-community/impermanence";
secrix = {
# TODO revert after my pulls are merged: https://github.com/Platonic-Systems/secrix/pulls/Zocker1999NET
#url = "github:Platonic-Systems/secrix";
url = "github:Zocker1999NET/secrix/release-bnet";
url = "github:Platonic-Systems/secrix";
inputs.nixpkgs.follows = "nixpkgs";
};
crane.url = "github:ipetkov/crane";
# required for configs
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
unattended-installer = {
url = "github:chrillefkr/nixos-unattended-installer";
inputs.disko.follows = "disko";
inputs.nixpkgs.follows = "nixpkgs";
# for debugging only
secrix_unstable = {
url = "github:Platonic-Systems/secrix";
inputs.nixpkgs.follows = "nixpkgs_unstable";
};
# TODO experiment with
# - https://git.sr.ht/~msalerno/wirenix
};
outputs =
{ self, ... }@inputs:
outputs = { self, ... }@inputs:
let
inherit (self) outputs;
inherit (outputs) lib;
# every flake "submodule" gets this passed:
flakeArg = {
# Usage in submodule:
# { ... }@flakeArg: { }
# add "..." this so new ones can easily be added
flake = self; # full flake reflection
inherit
# tools / shortcuts
lib # nixpkgs & my lib combined
# flake refs
inputs # evaluated inputs
outputs # evaluated outputs
;
# self: the modules result, via self-reflection
};
inherit (outputs.libAnchors) importFlakeMod;
inherit (lib) importFlakeModWithSystem;
# constants
system = "x86_64-linux";
# package repositories
pkgs = import inputs.nixpkgs { inherit system; };
pkgs_unstable = import inputs.nixpkgs_unstable { inherit system; };
in
{
apps = importFlakeModWithSystem ./nix/apps;
devShells = importFlakeModWithSystem ./nix/devShells;
homeManagerModules = importFlakeMod ./nix/hmModules;
# shortcut to fully configured secrix
apps.x86_64-linux.secrix = inputs.secrix.secrix self;
lib = outputs.libAnchors // importFlakeMod ./nix/lib;
# anchors required for importing modules
libAnchors =
let
lib = inputs.nixpkgs.lib;
inherit (lib.asserts) assertMsg;
in
rec {
# ({?} -> ?) -> {?} -> ?
# gives a function access to its own return value
# by adding it to its first argument (assuming thats an attrset)
reflect =
fun: attrs:
# TODO is there a more official way?
assert assertMsg (builtins.isAttrs attrs) ''
expected a set, got an ${builtins.typeOf attrs}
'';
assert assertMsg (!attrs ? "self") ''
reflect argument already contains a self attribute
'';
nixosConfigurations =
let
outputs = fun (attrs // { self = result; });
result = outputs;
nixosSystem = { modules, system }: inputs.nixpkgs.lib.nixosSystem {
modules = [
outputs.nixosModules.myOptions
outputs.nixosModules.withDepends
] ++ modules;
inherit system;
};
in
result;
initFlakeMod = mod: reflect mod flakeArg;
importFlakeMod = path: initFlakeMod (import path);
{
"x13yz" = nixosSystem {
modules = [
{
# TODO check if required & hide into modules
boot = {
initrd = {
availableKernelModules = [
"nvme"
"rtsx_pci_sdmmc"
"xhci_pci"
];
kernelModules = [
"dm-snapshot"
];
};
kernelModules = [
"kvm-intel"
];
};
}
inputs.nixos-hardware.nixosModules.lenovo-thinkpad-x13-yoga
{
# hardware
hardware.cpu.type = "intel";
hardware.graphics.intel.enable = true;
}
{
# as currently installed
boot.initrd.luks.devices."luks-herske.lvm.6nw.de" = {
device = "/dev/disk/by-uuid/16b8f83d-0450-4c4d-9964-788575a31eec";
preLVM = true;
allowDiscards = true;
};
fileSystems."/" = {
device = "/dev/disk/by-uuid/c93557db-e7c5-46ef-9cd8-87eb7c5753dc";
fsType = "ext4";
options = [ "relatime" "discard" ];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/5F9A-9A2D";
fsType = "vfat";
options = [ "uid=0" "gid=0" "fmask=0077" "dmask=0077" ];
};
swapDevices = [{ device = "/dev/disk/by-uuid/8482463b-ceb3-40b3-abef-b49df2de88e5"; }];
system.stateVersion = "24.05";
}
{
# host configuration
networking.domain = "pc.6nw.de";
networking.hostName = "x13yz";
services.fprintd.enable = true;
x-banananetwork.frontend.convertable = true;
x-banananetwork.frontend.enable = true;
}
];
system = "x86_64-linux";
};
# configs for https://github.com/Platonic-Systems/secrix/issues/25
"secrix_issue25" = inputs.nixpkgs.lib.nixosSystem {
modules = [
inputs.secrix.nixosModules.secrix
{
boot.loader.grub.enable = false;
fileSystems."/".device = "/dev/null";
networking.hostName = "test";
system.stateVersion = "24.05";
documentation.nixos.enable = true;
documentation.nixos.includeAllModules = true;
}
];
system = "x86_64-linux";
};
"secrix_issue25_working" = inputs.nixpkgs.lib.nixosSystem {
modules = [
inputs.secrix.nixosModules.secrix
{
boot.loader.grub.enable = false;
fileSystems."/".device = "/dev/null";
networking.hostName = "test";
system.stateVersion = "24.05";
documentation.nixos.enable = true;
documentation.nixos.includeAllModules = false; # <-- THIS
}
];
system = "x86_64-linux";
};
"secrix_issue25_unstable" = inputs.nixpkgs_unstable.lib.nixosSystem {
modules = [
inputs.secrix_unstable.nixosModules.secrix
{
boot.loader.grub.enable = false;
fileSystems."/".device = "/dev/null";
networking.hostName = "test";
system.stateVersion = "24.05";
documentation.nixos.enable = true;
documentation.nixos.includeAllModules = true;
}
];
system = "x86_64-linux";
};
nixosConfigurations = importFlakeMod ./nix/nixos;
nixosModules = importFlakeMod ./nix/nixos-modules;
};
nixosProfiles = importFlakeMod ./nix/nixosProfiles;
nixosModules = {
overlays = importFlakeMod ./nix/overlays;
# this one includes all of my modules
# - most of them only change things when enabled (e.g. x-banananetwork.*.enable)
# - others only introduce small, reasonable changes if other modules options are set, as reasonable defaults (if I intend to upstream them)
# however, use on your own discretion
banananetwork = import ./nix/nixos-modules;
# this one defines common options for my systems to my modules
# you definitely do not want to use this
myOptions = import ./nix/myOptions.nix;
# this one also includes required dependencies from flake inputs
withDepends = {
imports = [
inputs.home-manager.nixosModules.home-manager
inputs.impermanence.nixosModules.impermanence
inputs.secrix.nixosModules.secrix
outputs.nixosModules.banananetwork
];
};
};
devShells."${system}".default =
let
pkgs = pkgs_unstable;
in
pkgs.mkShell
{
packages = with pkgs; [
curl
rsync
opentofu
terranix
];
};
packages = importFlakeModWithSystem ./nix/packages;
};
}

@ -1,5 +1,3 @@
# nix configs & modules
This directory contains all nix-related (esp. NixOS) files.
Normally, I would have placed these directories on the same level as flake.nix, but:
@ -9,8 +7,3 @@ Normally, I would have placed these directories on the same level as flake.nix,
(i.e. also including systems without NixOS)
it also contains files for different systems.
These should be separated from nix files, in general.
## flakeArg
Every flake "submodule" gets passed a `flakeArg` parameter to access the flakes inputs & outputs and other goodies. This is documented in ../flake.nix.

@ -1,17 +0,0 @@
{
flake,
inputs,
lib,
...
}@flakeArg:
{ system, ... }@sysArg:
{
# shortcut to fully configured secrix
secrix =
assert lib.assertMsg (system == "x86_64-linux") ''
secrix is currently only compatible with x86_64-linux
'';
inputs.secrix.secrix flake;
}

@ -1,27 +0,0 @@
{ outputs, ... }@flakeArg:
{ pkgs_unstable, system, ... }@sysArg:
let
pkgs = pkgs_unstable;
in
{
default = pkgs.mkShell {
packages =
(with pkgs; [
curl
mkpasswd
rsync
opentofu
terranix
# tooling for services
wireguard-tools
])
++ [
# flake stuff
outputs.packages.${system}.secrix-wrapper
];
# TODO magic
shellHook = ''
export SECRIX_ID=~/".ssh/id_ed25519"
'';
};
}

@ -1,7 +0,0 @@
{
imports = [
# files
./gpg-agent.nix
./zsh.nix
];
}

@ -1,31 +0,0 @@
{
config,
lib,
osConfig ? null,
...
}:
let
cfg = config.services.gpg-agent;
hwSmartcards = osConfig.hardware.gpgSmartcards.enable;
scDaemon = cfg.enable && cfg.enableScDaemon;
in
{
config = lib.mkIf (!builtins.isNull osConfig) {
assertions = [
{
assertion = scDaemon -> hwSmartcards;
message = ''
gpg-agents scDaemon is enabled but NixOS hardware.gpgSmartcards is disabled
'';
}
];
warnings = [
(lib.mkIf (hwSmartcards && !scDaemon) ''
NixOS hardware.gpgSmartcards is enabled but gpg-agents scDaemon is disabled
'')
];
};
}

@ -1,24 +0,0 @@
{
config,
lib,
osConfig ? null,
...
}:
let
cfg = config.programs.zsh;
in
{
config = lib.mkIf cfg.enable {
assertions = lib.mkIf (!builtins.isNull osConfig) [
# see https://github.com/nix-community/home-manager/blob/e1391fb22e18a36f57e6999c7a9f966dc80ac073/modules/programs/zsh.nix#L353
{
assertion = cfg.enableCompletion -> builtins.elem "/share/zsh" osConfig.environment.pathsToLink;
message = ''
for useful ZSH completion, add "/share/zsh" to NixOS environment.pathsToLink
'';
}
];
};
}

@ -1,15 +0,0 @@
{ lib, self, ... }@flakeArg:
{
assertions.imports = lib.singleton ./assertions;
# combination of all my custom modules
# these should not change anything until you enable their custom options
default.imports = [
# flake
self.assertions
# directories
./extends
];
}

@ -1,8 +0,0 @@
{
imports = [
# files
./kdeconnect.nix
./retroarch.nix
./vscode.nix
];
}

@ -1,106 +0,0 @@
{
config,
lib,
osConfig ? null,
pkgs,
...
}:
let
cfg = config.services.kdeconnect;
configDescPreamble = ''
Configuring KDE Connect using these options is probably not endorsed by upstream
as no user documentation for these configuration files exist.
'';
optionDescPreamble = ''
${configDescPreamble}
{option}`services.kdeconnect.enableSettings` must be enabled
for this option to be applied.
'';
typeConfig = pkgs.formats.ini { };
in
{
options.services.kdeconnect = {
enableSettings = lib.mkEnableOption ''
KDE Connect settings defined in this module.
${configDescPreamble}
This option operates independently of {option}`services.kdeconnect.enable`
and so can also be used when KDE Connect is already installed by other means,
e.g. using the NixOS module option `programs.kdeconnect.enable`
'';
settings = {
name = lib.mkOption {
description = ''
Name of this device, advertised to other KDE Connect devices
${optionDescPreamble}
'';
type = lib.types.str;
default = osConfig.networking.hostName;
defaultText = lib.literalExpression "osConfig.networking.hostName";
};
customDevices = lib.mkOption {
description = ''
List of IPs & hostnames KDE Connect should try to connect to.
Useful in scenarios where auto discover does not work,
e.g. when combined with VPN.
${optionDescPreamble}
'';
# TODO limit to IPs
# TODO check if hostname works now
type = with lib.types; listOf str;
default = [ ];
example = [
"192.168.12.10"
"192.168.12.11"
];
};
};
config = lib.mkOption {
description = ''
Arbitary settings for KDE Connect.
${optionDescPreamble}
This will then overwrite the file {file}`$XDG_CONFIG_DIR/kdeconnect/config`.
'';
type = typeConfig.type;
default = { };
};
};
config = {
warnings = [
(lib.mkIf cfg.enableSettings "programs.kdeconnect.enableSettings is experimental, be aware")
];
services.kdeconnect.config =
let
sets = cfg.settings;
optConcat = sep: list: lib.mkIf (list != [ ]) (lib.concatStringsSep sep list);
in
{
General = {
customDevices = optConcat "," sets.customDevices;
name = sets.name;
};
};
xdg.configFile."kdeconnect/config" = lib.mkIf cfg.enableSettings {
# TODO make compatible with more systems
onChange = ''
${pkgs.systemd}/bin/systemctl --user reload-or-restart app-org.kde.kdeconnect.daemon@autostart.service
'';
source = typeConfig.generate "kdeconnect-config" cfg.config;
};
};
}

@ -1,43 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.retroarch;
in
{
options.programs.retroarch = {
enable = lib.mkEnableOption "RetroArch as user program";
package = lib.mkPackageOption pkgs "retroarch" {
example = lib.literalExpression "pkgs.retroarchFull";
};
cores = lib.mkOption {
description = "List of cores to install.";
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "with pkgs.libretro; [ twenty-fortyeight ]";
};
finalPackage = lib.mkOption {
description = "RetroArch package with the cores selected";
type = lib.types.package;
readOnly = true;
default = if cfg.cores == [ ] then cfg.package else cfg.package.override { inherit (cfg) cores; };
defaultText = ''
with config.programs.retroarch;
package.override { inherit cores; }
'';
};
};
config = {
home.packages = lib.singleton cfg.finalPackage;
};
}

@ -1,71 +0,0 @@
{
config,
lib,
options,
...
}:
let
cfg = config.programs.vscode;
in
{
options.programs.vscode = {
keybindingsNext = lib.mkOption {
description = ''
More expressive and ordered way to set {option}`programs.vscode.keybindings`.
Both options can be used simultaneously.
- key bindings are grouped by their key combination
- you can shortcut commands without further options (see example)
'';
type =
let
bindsType = options.programs.vscode.keybindings.type;
bindModule = bindsType.nestedTypes.elemType;
bindOpts = bindModule.getSubOptions;
inhOpts =
prefix:
builtins.removeAttrs (bindOpts prefix) [
"_module"
"key"
];
inhMod = lib.types.submodule { options = inhOpts [ ]; };
commType = (bindOpts [ ]).command.type;
bindsNextType = lib.types.either inhMod commType;
bindsListNext = lib.types.listOf bindsNextType;
in
lib.types.attrsOf bindsListNext;
default = { };
example = {
"ctrl+tab" = [
{
command = "-workbench.action.quickOpenNavigateNextInEditorPicker";
when = "inEditorsPicker && inQuickOpen";
}
"-workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup"
"workbench.action.nextEditor"
];
"ctrl+shift+tab" = [
{
command = "-workbench.action.quickOpenNavigatePreviousInEditorPicker";
when = "inEditorsPicker && inQuickOpen";
}
"-workbench.action.quickOpenLeastRecentlyUsedEditorInGroup"
"workbench.action.previousEditor"
];
};
};
};
config.programs.vscode = {
keybindings =
let
expandEntry = opts: if builtins.isAttrs opts then opts else { command = opts; };
transEntry = key: opts: (expandEntry opts) // { inherit key; };
transKey = key: opts: map (transEntry key) opts;
transAttr = attr: lib.flatten (lib.mapAttrsToList transKey attr);
in
transAttr config.programs.vscode.keybindingsNext;
};
}

@ -0,0 +1,24 @@
# this stuff replaces all settings which would be configured by the corresponding frontend NixOS module
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.frontend;
in
{
config = lib.mkIf (cfg.enable && !cfg.nixosModuleCompat) {
assertions = [
{
assertion = !cfg.nixosModuleCompat;
message = "missing implementation of base stuff";
}
];
};
}

@ -0,0 +1,35 @@
{ config
, lib
, pkgs
, ...
}: {
imports = [
./base.nix
./extension.nix
];
options = {
x-banananetwork.frontend = {
enable = lib.mkEnableOption ''
settings for frontend configuration in Home-Manager
'';
nixosModuleCompat = lib.mkEnableOption ''
compatibility to the corresponding frontend NixOS configuration.
This is created by opting out to configure stuff
which is already configured by the corresponding NixOS module.
'';
};
};
}

@ -0,0 +1,7 @@
# this stuff must all be compatible to settings already configured by the corresponding frontend NixOS module
{ config
, lib
, pkgs
, ...
}: { }

@ -1,7 +0,0 @@
{ ... }@flakeArg:
let
in
{
}

@ -1,42 +0,0 @@
{ inputs, self, ... }@flakeArg:
let
inherit (inputs) nixpkgs nixpkgs_unstable;
inherit (nixpkgs) lib; # prevent infinite recursion
inherit (builtins) isString;
inherit (lib.attrsets) attrByPath hasAttrByPath updateManyAttrsByPath;
inherit (lib.options) showOption;
inherit (lib.strings) splitString;
inherit (lib.trivial) flip pipe warnIf;
inherit (self) backportByPath;
in
{
backportByPath =
let
pathInterpret = p: if isString p then splitString "." p else p;
in
new: orig: prefix:
flip pipe [
(map (
path:
let
pathList = pathInterpret path;
pathFull = pathInterpret prefix ++ pathList;
error = abort "attr not found on path ${showOption pathFull}";
newVal = attrByPath pathFull error new;
origVal = attrByPath pathFull newVal orig;
in
{
path = pathList;
update =
_:
warnIf (hasAttrByPath pathFull orig) "${showOption pathFull} no longer needs to be backported"
origVal;
}
))
(flip updateManyAttrsByPath { })
];
backportNixpkg = backportByPath nixpkgs_unstable nixpkgs;
}

@ -1,44 +0,0 @@
{ inputs, lib, ... }@flakeArg:
let
inherit (inputs) nixpkgs;
inherit (builtins) isAttrs mapAttrs;
inherit (lib) autoExtend importFlakeMod;
in
# be a drop-in replacement
nixpkgs.lib
# groups
// mapAttrs (autoExtend nixpkgs.lib) {
attrsets = ./attrsets.nix;
backport = ./backport.nix;
lists = ./lists.nix;
math = ./math.nix;
modules = ./modules.nix;
network = ./network.nix;
strings = ./strings.nix;
types = ./types.nix;
x-banananetwork-unused = ./unused.nix;
}
# functions
// {
autoExtend =
upstream: name: obj:
(upstream.${name} or { }) // (if isAttrs obj then obj else importFlakeMod obj);
supportedSystems = builtins.attrNames nixpkgs.legacyPackages;
systemSpecificVars = system: {
pkgs = import nixpkgs { inherit system; };
pkgs_unstable = import inputs.nixpkgs_unstable { inherit system; };
inherit system;
};
forAllSystems =
gen: lib.genAttrs lib.supportedSystems (system: gen (lib.systemSpecificVars system));
importFlakeModWithSystem = path: lib.forAllSystems (lib.importFlakeMod path);
}

@ -1,42 +0,0 @@
{ lib, ... }@flakeArg:
let
inherit (builtins)
deepSeq
foldl'
groupBy
mapAttrs
;
inherit (lib.lists) singleton;
inherit (lib.trivial) flip pipe;
in
{
groupByMult =
groupers:
# TODO (maybe) make more efficient by building up grouping function from right
# from left
# 0= (x: x) vals
# 1= groupBy values[0] <0>
# 2= mapAttrs (_: groupBy values[1]) <1>
# 3= mapAttrs (_: mapAttrs (_: groupBy values[2])) <2>
# from right
# 1= (groupBy values[0])
# 2= ...
let
nul = {
mapper = x: x;
result = [ ];
};
op =
{ mapper, result }:
grouper: {
mapper = fun: mapAttrs (_: mapper fun);
result = result ++ singleton (mapper grouper);
};
list = map groupBy groupers;
pipeList = (foldl' op nul list).result;
in
# catch errors while building groupers before values are passed through
deepSeq pipeList (flip pipe pipeList);
}

@ -1,46 +0,0 @@
# inefficient, but sufficient math functions missing in Nix & nixpkgs
{ lib, ... }@flakeArg:
let
inherit (builtins) foldl' genList isInt;
inherit (lib.lists) imap0 reverseList;
inherit (lib.strings) stringToCharacters;
inherit (lib.trivial) flip pipe;
in
rec {
bitAsString = bool: if bool then "1" else "0";
binToInt = flip pipe [
stringToCharacters
reverseList
(imap0 (i: x: if x == "1" then pow 2 i else 0))
(foldl' (a: v: a + v) 0)
];
intToBin =
len: num:
assert isInt len && isInt num;
let
maxVal = pow 2 len;
twoPowers = genList (i: pow 2 (len - 1 - i)) len;
init = {
number = num;
output = "";
};
folder =
{ number, output }:
power:
let
bit = number >= power;
in
{
number = if bit then number - power else number;
output = "${output}${bitAsString bit}";
};
in
assert num < maxVal;
(foldl' folder init twoPowers).output;
pow = x: exp: foldl' (a: b: a * b) 1 (genList (_: x) exp); # TODO
}

@ -1,9 +0,0 @@
{ lib, ... }@flakeArg:
let
inherit (lib.modules) mkOverride;
in
{
mkTestOverride = mkOverride 55;
}

@ -1,238 +0,0 @@
{ lib, self, ... }@flakeArg:
let
inherit (builtins)
concatMap
concatStringsSep
mapAttrs
elemAt
fromTOML
genList
isAttrs
length
match
replaceStrings
;
inherit (lib.asserts) assertMsg;
inherit (lib.lists)
count
imap1
last
singleton
sublist
;
inherit (lib.math) binToInt intToBin;
inherit (lib.strings)
commonPrefixLength
hasInfix
splitString
substring
toIntBase10
toLower
;
inherit (lib.trivial) flip pipe toHexString;
fixedWidthStrSuffix =
width: filler: str:
let
strw = lib.stringLength str;
reqWidth = width - (lib.stringLength filler);
in
assert lib.assertMsg (strw <= width)
"fixedWidthString: requested string length (${toString width}) must not be shorter than actual length (${toString strw})";
if strw == width then str else fixedWidthStrSuffix reqWidth filler str + filler;
fromHexString = str: (fromTOML "v=0x${str}").v; # TODO not (yet) available in nixpkgs.lib
toHex = str: toLower (toHexString str);
toIpClass =
ipArg:
let
statics = {
ipv4 = {
_group_bits = 8;
_group_count = 4;
_group_sep = ".";
_group_toInt = toIntBase10;
compressed = ip.decCompressed;
shorted = ip.decCompressed;
};
ipv6 = {
_group_bits = 16;
_group_count = 8;
_group_sep = ":";
_group_toInt = fromHexString;
compressed = ip.hexShorted; # TODO temporary
shorted = ip.hexShorted;
};
};
ip =
statics.${ipArg.version} # avoid recursion error
// {
type = "ipAddress";
# internal operators
__toString = s: s.cidrCompressed;
# decimal
decGroups = map ip._group_toInt ip._groups;
decCompressed = concatStringsSep ip._group_sep (map toString ip.decGroups);
# binary
binGroups = map (v: intToBin ip._group_bits) ip.decGroups; # shortcut compared to hexGroups
binRaw = concatStringsSep "" ip.binGroups;
# hex
hexGroups = map toHex ip.decGroups;
hexShorted = concatStringsSep ":" ip.hexGroups;
# TODO hexCompressed
# TODO hexExploded
# network
binRawNet = substring 0 ip.cidrInt ip.binRaw;
_cidr_max = ip._group_count * ip._group_bits;
cidrInt = if ip._cidrGroup == null then ip._cidr_max else toIntBase10 ip._cidrGroup;
cidrCompressed = "${ip.compressed}/${ip.cidrStr}";
cidrShorted = "${ip.shorted}/${ip.cidrStr}";
cidrStr = "${toString ip.cidrInt}";
# helpers
isCompatible =
o:
assert self.isParsedIP o;
ip.type == o.type;
__verifyCompat = fun: o: if ip.isCompatible o then fun o else false;
split = map (self.parseBinNet ip.version) [
"${ip.binRawNet}0"
"${ip.binRawNet}1"
];
}
// mapAttrs (_: ip.__verifyCompat) {
contains = o: ip.cidrInt <= commonPrefixLength ip.binRawNet (o.binRawNet);
equals = o: ip.decGroups == o.decGroups && ip.cidrInt == o.cidrInt;
sameNetwork = o: ip.binRawNet == o.binRawNet;
}
// ipArg;
in
assert assertMsg (length ip._groups == ip._group_count)
"invalid IP group count, expected ${toString ip._group_count}, got: ${toString (length ip._groups)}, input: ${ipArg._input} (bug, please report)";
assert ip.cidrInt <= ip._cidr_max;
ip;
in
rec {
formatMAC =
let
badChars = [
"."
":"
"_"
"-"
];
goodChars = map (x: "") badChars;
in
mac:
pipe mac [
(replaceStrings badChars goodChars)
toLower
];
isParsedIP = x: isAttrs x && x.type or null == "ipAddress";
isParsedIPv4 = x: isParsedIP x && x.version == "ipv4";
isParsedIPv6 = x: isParsedIP x && x.version == "ipv6";
parseIP = ip: if hasInfix ":" ip then parseIPv6 ip else parseIPv4 ip;
parseIPv4 =
ipStr:
let
parsed = match ''^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)(/([0-9]+))?$'' ipStr;
ip = toIpClass {
version = "ipv4";
_input = ipStr;
_cidrGroup = last parsed;
_groups = substring 0 4 parsed;
};
in
assert parsed != null; # TODO improve
ip;
parseIPv6 =
# TODO add support for IPv4 mapped addresses
ipStr:
let
parsed = match ''^(([0-9a-f]{0,4}:){1,7}[0-9a-f]{0,4})(/([0-9]+))?$'' (toLower ipStr);
rawGroups = pipe parsed [
(flip elemAt 0)
(splitString ":")
# first & last zeros might be omitted as well, but are not a compression artifact
(imap1 (i: x: if (i == 1 || i == length rawGroups) && x == "" then "0" else x))
];
groups = flip concatMap rawGroups (
x: if x == "" then genList (_: "0") (9 - length rawGroups) else singleton x
);
ip = toIpClass {
version = "ipv6";
_input = ipStr;
_cidrGroup = last parsed;
_groups = groups;
};
in
assert parsed != null;
assert count (g: g == "") rawGroups <= 1;
ip;
parseIPv6IfId =
ipStr:
let
parsed = match ''^(([0-9a-f]{0,4}:){1,3}[0-9a-f]{0,4})$'' (toLower ipStr);
rawGroups = pipe parsed [
(flip elemAt 0)
(splitString ":")
# first & last zeros might be omitted as well, but are not a compression artifact
(imap1 (i: x: if (i == 1 || i == length rawGroups) && x == "" then "0" else x))
];
groups = flip concatMap rawGroups (
x: if x == "" then genList (_: "0") (5 - length rawGroups) else singleton x
);
ip = toIpClass {
type = "ipInterfaceIdentifier";
version = "ipv6";
_group_count = 4;
_input = ipStr;
_cidrGroup = null;
_groups = groups;
};
in
assert parsed != null;
assert count (g: g == "") rawGroups <= 1;
ip;
parseBinNet =
ipV: binStr:
let
ip = toIpClass {
version = ipV;
_input = binStr;
# special overwrites - TODO integrate into toIpClass
cidrInt = length binStr;
binRaw = fixedWidthStrSuffix ip._cidr_max "0" binStr;
binGroups = genList (i: substring (ip._group_bits * i) ip.binRaw) ip._group_count;
decGroups = map binToInt ip.binGroups;
# shortcuts
binRawNet = binStr;
};
in
ip;
mergeIPv6IfId =
prefix: suffix:
let
pref = parseIPv6 prefix;
suff = parseIPv6IfId suffix;
in
assert pref.cidrInt <= 64;
"${concatStringsSep ":" (sublist 0 4 pref.hexGroups ++ suff.hexGroups)}/64";
netMinus =
excl: net:
if net.sameNetwork excl then
[ ]
else if !net.contains excl then
singleton net
else
net.split;
netListMinus = excl: concatMap (netMinus excl);
}

@ -1,32 +0,0 @@
{ lib, ... }@flakeArg:
let
inherit (builtins)
isAttrs
isBool
isList
isNull
isString
typeOf
;
inherit (lib.strings) optionalString;
in
{
conditionalString =
cond:
optionalString (
if isNull cond then
false
else if isBool cond then
cond
else if isString cond then
cond != ""
else if isList cond then
cond != [ ]
else if isAttrs cond then
cond.enable or (cond != { })
else
throw "unexpected type of condition ${typeOf cond}"
);
}

@ -1,100 +0,0 @@
{
inputs,
lib,
self,
...
}@flakeArg:
# TODO upstream
let
inherit (builtins) concatStringsSep;
repeat = expr: count: builtins.genList (_: expr) count;
concatRepeat =
sep: str: count:
concatStringsSep sep (repeat str count);
concatGroup = patterns: "(${concatStringsSep "|" patterns})";
repeatOptional =
sep: pattern: count:
"${concatRepeat "" "(${pattern}${sep})?" count}${pattern}";
matchType =
{ description, pattern }: lib.types.strMatching "^${pattern}$" // { inherit description; };
# === regex parts
hexChar = "[0-9A-Fa-f]";
ipv4Block = "(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])";
euiHexBlock = concatRepeat "" hexChar 2;
euiWith = concatRepeat "[.:_-]?" euiHexBlock;
eui48 = euiWith 6;
eui64 = euiWith 8;
ipv4Addr = concatRepeat "\\." ipv4Block 4;
ipv6Block = repeatOptional "" hexChar 4;
ipv6Addr =
let
genVariant =
max: rightNum:
let
leftNum = max - rightNum - 1;
leftPart = concatRepeat ":" ipv6Block leftNum;
middlePart = lib.optionalString (rightNum == 0) "(${ipv6Block})?"; # full address only required once
rightPart = repeatOptional ":" ipv6Block rightNum;
in
"${leftPart}:${middlePart}:${rightPart}";
genAll = max: builtins.genList (genVariant max) max;
normals = genAll 8;
ipv4Mapped = map (x: "${x}:${ipv4Addr}") (genAll 6);
in
concatGroup (normals ++ ipv4Mapped);
v4CIDR = "/(3[0-2]|2[0-9]|1?[0-9])";
v6CIDR = "/(12[0-8]|1[0-2][0-9]|[1-9]?[0-9])";
interfaceId = "(%[[:alnum:]]+)?";
# === references
ipv6Ref = "RFC 4291 Section 2.2";
in
# extensions to the nix option types library
{
eui48 = matchType {
description = "EUI-48 (i.e. MAC address)";
pattern = eui48;
};
eui64 = matchType {
description = "EUI-64";
pattern = eui64;
};
ipAddress = lib.types.either self.ipv4Address self.ipv6Address;
ipAddressPlain = lib.types.either self.ipv4AddressPlain self.ipv6AddressPlain;
ipNetwork = lib.types.either self.ipv4Network self.ipv6Network;
ipv4Address = matchType {
description = "IPv4 address (no CIDR, opt. interface identifier)";
pattern = ipv4Addr + interfaceId;
};
ipv4AddressPlain = matchType {
description = "IPv4 address (no CIDR, no interface identifier)";
pattern = ipv4Addr;
};
ipv4Network = matchType {
description = "IPv4 address/network with CIDR";
pattern = ipv4Addr + v4CIDR;
};
ipv6Address = matchType {
description = "IPv6 address (${ipv6Ref}, no CIDR, opt. interface identifier)";
pattern = ipv6Addr + interfaceId;
};
ipv6AddressPlain = matchType {
description = "IPv6 address (${ipv6Ref}, no CIDR, no interface identifier)";
pattern = ipv6Addr;
};
ipv6Network = matchType {
description = "IPv6 address/network with CIDR (${ipv6Ref})";
pattern = ipv6Addr + v6CIDR;
};
}

@ -1,49 +0,0 @@
{ lib, ... }@flakeArg:
# functions I wrote but didnt end up using
# feel free to use them & reference them directly
{
slaacSuffix =
mac:
let
replaceChars = [
" "
"."
":"
"-"
];
rawMac = builtins.replaceStrings replaceChars (map (x: "") replaceChars) (lib.strings.toLower mac);
invert7bit = {
"0" = "2";
"1" = "3";
"2" = "0";
"3" = "1";
"4" = "6";
"5" = "7";
"6" = "4";
"7" = "5";
"8" = "a";
"9" = "b";
"a" = "8";
"b" = "9";
"c" = "e";
"d" = "f";
"e" = "c";
"f" = "d";
};
inherit (builtins) substring;
in
assert builtins.match (lib.strings.replicate 12 "[0-9a-f]") rawMac;
builtins.concatStringsSep "" [
(substring 0 1 rawMac)
(invert7bit.${substring 1 1 rawMac})
(substring 2 2 rawMac)
":"
(substring 4 2 rawMac)
"ff:f0"
(substring 6 2 rawMac)
":"
(substring 8 4 rawMac)
];
}

@ -1,46 +1,23 @@
# configures options only really useable for me
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
myOpts = config.x-banananetwork;
in
{
config = {
# personal defaults across the board
console.keyMap = lib.mkDefault "de";
documentation = {
man.mandoc.settings.output = {
paper = lib.mkDefault "a4";
};
};
i18n = {
# inspired by https://wiki.archlinux.org/title/Locale
defaultLocale = lib.mkDefault "en_US.UTF-8";
extraLocaleSettings = {
LANGUAGE = lib.mkDefault "en_US:en:C:de_DE";
LC_COLLATE = lib.mkDefault "C.UTF-8"; # language independent sorting
LC_MEASUREMENT = "de_DE.UTF-8"; # metric
LC_PAPER = "de_DE.UTF-8"; # metric
LC_TELEPHONE = "de_DE.UTF-8";
LC_TIME = lib.mkDefault "en_DK.UTF-8"; # ISO 8601
};
};
config = {
# for my own modules
x-banananetwork = {
# options defined in nixos-modules/options.nix
sshPublicKeys = [
@ -51,19 +28,20 @@ in
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAEaWqcgeNh3BjyDXCg0DQfbuPg5VLVYlt8ucYu7VZNr zocker@x13yz 2024-07-04"
];
userName = lib.mkDefault "zocker";
userName = "zocker";
# defaults for other modules, derived from them above
frontend = {
username = myOpts.userName;
};
vmCommon = {
userName = myOpts.userName;
hashedPassword = "$y$j9T$MdvgnTFGyCnZ.sLhXK7.w.$VkI6NqE7ZaN7xULmOrYCvgC6Sot19S0RWf.FmrOaLnC";
};
};
};
}

@ -1,72 +1,82 @@
# applies to all of my machines
# examples: PCs, laptops, VMs, hypervisors, ...
{
config,
lib,
options,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.allCommon;
in
{
options = {
x-banananetwork.allCommon = {
# TODO remove option, plan:
# - verify all configs still build (nix flake check)
# - i.e. all with allCommon.enable=true are using this module
# - remove option here & from all configs
# - again: nix flake check
enable = lib.mkEnableOption "for compatibility reasons" // {
default = true;
internal = true;
enable = lib.mkEnableOption ''
settings common to all systems
a set of opionated options to make systems useable & debugable for users.
This means e.g. adding common, useful tools and add documentation.
'';
};
};
config = lib.mkIf cfg.enable {
documentation = {
man.mandoc.settings.output = {
paper = lib.mkDefault "a4";
};
config = {
};
i18n = {
# inspired by https://wiki.archlinux.org/title/Locale
defaultLocale = lib.mkDefault "en_US.UTF-8";
extraLocaleSettings = {
LANGUAGE = lib.mkDefault "en_US:en:C:de_DE";
LC_COLLATE = lib.mkDefault "C.UTF-8"; # language independent sorting
LC_MEASUREMENT = "de_DE.UTF-8"; # metric
LC_PAPER = "de_DE.UTF-8"; # metric
LC_TELEPHONE = "de_DE.UTF-8";
LC_TIME = lib.mkDefault "en_DK.UTF-8"; # ISO 8601
};
};
assertions = [
{
assertion = cfg.enable;
message = "config imported profiles/common but tried to disable it";
}
(
let
defName = options.networking.hostName.default;
in
{
assertion = config.networking.hostName != defName;
message = "you must define a hostname (different from default: ${defName})";
}
)
];
nix = {
channel.enable = false;
daemonCPUSchedPolicy = lib.mkDefault "batch";
daemonIOSchedClass = lib.mkDefault "best-effort";
daemonIOSchedPriority = lib.mkDefault 7;
daemonCPUSchedPolicy = "batch";
daemonIOSchedClass = "best-effort";
daemonIOSchedPriority = 7;
settings = {
allowed-users = [
"root"
"@wheel"
];
auto-optimise-store = true;
experimental-features = [
"flakes"
"nix-command"
];
hashed-mirrors = [ "https://tarballs.nixos.org/" ];
hashed-mirrors = [
"https://tarballs.nixos.org/"
];
trusted-users = [
"root"
"@wheel"
];
};
@ -75,31 +85,21 @@ in
OOMScoreAdjust = lib.mkDefault 250;
};
programs = {
# for nixos-rebuild with flakes
git.enable = true;
ssh = {
extraConfig = ''
Host *
VerifyHostKeyDNS yes
'';
# well-known public keys
programs.ssh = {
hostKeyAlgorithms = [
"ssh-ed25519"
"ssh-rsa"
];
# well-known public keys
knownHosts = {
"git.banananet.work".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE854AkY/LYJ8kMe1olR+OsAxKIgvZ/JK+G+e0mMVWdH";
"git.sr.ht".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60";
"github.com".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl";
"gitlab.com".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf";
"gitlab.kit.edu".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOriuxsWoXA8CnvHndcG9c5u2lBXoCAmeFUhrMEMwY9+";
};
};
};
security = {
@ -141,12 +141,13 @@ in
};
system.activationScripts.diff = {
supportsDryActivation = true;
text = ''
if [[ -e /run/current-system ]]; then
echo "--- diff to current-system"
${lib.getExe pkgs.nvd} --nix-bin-dir=${config.nix.package}/bin diff /run/current-system "$systemConfig"
${pkgs.nvd}/bin/nvd --nix-bin-dir=${config.nix.package}/bin diff /run/current-system "$systemConfig"
echo "---"
fi
'';
@ -155,21 +156,29 @@ in
# ensure activation scripts are fine
# TODO upstream, probably replacing https://github.com/NixOS/nixpkgs/pull/149932
system.activatableSystemBuilderCommands = lib.mkAfter ''
${lib.getExe pkgs.shellcheck} --check-sourced --external-sources --norc --severity=warning $out/activate $out/dry-activate
${pkgs.shellcheck}/bin/shellcheck --check-sourced --external-sources --norc --severity=warning $out/activate $out/dry-activate
'';
time = {
hardwareClockInLocalTime = lib.mkDefault false;
timeZone = lib.mkDefault "Etc/UTC";
};
x-banananetwork = {
improvedDefaults.enable = true;
secrix = {
enable = true;
hostKeyType = "ed25519";
};
};
};
}

@ -1,9 +0,0 @@
{
imports = [
# files
./efi.nix
./fileSystems.nix
./mdns.nix
./nixos.nix
];
}

@ -1,39 +0,0 @@
{
config,
lib,
options,
...
}:
let
inherit (lib.strings) escapeNixString;
cfg = config.boot.loader;
fs = config.fileSystems;
efiIndicator = builtins.any (x: x) [
(cfg.grub.enable && cfg.grub.efiSupport)
(cfg.systemd-boot.enable)
];
efiMountPath = escapeNixString cfg.efi.efiSysMountPoint;
efiMount = fs.${cfg.efi.efiSysMountPoint} or null;
in
# TODO check cfg.grub.mirroredBoots as well
# TODO enable disko checks (optional i.e. when disko options are available)
{
config = lib.mkIf efiIndicator {
assertions = [
{
assertion = efiMount != null;
message = ''
There is no filesystem declaration for EFI System Partition ${efiMountPath}
'';
}
{
assertion = efiMount != null -> efiMount.fsType == "vfat";
message = ''
EFI System Partition ${efiMountPath} has not fsType "vfat"
'';
}
];
};
}

@ -1,21 +0,0 @@
{ config, ... }:
let
inherit (builtins) any attrValues elem;
allMounts = attrValues config.fileSystems;
testDiskOption = option: disk: elem option disk.options;
testDiskDiscard = testDiskOption "discard";
in
{
config = {
assertions = [
{
assertion = config.services.fstrim.enable -> !any testDiskDiscard allMounts;
message = ''
enabling "discard" mount option is discouraged because services.fstrim is enabled
'';
}
];
};
}

@ -1,22 +0,0 @@
{ config, lib, ... }:
let
cfgAvahi = config.services.avahi;
avahiMDNS = cfgAvahi.enable && (cfgAvahi.nssmdns4 || cfgAvahi.nssmdns6);
cfgResolved = config.services.resolved;
# TODO check settings when cfgResolved.settings exist
resolvedMDNS = cfgResolved.enable;
in
{
config = {
assertions = [
{
assertion = !(avahiMDNS && cfgResolved.enable);
message = ''
systemd-resolved is enabled while Avahi mDNS is enabled, disable one of both!
'';
}
];
};
}

@ -1,42 +0,0 @@
{ config, lib, ... }:
let
channelsEn = config.nix.channel.enable;
nixFeature = lib.trivial.flip builtins.elem config.nix.settings.experimental-features;
packageNames = map lib.strings.getName config.environment.systemPackages;
isInstalled = lib.trivial.flip builtins.elem packageNames;
gitInst = isInstalled "git";
gitEn = config.programs.git.enable;
in
{
config = {
assertions = [
{
assertion = !channelsEn -> nixFeature "flakes";
message = ''
You disabled Nix channels, then you should enable flakes, otherwise you cannot build a new config.
'';
}
{
assertion = (!channelsEn && nixFeature "flakes") -> (gitInst || gitEn);
message = ''
Missing git, which is required to interact with most flakes.
'';
}
{
assertion = nixFeature "flakes" -> nixFeature "nix-command";
message = ''
Nix experimental-feature "flakes" requires feature "nix-command"
'';
}
];
warnings = [
# TODO add link to this file
(lib.mkIf (gitEn && !gitInst) ''
(not relevant for you, please report to the module author) git package was not detected properly, fallback to programs.git module
'')
];
};
}

@ -1,14 +1,14 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.autoUnfree;
in
{
options = {
x-banananetwork.autoUnfree = {
@ -59,23 +59,34 @@ in
};
config = lib.mkIf cfg.enable {
nixpkgs.config = {
allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) (map lib.getName cfg.packages);
};
# TODO add alternative for allowUnfreePredicate for users
x-banananetwork.autoUnfree.packages =
let
inherit (lib.lists) flatten optional optionals;
# supported (ordered by long option name)
amd = config.hardware.cpu.amd;
intel = config.hardware.cpu.intel;
steam = config.programs.steam;
in
flatten [
# hardware.cpu
# TODO in nixpkgs, create {amd,intel}.microcodePackage options
# TODO test if this is really required
#(optional amd.updateMicrocode pkgs.microcodeAmd)
#(optional intel.updateMicrocode pkgs.microcodeIntel)
# programs
(optional steam.enable steam.package)
# TODO improve pulling in dependencies more accurate
@ -86,6 +97,8 @@ in
]))
];
};
}

@ -1,14 +1,14 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.debugMinimal;
in
{
options = {
x-banananetwork.debugMinimal = {
@ -21,16 +21,9 @@ in
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
dnsutils
jq # JSON
parallel
psmisc
pv
wcurl
];
programs = {
@ -38,9 +31,6 @@ in
enableCompletion = true;
enableLsColors = true;
vteIntegration = true;
interactiveShellInit = ''
export HISTCONTROL=ignoreboth:erasedups
'';
};
htop = {
@ -95,6 +85,8 @@ in
};
};
}

@ -1,75 +1,22 @@
# ../../flakes.nix expects this to just be a NixOS module
{
inputs,
lib,
outputs,
self,
...
}@flakeArg:
let
importModuleGroup = lib.importFlakeMod;
importModule = path: { imports = lib.singleton path; };
in
{
default = self.withDepends;
# assertions checking for good practices
assertions = importModule ./assertions;
# this one includes all of my modules
# - most of them only change things when enabled (e.g. x-banananetwork.*.enable)
# - others only introduce small, reasonable changes if other modules options are set, as reasonable defaults (if I intend to upstream them)
# however, use on your own discretion
banananetwork.imports = [
# flake
self.assertions
imports = [
# directories
./extends
./frontend
./improvedDefaults
./packages
(lib.importFlakeMod ./router)
./vmDisko
# files
./allCommon.nix
./autoUnfree.nix
./debugMinimal.nix
./graphics.nix
./hwCommon.nix
./options.nix
./privacy.nix
./secrix.nix
./sshSecurity.nix
./useable.nix
./vmCommon.nix
];
# this one defines common options for my systems to my modules
# you definitely do not want to use this
myOptions = importModule ../myOptions.nix;
# this one also includes required dependencies from flake inputs
withDepends =
{ config, pkgs, ... }:
{
imports = [
inputs.disko.nixosModules.disko
inputs.home-manager.nixosModules.home-manager
inputs.impermanence.nixosModules.impermanence
inputs.secrix.nixosModules.secrix
self.banananetwork
];
config = {
nixpkgs.overlays = [
outputs.overlays.backports
outputs.overlays.fromFlake
];
};
};
# from sub groups
# NOTE: these will change possibly unsensible stuff just by importing them
inherit (importModuleGroup ./overlays)
# (make list commitable)
systemd-radv-fadeout
;
}

@ -1,54 +0,0 @@
{ config, lib, ... }:
let
cpu = config.hardware.cpu;
anyArg = builtins.any (x: x) [
# list of conditions which require cpu type to be known
cpu.updateMicrocode
];
cpuOpts =
type:
lib.mkIf (anyArg && cpu.type == type) {
# options for all cpu types
updateMicrocode = lib.mkDefault cpu.updateMicrocode;
};
in
{
options = {
hardware.cpu = {
type = lib.mkOption {
description = ''
Configures the CPU type to expect this configuration to run on.
This setting is required when using generalizing options
like option{hardware.cpu.updateMicrocode}.
'';
type =
with lib.types;
nullOr (enum [
"amd"
"intel"
]);
# required
};
updateMicrocode = lib.mkEnableOption ''
microcode updates for CPU type selected in option{hardware.cpu.type}
'';
};
};
config = {
hardware.cpu = {
amd = cpuOpts "amd";
intel = cpuOpts "intel";
};
};
}

@ -1,11 +0,0 @@
{
imports = [
# files
./cpu.nix
./kernel.nix
./openssh.nix
./podman.nix
./printing.nix
./tailscale.nix
];
}

@ -1,41 +0,0 @@
{
config,
lib,
options,
pkgs,
...
}:
let
blocked = config.boot.blockedKernelModules;
in
{
options = {
boot.blockedKernelModules = lib.mkOption {
description = ''
Kernel modules which are blocked from being loaded
by using a rather hacky workaround called "fake install".
Read in the [Debian Wiki](https://wiki.debian.org/KernelModuleBlacklisting) for more info.
Be aware that this should block all attempts
from loading that module at runtime,
*including other modules* depending on it.
Modules listed here are automatically blacklisted as well
by adding them to {option}`boot.blacklistedKernelModules`,
which should hinder them being loaded automatically
due to supported devices detected.
'';
type = options.boot.blacklistedKernelModules.type;
default = [ ];
};
};
config = {
boot.blacklistedKernelModules = blocked;
boot.extraModprobeConfig = lib.flip lib.concatMapStrings blocked (module: ''
install ${module} ${lib.getExe' pkgs.coreutils "true"}
'');
};
}

@ -1,17 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.podman;
in
{
options.virtualisation.podman = {
compose.enable = lib.mkEnableOption "podman-compose";
};
config.environment.systemPackages = lib.mkIf (cfg.enable && cfg.compose.enable) [
pkgs.podman-compose
];
}

@ -1,25 +0,0 @@
{ config, lib, ... }:
let
cfg = config.services.printing;
in
{
options.services.printing = {
enableAutoDiscovery = lib.mkEnableOption ''
CUPS automatic discovery of printers.
This will enable & configure Avahi accordingly,
including opening ports in the firewall'';
};
config = lib.mkIf cfg.enable {
# TODO make also possible with systemd-resolved
services.avahi = lib.mkIf cfg.enableAutoDiscovery {
enable = true;
nssmdns4 = true;
nssmdns6 = true;
openFirewall = true;
};
};
}

@ -1,67 +0,0 @@
{ config, lib, ... }:
let
cfg = config.services.tailscale;
boolToStr = v: if v then "true" else "false";
toTsCli = lib.cli.toGNUCommandLine {
mkBool = k: v: lib.singleton "--${k}=${boolToStr v}";
mkList = k: v: lib.singleton "--${k}=${builtins.concatStringsSep "," v}";
mkOption =
k: v:
if v == null then [ ] else lib.singleton "--${k}=${lib.generators.mkValueStringDefault { } v}";
};
in
{
options.services.tailscale = {
setFlags = lib.mkOption {
description = ''
Options which are given to `tailscale set` on every boot.
Will be translated to {option}`services.tailscale.extraSetFlags`.
'';
type = lib.types.anything;
default = { };
example = {
advertise-exit-node = true;
advertise-tags = [
"mytag"
"other"
];
netfilter-mode = "none";
};
};
upFlags = lib.mkOption {
description = ''
Will be translated to {option}`services.tailscale.extraUpFlags`.
'';
type = lib.types.anything;
default = { };
example = {
ssh = true;
advertise-tags = [
"mytag"
"other"
];
};
};
};
config = lib.mkIf cfg.enable {
services.tailscale = {
extraSetFlags = toTsCli cfg.setFlags;
# apply set flags already on autoconnect
extraUpFlags = toTsCli cfg.upFlags ++ cfg.extraSetFlags;
};
# ensure tailscale set settings really apply
systemd.services.tailscaled-set = lib.mkIf (cfg.authKeyFile != null) {
after = lib.singleton "tailscaled-autoconnect.service";
wants = lib.singleton "tailscaled-autoconnect.service";
};
};
}

@ -1,14 +1,14 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.frontend;
in
{
options = {
x-banananetwork.frontend = {
@ -27,18 +27,25 @@ in
};
config = lib.mkIf cfg.enable {
# TODO copy modem-manager overlay (for now)
# NixOS configuration
console = {
useXkbConfig = true;
};
environment = {
pathsToLink = [
"/share/zsh" # for ZSH completion
"/share/zsh" # required for Home-Manager ZSH autocompletion, see https://github.com/nix-community/home-manager/blob/e1391fb22e18a36f57e6999c7a9f966dc80ac073/modules/programs/zsh.nix#L353
];
plasma6.excludePackages = with pkgs.kdePackages; [
@ -47,6 +54,7 @@ in
};
hardware = {
bluetooth = {
@ -54,8 +62,6 @@ in
powerOnBoot = true;
};
gpgSmartcards.enable = true; # scdaemon
graphics.required = true;
opengl = {
@ -63,30 +69,30 @@ in
driSupport = true;
};
sane = {
enable = true;
openFirewall = true;
};
usb-modeswitch.enable = true; # for specific WLAN/WWAN cards
};
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
extraSpecialArgs = {
nixosConfig = config;
};
users."${cfg.username}" = import ./home.nix;
};
networking = {
firewall = {
trustedInterfaces =
with lib.lists;
flatten [ (optional config.services.tailscale.enable "tailscale0") ];
trustedInterfaces = with lib.lists; flatten [
(optional config.services.tailscale.enable "tailscale0")
];
};
networkmanager.enable = true;
@ -95,16 +101,13 @@ in
};
nix.settings = {
builders-use-substitutes = lib.mkDefault true;
};
programs = {
captive-browser = {
enable = true;
bindInterface = true;
};
programs = {
firefox = {
enable = true;
@ -115,7 +118,6 @@ in
Locked = true;
};
DisablePocket = true;
DisableSetDesktopBackground = true;
EnableTrackingProjection = {
Value = true;
Locked = true;
@ -125,59 +127,14 @@ in
EncryptedMediaExtensions = {
Enabled = true;
};
ExtensionSettings =
let
# TODO upstream
addon = id: opts: {
name = id;
value = {
default_area = "menupanel";
ExtensionSettings = {
"uBlock0@raymondhill.net" = {
installation_mode = "force_installed";
install_url = "https://addons.mozilla.org/firefox/downloads/latest/${id}/latest.xpi";
} // opts;
install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
};
enrichAddons = id: opts: if id == "*" then opts else (addon id opts).value;
in
builtins.mapAttrs enrichAddons {
"*" = {
blocked_install_message = ''
Please add add-ons by changing your NixOS configuration.
'';
installation_mode = "blocked";
};
# Cast Kodi
"castkodi@regseb.github.io" = { };
# DeArrow
"deArrow@ajay.app" = { };
# KeePassXC-Browser
"keepassxc-browser@keepassxc.org" = {
default_area = "navbar";
};
# LibRedirect
"7esoorv3@alefvanoon.anonaddy.me" = { };
# Link Gopher
"linkgopher@oooninja.com" = { };
# ProtonDB for Steam
"{30280527-c46c-4e03-bb16-2e3ed94fa57c}" = { };
# Refined GitHub
"{a4c4eda4-fb84-4a84-b4a1-f7c1cbf2a1ad}" = { };
# Request Control
"{1b1e6108-2d88-4f0f-a338-01f9dbcccd6f}" = {
default_area = "navbar";
};
# SponsorBlock
"sponsorBlocker@ajay.app" = { };
# SteamDB
"firefox-extension@steamdb.info" = { };
# Tab Stash
"tab-stash@condordes.net" = {
default_area = "navbar";
};
# Tabliss
"extension@tabliss.io" = { };
# uBlock Origin
"uBlock0@raymondhill.net" = {
default_area = "navbar";
"7esoorv3@alefvanoon.anonaddy.me" = {
# TODO probably just for a test
installation_mode = "allowed";
};
};
FirefoxHome = {
@ -190,60 +147,17 @@ in
Snippets = true;
Locked = true;
};
HttpAllowList = [
"http://hatoria:8088"
"http://penny:8123"
];
HttpsOnlyMode = "force_enabled";
NetworkPrediction = false;
NoDefaultBookmarks = true;
HttpsOnlyMode = "enabled";
OfferToSaveLogins = false;
OverrideFirstRunPage = "";
OverridePostUpdatePage = "";
Permissions = {
Autoplay = {
Default = "block-audio-video";
};
Location = {
BlockNewRequests = true;
Locked = true;
};
};
PopupBlocking = {
Allow = [
"https://app.roll20.net"
# placeholder for more
];
Default = true;
Locked = true;
};
PostQuantumKeyAgreementEnabled = true;
# Preferences set by ..preferences below
PrimaryPassword = true;
SearchBar = "unified";
SearchEngines = {
# TODO setting search engines here only works on ESR
Default = "DuckDuckGo";
};
ShowHomeButton = false;
UserMessaging = {
ExtensionRecommendations = false;
FeatureRecommendations = false;
UrlbarInterventions = false;
SkipOnboarding = true;
MoreFromMozilla = false;
Locked = true;
};
};
preferences = {
"accessibility.typeaheadfind.flashBar" = 0;
"browser.aboutConfig.showWarning" = false;
"browser.language.detectLanguage" = false;
"browser.search.suggest.enabled" = false;
"browser.startup.page" = 3; # restore previous session
"browser.search.suggest.enabled" = false;
"browser.urlbar.showSearchSuggestionsFirst" = false;
"print.more-settings.open" = true;
"security.insecure_connection_text.enabled" = true;
};
};
@ -274,9 +188,7 @@ in
enable = false;
};
# TODO fails as of now & creates CPU spikes every 15 minutes
# journalctl --since="2024-08-21 10:00" --until="2024-08-21 20:20" -u rust-motd
rust-motd = lib.mkIf false {
rust-motd = {
enable = true;
order = [
"banner"
@ -291,7 +203,7 @@ in
let
hostName = config.networking.hostName;
figlet = pkgs.runCommandLocal "static-figlet-${hostName}" { } ''
echo '${hostName}' | ${lib.getExe pkgs.figlet} -f slant > $out
echo '${hostName}' | ${pkgs.figlet}/bin/figlet -f slant > $out
'';
in
{
@ -346,12 +258,14 @@ in
};
security = {
rtkit.enable = lib.mkIf config.services.pipewire.enable true;
};
services = {
desktopManager.plasma6 = {
@ -400,13 +314,11 @@ in
};
printing = {
# cups
enable = true;
cups-pdf = {
enable = true;
};
enableAutoDiscovery = true;
stateless = true; # TODO test
stateless = true; # test
};
pcscd.enable = true;
@ -438,45 +350,18 @@ in
};
specialisation =
let
kernelSpecial = pkg: { configuration.boot.kernelPackages = pkg; };
mapAttrs = builtins.mapAttrs (name: kernelSpecial);
in
mapAttrs {
# TODO enable all kernels with faster build machine
# TODO experiment with gaming kernels
# gaming/performance kernels
#linux_lqx = pkgs.linuxPackages_lqx;
#linux_xanmod_latest = pkgs.linuxPackages_xanmod_latest;
#linux_xanmod_stable = pkgs.linuxPackages_xanmod_stable;
#linux_zen = pkgs.linuxPackages_zen;
# older kernels (for cases like again: https://github.com/NixOS/nixpkgs/issues/330685)
# list of supported kernels taken from https://www.kernel.org/releases.html
#linux_6_6 = pkgs.linuxPackages_6_6;
linux_6_1 = pkgs.linuxPackages_6_1;
#linux_5_15 = pkgs.linuxPackages_5_15;
};
users = {
users.${cfg.username} = {
description = cfg.username;
extraGroups =
with lib.lists;
flatten [
# TODO make user groups an assertion
(optional config.programs.gamemode.enable "gamemode")
(optional config.services.printing.enable "lpadmin")
users."${cfg.username}" = {
description = "${cfg.username}";
extraGroups = with lib.lists; flatten [
(optional config.networking.networkmanager.enable "networkmanger")
(optional config.hardware.sane.enable "scanner")
"wheel"
];
isNormalUser = true;
openssh.authorizedKeys.keys = config.x-banananetwork.sshPublicKeys;
packages =
with pkgs;
lib.lists.flatten [
packages = with pkgs; lib.lists.flatten [
kdePackages.kate
(lib.lists.optional cfg.convertable [
maliit-keyboard # on-screen keyboard (should just work, see https://discuss.kde.org/t/how-to-enable-virtual-keyboard-included-in-kde/264/2)
@ -486,16 +371,6 @@ in
};
virtualisation = {
podman = {
enable = true;
compose.enable = true;
dockerCompat = true;
dockerSocket.enable = true;
};
};
x-banananetwork = {
@ -503,25 +378,29 @@ in
autoUnfree = {
enable = true;
packages = with pkgs.mpvScripts; [
# TODO merge with nixos-modules/frontend/home.nix
packages = with pkgs.mpvScripts; [
evafast
];
};
hwCommon.enable = lib.mkDefault true;
privacy.enable = lib.mkDefault true;
useable.enable = true;
};
# TODO wishlist:
# - lockdown more (at least disable systemd-boot.editor OR enable TPM PCR checks)
# - enable & disable touch keyboard automatically based on convertable status
# - programs.captive-browser
# - https://github.com/cynicsketch/nix-mineral (NixOS hardening)
# - programs.mepo
# - programs.autojump
# - programs.yubikey-touch-detector
};
}

@ -1,9 +1,8 @@
{
config,
lib,
osConfig,
pkgs,
...
{ nixosConfig
, config
, lib
, pkgs
, ...
}:
let
@ -18,7 +17,13 @@ let
in
{
home.stateVersion = osConfig.system.stateVersion;
home = {
stateVersion = nixosConfig.system.stateVersion;
};
home.file = {
@ -40,27 +45,21 @@ in
};
home.packages = with pkgs; [
# dev
neovim
podman
# media
ncspot
sptlrx # spotify subtitle generator
# TODO server with: your_spotify
# dev
nix-output-monitor
# tools
fzf # fuzzy finder # TODO integrate better: https://home-manager-options.extranix.com/?query=fzf&release=master
ipv6calc # IPv4/IPv6 swiss kit
jdupes
kalker # advanced calculator
pcalc # programmers calculator
rink # unit aware calculator
subnetcalc # IPv4/IPv6 subnet info parser
# UI
element-desktop
@ -69,15 +68,11 @@ in
keepassxc
krita
wireshark
tor-browser
trilium-desktop
xournalpp
yubikey-manager
yubioath-flutter
# Gaming
steamcontroller # userspace driver (manual start/stop)
# utilities
fira
(pkgs.nerdfonts.override {
@ -87,36 +82,28 @@ in
];
})
# # You can also create simple shell scripts directly inside your
# # configuration. For example, this adds a command 'my-hello' to your
# # environment:
# (pkgs.writeShellScriptBin "my-hello" ''
# echo "Hello, ${config.home.username}!"
# '')
];
programs = {
bash = {
enable = true;
enableCompletion = true;
historyControl = [
"ignoredups"
"erasedups"
"ignorespace"
];
};
bat = {
enable = true;
extraPackages = with pkgs.bat-extras; [
batdiff
batwatch
prettybat # format code before
];
# fix for https://github.com/nix-community/home-manager/issues/3091, from https://github.com/NixOS/nix/issues/2033#issuecomment-1366974053
#bashrcExtra = ''
# export NIXPATH="/nix/var/nix/profiles/per-user/$USER/channels:nixos-config=/etc/nixos/configuration.nix"
#'';
};
chromium = {
commandLineArgs = [
"--no-default-browser-check"
"--no-first-run"
"--no-service-autorun" # just in case
"--use-system-default-printer" # instead of least recently used
];
dictionaries = with pkgs.hunspellDictsChromium; [
de_DE
en_US
@ -140,19 +127,20 @@ in
git = {
enable = true;
extraConfig =
let
inherit (config.programs) vscode;
in
{
extraConfig = {
diff = {
tool = lib.mkIf vscode.enable "vscode";
tool = "vscode";
};
difftool = {
prompt = false;
};
"difftool \"vscode\"" = lib.mkIf vscode.enable {
cmd = "${lib.getExe vscode.package} --wait --diff $LOCAL $REMOTE";
"difftool \"vscode\"" =
let
vscode = config.programs.vscode.package;
main = pkg: "${pkg}/bin/${pkg.meta.mainProgram}";
in
{
cmd = "${main vscode} --wait --diff $LOCAL $REMOTE";
};
};
userName = "Felix Stupp";
@ -168,17 +156,11 @@ in
mutableKeys = false;
mutableTrust = false;
publicKeys = [
{
source = "${myGpgKey}";
trust = 5;
}
{
source = "${archiveGpgKey}";
trust = 5;
}
{ source = "${myGpgKey}"; trust = 5; }
{ source = "${archiveGpgKey}"; trust = 5; }
];
scdaemonSettings = {
disable-ccid = lib.mkIf osConfig.services.pcscd.enable true;
disable-ccid = lib.mkIf nixosConfig.services.pcscd.enable true;
};
};
@ -192,23 +174,6 @@ in
WHEEL_RIGHT = "ignore";
};
extraInput = ''
# video: move
Alt+Shift+LEFT add video-pan-x 0.01
Alt+Shift+RIGHT add video-pan-x -0.01
Alt+Shift+UP add video-pan-y 0.01
Alt+Shift+DOWN add video-pan-y -0.01
# video: resize
Alt++ ignore
Alt+- ignore
Alt+SHARP add video-zoom 0.01
Alt++ add video-zoom 0.01
Alt+- add video-zoom -0.01
# video: rotate
r cycle_values video-rotate 90 180 270 0
R cycle_values video-rotate 270 180 90 0
# audio
Shift+m af toggle "lavfi=[pan=1c|c0=0.5*c0+0.5*c1]" ; show-text "Audio mix set to Mono"
# playback speed (make keys more sane)
[ ignore
] ignore
[ add speed -0.05
@ -217,12 +182,10 @@ in
} ignore
{ add speed -0.2
} add speed 0.2
# misc
+ script-binding console/enable
'';
config = {
save-position-on-quit = true;
vo = "gpu";
vo = "gpu"; # or: dmabuf-wayland
hwdec = "auto-safe";
gpu-context = "wayland";
alpha = "yes";
@ -230,70 +193,33 @@ in
#ytdl-format = "bestvideo[height<=?1080]+bestaudio/best";
};
scripts = with pkgs.mpvScripts; [
autoload # "autoplay" files in same dir
evafast # VHC rewind effect
modernx-zydezu # modern OSC
autoload
evafast
modernx-zydezu
mpris
mpv-cheatsheet # see all keybindings, use ?
quack # fade volume on seek
quality-menu # select quality on yt-dlp streaming
reload # reload on connection-loss
quack
quality-menu
reload
sponsorblock
thumbfast # thumbnails on modernx
thumbfast
];
scriptOpts =
let
scriptNames = map (p: lib.getName p) config.programs.mpv.scripts;
mkIfScript = name: lib.mkIf (builtins.elem name scriptNames);
in
{
scriptOpts = {
modernx = {
# order by README (https://github.com/zydezu/ModernX#configurable-options)
# general
welcomescreen = true;
# interface
persistentprogress = true;
# button
timetotal = false;
downloadbutton = false;
showyoutubecomments = false;
};
sponsorblock = {
skip_categories = "sponsor,intro,outro,interaction,selfpromo";
local_database = true;
auto_update = true;
skip_once = true;
server_fallback = true;
make_chapters = true;
audio_fade = mkIfScript "quack" false; # quack does the same, but always
fast_forward = false;
# Lua pattern: https://www.lua.org/pil/20.2.html
# TL;DR: '%' = '\', rest is as normal
local_pattern = (lib.mkIf config.programs.yt-dlp.enable "[%s_-]%[([%w-_]+)%]%.[mw][kpe][v4b]m?$"); # tuned for yt-dlp default
};
};
};
retroarch = {
enable = true;
cores = with pkgs.libretro; [
# as recommended by https://emulation.gametechwiki.com
mesen # 1983 NES
sameboy # 1989 GB
bsnes-hd # 1990 SNES
mupen64plus # 1996 N64 (multi, maybe for SNES too; not for NES)
sameboy # 1998 GBC
mgba # 2001 GBA
dolphin # 2001 GCN
melonds # 2004 NDS (+NDSi)
dolphin # 2006 Wii
];
};
texlive = {
enable = true;
# TODO filter hard to save storage
extraPackages = tpkgs: { inherit (tpkgs) scheme-full; };
};
};
vscode = {
@ -303,36 +229,9 @@ in
extensions = with pkgs.vscode-extensions; [
# general
vscodevim.vim
# IDE: Nix
# IDE-specific
jnoortheen.nix-ide
# IDE: Python
ms-python.black-formatter
ms-python.python
];
keybindingsNext = {
# tabbing in *visually visible* order
"ctrl+tab" = [
{
command = "-workbench.action.quickOpenNavigateNextInEditorPicker";
when = "inEditorsPicker && inQuickOpen";
}
"-workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup"
"workbench.action.nextEditor"
];
"ctrl+shift+tab" = [
{
command = "-workbench.action.quickOpenNavigatePreviousInEditorPicker";
when = "inEditorsPicker && inQuickOpen";
}
"-workbench.action.quickOpenLeastRecentlyUsedEditorInGroup"
"workbench.action.previousEditor"
];
# disable overlappings from vim plugin
"ctrl+p" = lib.singleton {
command = "-extension.vim_ctrl+p";
when = "editorTextFocus && vim.active && vim.use<C-p> && !inDebugRepl || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'CommandlineInProgress' || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'SearchInProgressMode'";
};
};
mutableExtensionsDir = false;
package = pkgs.vscodium;
userSettings = {
@ -345,10 +244,10 @@ in
"editor.defaultFormatter" = "ms-python.black-formatter";
};
"ansible.ansibleLint.path" = "${lib.getExe pkgs.ansible-lint}";
"ansible.ansibleLint.path" = "${pkgs.ansible-lint}/bin/ansible-lint";
"dev.containers.dockerComposePath" = "${lib.getExe pkgs.podman-compose}";
"dev.containers.dockerPath" = "${lib.getExe pkgs.podman}";
"dev.containers.dockerComposePath" = "${pkgs.podman-compose}/bin/podman-compose";
"dev.containers.dockerPath" = "${pkgs.podman}/bin/podman";
"diffEditor.ignoreTrimWhitespace" = false;
"diffEditor.renderSideBySide" = false;
@ -411,10 +310,10 @@ in
};
"nix.enableLanguageServer" = true;
"nix.serverPath" = "${lib.getExe pkgs.nil}";
"nix.serverPath" = "${pkgs.nil}/bin/nil";
"nix.serverSettings" = {
nil = {
formatting.command = [ (lib.getExe pkgs.nixfmt-rfc-style) ];
formatting.command = [ "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt" ];
};
};
@ -422,7 +321,7 @@ in
"python.analysis.autoImportCompletions" = true;
"python.analysis.stubPath" = "./typings/";
"python.defaultInterpreterPath" = lib.getExe pkgs.python3;
"python.defaultInterpreterPath" = "${pkgs.python3}/bin/python";
"python.linting.enabled" = false;
"python.showStartPage" = false;
@ -439,7 +338,7 @@ in
"update.mode" = "none";
"update.showReleaseNotes" = false;
"vscode-neovim.neovimPath" = lib.getExe pkgs.neovim;
"vscode-neovim.neovimPath" = "${pkgs.neovim}/bin/nvim";
"vsintellicode.modify.editor.suggestSelection" = "automaticallyOverrodeDefaultValue";
@ -460,7 +359,7 @@ in
ssh = {
enable = true;
controlMaster = "auto";
controlMaster = "autoask";
controlPath = "~/.ssh/connections/%r@%h:%p";
controlPersist = "10m";
matchBlocks = {
@ -481,20 +380,8 @@ in
};
};
yt-dlp = {
enable = true;
settings = {
no-playlist = true; # only relevant if URL refers to video & playlist
remux-video = "aac>m4a/mkv";
sub-format = "ass/srt/best";
sub-langs = "en.*,de.*,-live_chat";
embed-subs = true;
embed-thumbnail = true;
embed-metadata = true; # includes: chapters & info-json
};
};
};
services = {
@ -503,7 +390,6 @@ in
enable = true;
enableExtraSocket = true;
enableScDaemon = true;
# ssh-addkey needs to be done for every key manually, read man gpg-agent
enableSshSupport = true;
enableZshIntegration = true;
pinentryPackage = pkgs.pinentry-qt;
@ -524,6 +410,7 @@ in
# TODO improve fix permanently
systemd.user.services.syncthingtray.Service.ExecStartPre = "sleep 10";
# TODO does not work yet (current: manual config)
#accounts.email.accounts."Mailbox Personal" = {
# primary = true;
@ -551,8 +438,10 @@ in
# };
#};
# ======================================
# hotfix because GUI is managed on system level (fow now)
systemd.user.targets.tray = {
Unit = {
@ -563,18 +452,19 @@ in
# allow unfree limited
# TODO merge with nixos-modules/frontend/default.nix
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
# mpv plugins missing licenses
"evafast"
];
# ZSH config
programs.zsh.enable = true;
programs.zsh.antidote = {
enable = true;
plugins = [ "djui/alias-tips" ];
plugins = [
"djui/alias-tips"
];
};
}

@ -1,14 +1,14 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.hardware.graphics;
in
{
options = {
hardware.graphics = {
@ -23,28 +23,32 @@ in
};
config = lib.mkMerge [
{
assertions = [
{
assertions = [{
assertion = cfg.required -> cfg.amd.enable || cfg.intel.enable;
message = "'hardware.graphics.required' not fullfilled by any of 'hardware.graphics.*.enable'";
}
];
}];
}
(
# TODO replace with drivers
lib.mkIf cfg.amd.enable {
assertions = lib.singleton {
lib.mkIf
cfg.amd.enable
{
assertions = [{
assertion = !cfg.amd.enable;
message = "graphics module missing support for AMD drivers";
};
}];
}
)
(lib.mkIf cfg.intel.enable {
(
lib.mkIf
cfg.intel.enable
{
hardware.opengl = {
enable = true;
extraPackages = with pkgs; [
@ -52,13 +56,11 @@ in
intel-media-sdk
libvdpau-va-gl
];
extraPackages32 = with pkgs.pkgsi686Linux; [
# limited set for Steam & co.
intel-media-driver
];
};
})
}
)
];
}

@ -0,0 +1,151 @@
# applicable to all hosts running on bare hardware
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.hwCommon;
cpu = config.hardware.cpu;
in
{
options = {
hardware.cpu = {
type = lib.mkOption {
description = ''
Configures the CPU type to expect this configuration to run on.
This setting is required when using generalizing options
like option{hardware.cpu.updateMicrocode}.
'';
type = with lib.types; nullOr (enum [
"amd"
"intel"
]);
# required
};
updateMicrocode = lib.mkEnableOption ''
microcode updates for CPU type selected in option{hardware.cpu.type}.
Because this module is not yet part of upstream,
it requires option{x-banananetwork.hwCommon.enable} to be enabled.
'';
};
x-banananetwork.hwCommon = {
enable = lib.mkEnableOption ''
settings common to all bare hardware-based hosts
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.enable -> !config.x-banananetwork.vmCommon.enable;
message = "hwCommon & vmCommon profiles cannot both be enabled at the same time";
}
];
boot = {
# TODO adapt better
loader = {
efi.canTouchEfiVariables = lib.mkDefault true;
systemd-boot = {
enable = true;
editor = lib.mkDefault true; # TODO lockdown (disable this OR enable TPM PCR checks)
memtest86.enable = lib.mkDefault true;
};
};
};
hardware = {
cpu = lib.mkMerge [
# TODO maybe upstream?
(
let
type = config.hardware.cpu.type;
opts = isType: {
updateMicrocode = lib.mkDefault (isType && config.hardware.cpu.updateMicrocode);
};
in
{
amd = opts (type == "amd");
intel = opts (type == "intel");
}
)
{
updateMicrocode = lib.mkDefault true;
}
];
enableRedistributableFirmware = lib.mkDefault true;
};
powerManagement = {
cpuFreqGovernor = "ondemand";
enable = true;
};
services = {
fwupd = {
enable = true;
};
power-profiles-daemon = {
# 2024-08-14: tlp seems way better in my experience, hence disable it
enable = lib.mkIf config.services.tlp.enable false;
};
smartd = {
enable = true;
};
tlp = {
# energy-saving daemon, similar to powertop --autotune, but adaptive to BAT / AC
enable = true;
};
};
x-banananetwork = {
allCommon.enable = true;
useable.enable = lib.mkDefault true; # add docs & tools for emergencies
};
};
}

@ -1,24 +1,22 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.improvedDefaults;
in
{
config = lib.mkIf cfg.enable (
let
nixI = config.programs.nix-index;
shellInt = builtins.any (x: x) (
with nixI;
shellInt = builtins.any (x: x) (with nixI;
[
enableBashIntegration
enableZshIntegration
]
);
]);
nixIclash = nixI.enable && shellInt;
in
{
@ -28,4 +26,5 @@ in
}
);
}

@ -1,24 +1,22 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.improvedDefaults;
in
{
imports = [
./command-not-found.nix
./firefox.nix
./networking.nix
./power-profiles-daemon.nix
./powertop-tlp.nix
./sshAuthorize.nix
./wayland.nix
];
options = {
x-banananetwork.improvedDefaults = {
@ -36,4 +34,5 @@ in
};
}

@ -1,34 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.x-banananetwork.improvedDefaults;
fx = config.programs.firefox;
in
{
config = lib.mkIf (cfg.enable && fx.enable) {
# TODO only on touchscreen / wayland
environment.sessionVariables = {
MOZ_USE_XINPUT2 = "1";
};
programs.firefox = {
preferences = {
"widget.use-xdg-desktop-portal.file-picker" = lib.mkIf config.xdg.portal.enable true;
};
wrapperConfig = {
pipewireSupport = lib.mkIf config.services.pipewire.enable true;
};
};
};
}

@ -1,19 +0,0 @@
{ config, lib, ... }:
let
cfg = config.x-banananetwork.improvedDefaults;
nmEn = config.networking.networkmanager.enable;
waitOnlineEn = config.systemd.network.wait-online.enable;
in
{
config = lib.mkIf cfg.enable {
systemd.network.wait-online.enable = lib.mkIf nmEn (lib.mkDefault false);
warnings = lib.singleton (
lib.mkIf (nmEn && waitOnlineEn) ''
systemd-networkd-wait-online is in most cases useless on systems primarily using NetworkManager & it may increase boot times if it just fails
''
);
};
}

@ -1,10 +0,0 @@
{ config, lib, ... }:
let
cfg = config.x-banananetwork.improvedDefaults;
tlpEn = config.services.tlp.enable;
in
{
# power-profiles-daemon gets enabled by most display managers
# so this suppresses this if another daemon is enabled
config = lib.mkIf cfg.enable { services.power-profiles-daemon.enable = lib.mkIf tlpEn false; };
}

@ -1,14 +1,14 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.improvedDefaults;
in
{
config = lib.mkIf cfg.enable (
let
powertop = config.powerManagement.powertop;
@ -26,4 +26,5 @@ in
}
);
}

@ -1,33 +1,34 @@
{ config, lib, ... }:
{ config
, lib
, pkgs
, ...
}:
let
myOpts = config.x-banananetwork;
cfg = config.x-banananetwork.improvedDefaults;
in
{
options = {
x-banananetwork.improvedDefaults = {
autoSshAuthorizeRoot =
lib.mkEnableOption ''
autoSshAuthorizeRoot = lib.mkEnableOption ''
automatically add option{x-banananetwork.sshPublicKeys} to roots authorized keys
and enable option{services.openssh.settings.PermitRootLogin}
if no other user has "wheel" power & SSH authorized keys defined.
Also, option{services.openssh.settings.PermitRootLogin} will be disabled
if this module does not require it.
''
// {
default = true;
};
'' // { default = true; };
};
};
config =
lib.mkIf
config = lib.mkIf
(lib.lists.all (x: x) [
cfg.enable
cfg.autoSshAuthorizeRoot
@ -39,11 +40,8 @@ in
inherit (lib.lists) any;
# variables
users = config.users.users;
nonRootUsers = lib.trivial.pipe users [
wheelUsers = lib.trivial.pipe users [
(filterAttrs (n: v: n != "root"))
(filterAttrs (n: v: v.isNormalUser))
];
wheelUsers = lib.trivial.pipe nonRootUsers [
(filterAttrs (n: v: builtins.elem "wheel" v.extraGroups))
];
areKeysSet = authKeysOpts: any (x: true) (authKeysOpts.keys ++ authKeysOpts.keyFiles);
@ -52,23 +50,13 @@ in
isNonRootAuthed = any isUserAuthed (attrValues wheelUsers);
isRootAuthed = isUserAuthed users."root";
doRootAuth = !isNonRootAuthed;
otherUserExists = nonRootUsers != [ ];
# explicit installer check required because installer set ups user "nixos" for installation
isInstaller = config.system.nixos.variant_id == "installer";
in
{
# TODO mkOverride until https://github.com/NixOS/nixpkgs/pull/339786
services.openssh.settings.PermitRootLogin =
if isRootAuthed then lib.mkOverride 99 "prohibit-password" else lib.mkDefault "no";
users.users.root.openssh.authorizedKeys.keys = lib.mkIf doRootAuth (
lib.mkDefault myOpts.sshPublicKeys
);
services.openssh.settings.PermitRootLogin = if isRootAuthed then true else lib.mkDefault false;
# warn only if other users exist -> multi-user machine
# compared to "root"-only systems (e.g. installer, embedded systems)
warnings = lib.mkIf (doRootAuth && otherUserExists && !isInstaller) [
users.users.root.openssh.authorizedKeys.keys = lib.mkIf doRootAuth (lib.mkDefault myOpts.sshPublicKeys);
warnings = lib.mkIf doRootAuth [
''
roots authorized keys were automatically configured
because no other user with wheel permission has authorized keys configured
@ -78,4 +66,5 @@ in
}
);
}

@ -1,33 +1,20 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
prgs = config.programs;
servDM = config.services.desktopManager;
xDM = config.services.xserver.desktopManager;
cfg = config.x-banananetwork.improvedDefaults;
in
{
options = {
services.wayland.enable = lib.mkEnableOption ''
sensible defaults for Wayland sessions.
Be aware that a Wayland compositor or desktop environment is not enabled automatically
as there is no main implementation of Wayland.
'';
};
config = lib.mkMerge [
(lib.mkIf (cfg.enable) {
# auto detect if a wayland compatible compositor is already enabled
services.wayland.enable = builtins.any (x: x) ([
config = lib.mkIf cfg.enable (
let
prgs = config.programs;
servDM = config.services.desktopManager;
xDM = config.services.xserver.desktopManager;
waylandEnabled = builtins.any (x: x) ([
prgs.hyprland.enable
prgs.miriway.enable
prgs.river.enable
@ -37,25 +24,14 @@ in
servDM.lomiri.enable # unsure wheather this is using Wayland
servDM.plasma6.enable
]);
})
(lib.mkIf (config.services.wayland.enable) {
# TODO mirror on home-manager
environment.sessionVariables = {
MOZ_ENABLE_WAYLAND = lib.mkIf config.programs.firefox.enable "1";
NIXOS_OZONE_WL = "1";
};
# make Steam Input events possible
programs.steam.extest.enable = lib.mkIf config.programs.steam.enable true;
in
{
warnings = lib.mkIf (xDM.mate.enable && !xDM.mate.enableWaylandSession) [
"Wayland & Mate are enabled, but Mates Wayland support is disabled, you should enable services.xserver.displayManager.enableWaylandSession"
];
# make Steam Input events on Wayland possible
programs.steam.extest.enable = lib.mkIf (config.programs.steam.enable && waylandEnabled) true;
})
}
);
];
}

@ -2,11 +2,10 @@
# for me, most of them are defined in ../mySettings.nix
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
{
@ -14,17 +13,6 @@
x-banananetwork = {
sshHostPublicKey = lib.mkOption {
description = ''
SSH host public key of that system.
This is used by other option{x-banananetwork} modules.
'';
type = with lib.types; nullOr str;
default = null;
example = "ssh-ed25519 ";
};
sshPublicKeys = lib.mkOption {
description = ''
SSH public keys used to manage this system.
@ -32,7 +20,9 @@
This is used by other option{x-banananetwork} modules.
'';
type = with lib.types; listOf str;
example = [ "ssh-ed25519 ..." ];
example = lib.literalExpression ''
[ "ssh-ed25519 ..." ]
'';
};
userName = lib.mkOption {
@ -40,7 +30,7 @@
my username for most/all uses
'';
type = lib.types.str;
example = "zocker";
example = lib.literalExpression "zocker";
};
};

@ -1,29 +0,0 @@
# This file especially especially includes special overlays
# where a full include via the nixpkgs.overlays option is not feasible
# because of a big list of dependencies
# and where in most cases just setting a certain package option should be enough (e.g. systemd).
{
inputs,
lib,
outputs,
...
}@flakeArg:
let
withOverlay =
overlay: configFun:
{ pkgs, ... }:
configFun (
import inputs.nixpkgs {
system = pkgs.system;
overlays = lib.singleton overlay;
}
);
in
{
# TODO until https://github.com/systemd/systemd/issues/29651 is fixed
systemd-radv-fadeout = withOverlay outputs.overlays.systemd-radv-fadeout (pkgs: {
config.systemd.package = pkgs.systemd;
});
}

@ -1,6 +0,0 @@
{
imports = [
# files
./nft-update-addresses.nix
];
}

@ -1,187 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
servName = "nft-update-addresses";
cfg = config.services.${servName};
settingsFormat = pkgs.formats.json { };
mkDisableOption = desc: lib.mkEnableOption desc // { default = true; };
# output options values
configFile = pkgs.writeTextFile {
name = "${servName}.json";
text = builtins.toJSON cfg.settings; # TODO can otherwise not easily check the file for errors
checkPhase = ''
${lib.getExe cfg.package} --check-config --config-file "$out"
'';
};
staticDefs = builtins.readFile (
pkgs.runCommandLocal "${servName}.nftables" { } ''
${lib.getExe cfg.package} --output-set-definitions --config-file ${configFile} > $out
''
);
in
{
options.services.${servName} = {
enable = lib.mkEnableOption "${servName} service";
package = lib.mkPackageOption pkgs (lib.singleton servName) { };
settings = lib.mkOption {
# TODO link to docu
description = "Configuration for ${servName}";
type = settingsFormat.type;
default = {
nftTable = "nixos-fw";
};
example.interfaces = {
wan0 = { };
lan0.ports.tcp = {
exposed = [
{
dest = "aa:bb:cc:dd:ee:ff";
port = 80;
}
{
dest = "aa:bb:cc:00:11:22";
port = 80;
}
];
forwarded = [
{
dest = "aabb-ccdd-eeff";
lanPort = 80;
wanPort = 80;
}
{
dest = "aa.bbcc.0011.22";
lanPort = 80;
wanPort = 8080;
}
];
};
};
};
includeStaticDefinitions = mkDisableOption ''inclusion of static definitions from {option}`services.${servName}.nftablesStaticDefinitions` into the nftables config'';
configurationFile = lib.mkOption {
description = "Path to configuration file used by ${servName}.";
type = lib.types.path; # needs to be available at build time
readOnly = true;
default = configFile;
defaultText = lib.literalExpression "# content as generated from config.services.${servName}.settings";
};
nftablesStaticDefinitions = lib.mkOption {
description = ''
Static definitions provided by ${servName} when called with given configuration.
When {option}`services.${servName}.includeStaticDefinitions (which is default),
these will be already included in your nftables setup.
Otherwise, you can use the value of this output option as you prefer.
'';
readOnly = true;
default = staticDefs;
defaultText = lib.literalExpression "# as provided by ${servName}";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.enable -> config.networking.nftables.enable;
message = "${servName} requires nftables to be configured";
}
# TODO assert for port duplications
];
networking.nftables.tables.${cfg.settings.nftTable}.content = lib.mkIf cfg.includeStaticDefinitions staticDefs;
systemd.services.${servName} = {
description = "IPv6 prefix rules updater for nftables";
after = [
"nftables.service"
"network.target"
];
partOf = lib.singleton "nftables.service";
requisite = lib.singleton "nftables.service";
wantedBy = lib.singleton "multi-user.target";
upheldBy = lib.singleton "systemd-networkd.service";
unitConfig.ReloadPropagatedFrom = lib.singleton "nftables.service";
restartIfChanged = true;
restartTriggers = config.systemd.services.nftables.restartTriggers;
serviceConfig = {
# Service
Type = "notify-reload";
ExecStart = lib.singleton "${lib.getExe cfg.package} ${
lib.cli.toGNUCommandLineShell { } {
config-file = configFile;
ip-command = "${pkgs.iproute2}/bin/ip";
nft-command = lib.getExe pkgs.nftables;
}
}";
RestartSec = "250ms";
RestartSteps = 3;
RestartMaxDelaySec = "3s";
TimeoutSec = "10s";
Restart = "always";
NotifyAccess = "all"; # bash script opens subprocesses in pipes
# Paths
ProtectProc = "noaccess";
ProcSubset = "pid";
CapabilityBoundingSet = [
"CAP_BPF" # nft is compiled to bpf
"CAP_IPC_LOCK" # ?
"CAP_KILL" # ?
"CAP_NET_ADMIN"
];
# Security
NoNewPrivileges = true;
# Process
KeyringMode = "private";
OOMScoreAdjust = 10;
# Scheduling
Nice = -2;
CPUSchedulingPolicy = "fifo";
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateNetwork = false; # breaks nftables
PrivateIPC = true;
PrivateUsers = false; # breaks nftables
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true; # are already loaded
ProtectKernelLogs = true;
ProtectControlGroups = true;
#RestrictAddressFamilies = [
# # ?
# "AF_INET"
# "AF_INET6"
# #"AF_NETLINK"
#];
RestrictNamespaces = true;
RestrictSUIDSGID = true;
#SystemCallFilter = "@basic-io @ipc @network-io @signal @timer" # definitly will break that
#SystemCallLog = "~"; # for debugging; should lock all system calls made
# Resource Control
CPUQuota = "50%";
# TODO test to gather real values
MemoryLow = "8M";
MemoryHigh = "32M";
MemoryMax = "128M";
};
};
};
}

@ -3,11 +3,10 @@
lib,
pkgs,
...
}:
let
}: let
cfg = config.x-banananetwork.privacy;
in
{
in {
options = {
@ -27,17 +26,22 @@ in
};
config = lib.mkIf cfg.enable {
boot.kernel.sysctl = {
"net.ipv6.conf.all.temp_prefered_lft" = 1 * 60 * 60; # = 1h
"net.ipv6.conf.all.temp_valid_lft" = 21 * 60 * 60; # = 21h
"net.ipv6.conf.all.temp_prefered_lft" = 1* 60*60; # = 1h
"net.ipv6.conf.all.temp_valid_lft" = 21 *60*60; # = 21h
};
networking = {
tempAddresses = "default";
};
};
}

@ -1,48 +0,0 @@
# NixOS Router Framework
This is another NixOS router framework working better for my usecase
## Features
- designed for environments with dynamic IP address configs
- uses DHCPv4 on WAN to get private or public IPv4
- uses DHCPv6 on WAN to get public IPv6 prefix via DHCP prefix delegation (DHCP-PD)
- allows easy exposing & forwarding of ports
- exposed port rules auto-adapt to changing IPv6 prefix
- port forwardings (i.e. DNAT) work on IPv4 & IPv6
- configuring them only requires MAC & static IPv4
- configures AdGuard Home as filtering DNS server for clients
- stays mostly compatible with common NixOS networking & firewall configs, e.g.:
- `.openFirewall` & `.allowedTCPPorts`/`.allowedUDPPorts` options continue to work (opens port on all interfaces)
I also develop a NixOS test which tries to verify that these features work as expected, which will be published later in this flake.
### Restrictions
Given all features, this module comes up with a few restrictions (; incomplete list):
- supports only one WAN & one LAN interface
- does not allow easy integration of a VPN network
- fully relies on systemd-networkd for DHCPv4/v6 client, DHCPv4 server & prefix-delegated router advertisements
It is not impossible or really, really hard to overcome these limitations but it may require changing this module in substantional ways.
## Example Use
(**TODO** link to yet uncommited stuff)
## Inspirators
I was inspired to implement this by other, similar projects, which were sadly lacking some features highly important to me.
However, as a form of credit & to provide further ressources to you:
- [nixos-router](https://github.com/chayleaf/nixos-router) by [@chayleaf](https://github.com/chayleaf)
- utilizes network namespaces (mine does not!)
- because of that, (at time of writing) it ditched systemd-networkd for now, which I wanted to use
- was not designed for a environment with dynamic IPs
- [NixOS based router in 2023](https://github.com/ghostbuster91/blogposts/blob/a2374f0039f8cdf4faddeaaa0347661ffc2ec7cf/router2023-part2/main.md) by [@ghostbuster91](https://github.com/ghostbuster91)
- was a useful ressource in creating my module

@ -1,969 +0,0 @@
{
lib, # uses some of my library extensions
...
}@flakeArg:
{ config, ... }:
let
cfg = config.x-banananetwork.routerVM;
# TODO avert to options
wanName = "wan0";
lanName = "lan0";
# my lib
escapeNftablesStr =
arg:
let
inherit (builtins) match replaceStrings;
string = toString arg;
in
if match "[[:alnum:],._+:@%/-]+" string == null then
''"${replaceStrings [ ''"'' ] [ ''\"'' ] string}"''
else
string;
filterAttrs =
filtFun:
assert builtins.isFunction filtFun;
lib.filterAttrs (_: filtFun);
filterMapAttrs =
attr: filtFun: mapFun:
assert builtins.isAttrs attr;
assert builtins.isFunction filtFun;
assert builtins.isFunction mapFun;
builtins.mapAttrs (_: mapFun) (filterAttrs filtFun attr);
filterMapAttrsToList =
attr: filtFun: mapFun:
assert builtins.isAttrs attr;
assert builtins.isFunction filtFun;
assert builtins.isFunction mapFun;
lib.mapAttrsToList (_: mapFun) (filterAttrs filtFun attr);
# useful to combine port rules
mapListJoin =
sep: list: mapFun:
assert builtins.isString sep;
assert builtins.isList list;
assert builtins.isFunction mapFun;
builtins.concatStringsSep sep (builtins.map mapFun list);
filterMap =
list: mapFun:
assert builtins.isList list;
assert builtins.isFunction mapFun;
map mapFun (builtins.filter (x: x.enable) list);
filterMapJoin =
sep: attr: mapFun:
assert builtins.isString sep;
assert builtins.isAttrs attr;
assert builtins.isFunction mapFun;
lib.trivial.pipe mapFun [
(filterMapAttrsToList attr (x: x.enable))
(builtins.filter (x: x != null && x != ""))
(builtins.concatStringsSep sep)
];
mkDisableOption = arg: (lib.mkEnableOption arg) // { default = true; };
# TODO think about to make it just readOnly, requiring defaultText
mkOutputOption =
arg:
lib.mkOption (
{
internal = true;
readOnly = true;
}
// arg
);
mkInterfaceOption =
name:
lib.mkOption {
description = ''
MAC address of device to be used as ${name} interface.
Corresponds to the MACAdress= option in the [Match] section
in {manpage}`systemd.network(5)` files.
'';
type = lib.types.eui48;
example = "AA:BB:CC:DD:EE:FF";
};
whitespaceList = list: builtins.concatStringsSep " " list;
hostnameType = lib.types.strMatching "^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" // {
description = "hostname as per RFC";
};
formatMAC =
let
badChars = [
"."
":"
"_"
"-"
];
goodChars = map (x: "") badChars;
in
mac:
lib.trivial.pipe mac [
(builtins.replaceStrings badChars goodChars)
lib.strings.toLower
];
protoList = [
"dccp"
"sctp"
"tcp"
"udp"
"udplite"
];
protoType = lib.types.enum protoList;
# required for IPv4 as handled at built time
dnatMapBuilder =
proto: destMap: devices: embedFun:
let
devFilt = filterAttrs (dev: (destMap dev) != null) devices;
protoMap = dev: (dev.nftUpdateAddressesConfig destMap).${proto}.forwarded;
mapEntry =
{
wanPort,
dest,
lanPort,
}:
"${toString wanPort} : ${dest} . ${toString lanPort}";
builtMap = dev: lib.concatMapStringsSep ", " mapEntry (protoMap dev);
built = filterMapJoin ", " devFilt builtMap;
in
if built == "" then "" else embedFun built;
# TODO allow exposure of devices without static IPs
# - by marking "dropped" packets during forwarding instead
# - and then filtering them on postrouting using dest MAC
# - disadvantage: enables use of all addresses (incl. privacy extensions)
deviceSubmodule =
{ name, ... }@device:
let
dev = device.config;
in
{
options = {
enable = lib.mkOption {
description = "Configure rules for this device";
type = lib.types.bool;
default = true;
};
name = lib.mkOption {
description = "hostname of the device";
type = hostnameType;
default = name;
};
description = lib.mkOption {
description = "Descriptive, human-readable name for that device";
type = lib.types.str;
default = name;
};
mac = lib.mkOption {
description = "MAC Address of device";
type = lib.types.eui48;
};
staticIPv4 = lib.mkOption {
description = "Static DHCPv4 lease address for device";
type = with lib.types; nullOr ipv4AddressPlain;
default = null;
};
fullyExposed = lib.mkEnableOption "to fully expose this device via IPv6, making other firewall rules except NATs irrelevant";
forwardedExposed = mkDisableOption ''
Whether to enable automatic exposure of port forwardings via IPv6.
For example, if a port forwarding from 8080 to 80 is established,
then the port 80 will also be exposed directly for the devices IPv6,
making using NAT optional'';
# TODO really limit ICMP pings to IPv6 addresses (currently fully allowed)
allowICMPEcho = lib.mkEnableOption "forwarding of ICMP echos via IPv6 to the device" // {
default = device.config.exposedPorts != [ ];
defaultText = lib.literalExpression '''';
};
exposedPorts = lib.mkOption {
description = ''
Ports which should be accessible to the public via IPv6.
Adding one port does enable ICMP pings to that host.
'';
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }@exposed:
let
cfg = exposed.config;
comment = "comment ${escapeNftablesStr cfg.comment}";
accept = "accept ${comment}";
in
{
options = {
enable = mkDisableOption "this port exposure rule";
comment = lib.mkOption {
description = ''
Comment for this port forwarding rule.
Will be added to nftables rules.
'';
default = "${device.name} - ${name}";
defaultText = lib.literalExpression ''''${device.name} - ''${name}'';
};
port = lib.mkOption {
description = "port";
type = lib.types.port;
};
protocol = lib.mkOption {
description = "Protocol for which the port forwarding will be established";
type = protoType;
default = "tcp";
};
nftablesForwardingRules = lib.mkOption {
internal = true;
default = ''
ip6 daddr ${dev.nftablesIPv6Dest} ${cfg.protocol} dport ${toString cfg.port} ${accept}
'';
};
};
}
)
);
default = { };
};
forwardedPorts = lib.mkOption {
description = ''
Ports which should be forwarded from the router WAN IPv4 & v6 address to this device.
The port is then made accessible using DNAT
on all packets sent to the routers public interface
via IPv4 (if `config`{staticIPv4}` is set) and IPv6.
'';
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }@forwarded:
let
cfg = forwarded.config;
comment = "comment ${escapeNftablesStr cfg.comment}";
accept = "accept ${comment}";
l4Rule = "${cfg.protocol} dport ${toString cfg.lanPort} ${accept}";
l4WanRule = "${cfg.protocol} dport ${toString cfg.wanPort}";
in
{
options = {
enable = mkDisableOption "this port forwarding rule";
comment = lib.mkOption {
description = ''
Comment for this port forwarding rule.
Will be added to nftables rules.
'';
default = "${device.name} - ${name}";
defaultText = lib.literalExpression ''''${device.name} - ''${name}'';
};
protocol = lib.mkOption {
description = "Protocol for which the port forwarding will be established.";
type = protoType;
default = "tcp";
};
wanPort = lib.mkOption {
description = "port on the WAN side";
type = lib.types.port;
default = cfg.lanPort;
defaultText = lib.literalExpression "cfg.lanPort";
};
lanPort = lib.mkOption {
description = "port on the LAN / client side";
type = lib.types.port;
};
expose = lib.mkOption {
description = "Enables exposure of {option}`lanPort` directly via IPv6";
type = lib.types.bool;
default = device.config.forwardedExposed;
defaultText = lib.literalExpression "cfg.forwardedExposed";
};
nftablesForwardingRules = mkOutputOption {
default = ''
${lib.optionalString (dev.staticIPv4 != null) "ip daddr ${dev.staticIPv4} ${l4Rule}"}
ip6 daddr ${dev.nftablesIPv6Dest} ${l4Rule}
'';
};
nftablesPreroutingRules = mkOutputOption {
default = ''
${lib.optionalString (
dev.nftablesIPv4Dest != null
) "${l4WanRule} dnat ip to ${dev.nftablesIPv4Dest}:${toString cfg.lanPort} ${comment}"}
'';
};
};
}
)
);
default = { };
example = {
http = {
wanPort = 8080;
lanPort = 80;
};
quic = {
wanPort = 8443;
lanPort = 443;
protocol = "udp";
};
};
};
nftablesIPv4Dest = mkOutputOption { default = dev.staticIPv4; };
nftablesIPv6Dest = mkOutputOption { default = ''@${lanName}v6_${formatMAC dev.mac}''; };
nftUpdateAddressesConfig = mkOutputOption {
default =
destMap:
let
inherit (builtins) attrValues groupBy;
inherit (lib.attrsets) genAttrs;
groupByProto = attrs: groupBy (x: x.protocol) (attrValues attrs);
exposed = groupByProto dev.exposedPorts;
forwarded = groupByProto dev.forwardedPorts;
in
genAttrs protoList (proto: {
exposed = filterMap (exposed.${proto} or [ ]) (exp: {
inherit (exp) port;
dest = destMap dev;
});
forwarded = filterMap (forwarded.${proto} or [ ]) (fw: {
inherit (fw) wanPort lanPort;
dest = destMap dev;
});
});
};
nftablesForwardingRules = mkOutputOption {
default =
if dev.fullyExposed then
''
ip6 daddr ${dev.nftablesIPv6Dest} accept comment ${escapeNftablesStr "${dev.description} full exposure"}
''
else
''
${filterMapJoin "\n" dev.exposedPorts (ep: ep.nftablesForwardingRules)}
${filterMapJoin "\n" dev.forwardedPorts (fw: fw.nftablesForwardingRules)}
ip daddr ${dev.nftablesIPv4Dest} jump drop-reject
ip6 daddr ${dev.nftablesIPv6Dest} jump drop-reject
'';
};
nftablesPreroutingRules = mkOutputOption {
default = filterMapJoin "\n" dev.forwardedPorts (fw: fw.nftablesPreroutingRules);
};
};
config = {
exposedPorts = filterMapAttrs dev.forwardedPorts (fw: fw.enable && fw.expose) (fw: {
comment = "${fw.comment} (exposed port forwarding)";
port = fw.lanPort;
inherit (fw) protocol;
});
};
};
# automatically assign mark integers for these reasons (if enabled)
nftMarksDeclared = {
inherit (cfg) protectWanSubnets;
"${wanName}_snatShortcutted" = true;
};
nftMarks = lib.trivial.pipe nftMarksDeclared [
lib.attrsToList
(builtins.filter (x: x.value))
(lib.imap1 (
i: m: {
inherit (m) name;
value = toString i; # because will mostly be used in nftables rules
}
))
builtins.listToAttrs
];
in
{
options = {
boot.loader.systemd-boot.bootCounting.enable = lib.mkOption {
description = ''
to already pin option which will be coming in the future
see https://nixos.org/manual/nixos/unstable/#sec-automatic-boot-assessment
'';
visible = false;
type = lib.types.bool;
};
x-banananetwork.routerVM = {
enable = lib.mkEnableOption "router functionality. This is intended to be disabled by a specialisation for recovery reasons";
wanMAC = mkInterfaceOption "WAN";
lanMAC = mkInterfaceOption "VM LAN";
lanDomain = lib.mkOption {
description = "domain advertised via DHCP / RA / DHCPv6";
type = lib.types.str;
default = config.networking.domain;
defaultText = lib.literalExpression "config.networking.domain";
};
lanIPv4Address = lib.mkOption {
description = ''
The IPv4 address used for the router &
its subnet used for addresses of clients.
'';
type = lib.types.str;
};
lanIPv6ULAPrefix = lib.mkOption {
description = ''
An IPv6 unique local address prefix to announce on LAN as well.
'';
type = lib.types.str;
};
trustableIPv6Prefixes = lib.mkOption {
description = ''
A list of IPv6 prefixes which should be trustable.
In general, this means following features should be locked to trustable links only:
- announced routes to these prefixes
- TODO packets from/to these prefixes to/from links
'';
type = with lib.types; listOf str;
default = [ ];
};
# TODO DNS entries for WAN & LAN side
lanDevices = lib.mkOption {
description = ''
Describes properties for a LAN device
(e.g. DHCP static leases & port forwardings).
For IPv6 based rules to work,
devices must use their stable EUI64 IPv6 address
for the prefix announced via SLAAC.
You can recognise those as they reflect the MAC address of the interface.
The router cannot forward ports for devices only using
private IPv6 addresses according to
RFC 4941 (IPv6 privacy extensions)
or RFC 7217 (stable private IPv6 addresses).
'';
type = lib.types.attrsOf (lib.types.submodule deviceSubmodule);
default = { };
};
lanEmitRejections = lib.mkEnableOption ''
Whether to emit ICMP(v6) rejections to the trusted LAN
if the routers firewall blocks a package.
Should make it easier to debug cases
where the router blocks certain packages'';
# TODO test
protectWanSubnets = lib.mkEnableOption ''
firewall rules *trying* to protect the WANs local subnets.
Despite best efforts, I cannot make any gurantees
that this can prevent all attempts from LAN devices accessing WAN devices,
especially because of possible, additional IPv6 subnets.'';
acceptableWanMACs = lib.mkOption {
description = ''
When {option}`x-banananetwork.routerVM.protectWanSubnets` is enabled,
this provides a list of devices which should still be accessible.
If you want your LAN to have connection to the Internet,
this list MUST include your gateways MAC address.
'';
type = lib.types.listOf lib.types.eui48;
default = [ ];
example = [ "AA:BB:CC:DD:EE:FF" ];
};
dns = {
upstreams = lib.mkOption {
description = "List of DNS servers used by DNS server for clients.";
type = lib.types.listOf lib.types.str;
};
fallbacks = lib.mkOption {
description = "List of DNS servers used as fallbacks (i.e. when all upstreams are unreachable). Used by DNS server for clients.";
type = lib.types.listOf lib.types.str;
default = [ ];
};
bootstraps = lib.mkOption {
description = "List of DNS servers used to bootstrap list of other DNS servers. Used by DNS server for clients.";
type = lib.types.listOf lib.types.str;
default = cfg.dns.localFallbacks;
defaultText = lib.literalExpression "config.x-banananetwork.routerVM.dns.localFallbacks";
};
localFallbacks = lib.mkOption {
description = "List of DNS servers used as local fallbacks, i.e. added aside of own DNS server to {file}`/etc/resolv.conf` of router itself.";
type = with lib.types; listOf ipAddress;
default = [ ];
};
webui.username = lib.mkOption {
description = "Username for AdGuard Home admin account.";
type = lib.types.str;
};
webui.password = lib.mkOption {
description = ''
Hash for AdGuard Home admin account.
Can be created with .e.g. {command}`mkpasswd --method=bcrypt`'
For more info, read in the [Adguard Home Wiki](https://github.com/AdguardTeam/AdguardHome/wiki/Configuration#reset-web-password)
'';
type = lib.types.str;
};
filterlists = lib.mkOption {
description = "List of URLs of filterlists which entries should be blocked";
type = with lib.types; listOf str;
default = [ ];
};
};
};
};
# TODO configure NAT64 with 464XLAT
# requires: https://github.com/systemd/systemd/issues/23674
# or services.clat.enable = true; & test how it works
# IPv6 prefix delegation config inspired by: https://major.io/p/dhcpv6-prefix-delegation-with-systemd-networkd/
config = lib.mkIf cfg.enable {
warnings = lib.singleton (
lib.mkIf (lib.versionAtLeast lib.version "24.11") "remove manual sysctl for ip forwarding, as can be replaced by systemd-networkd settings"
);
# I will pin a lot of stuff, to ensure future changes on NixOS are noticed
networking = {
enableIPv6 = true; # also just a statement
nameservers = [
# localhost
"::1"
"127.0.0.1"
] ++ cfg.dns.localFallbacks;
tempAddresses = "disabled"; # do not manage that here
useDHCP = false; # do not intervene with router config
useNetworkd = true;
};
services.resolved.enable = false; # invoked by systemd.network
# general
systemd.network = {
enable = true;
config.networkConfig = lib.mkIf (lib.versionAtLeast lib.version "24.11") {
# these options are introduced with systemd 256 -> NixOS 24.11
IPv4Forwarding = true;
IPv6Forwarding = true;
UseDomains = false;
};
wait-online = {
anyInterface = false; # all which are listed as required
enable = true;
# more is configured per netdev/network as RequiredForOnline
# TODO add that hint to nixos docs in systemd.network.wait-online.anyInterface etc.
};
};
boot.kernel.sysctl = lib.mkIf (!lib.versionAtLeast lib.version "24.11") {
"net.ipv4.conf.default.forwarding" = true;
"net.ipv4.conf.all.forwarding" = true;
"net.ipv6.conf.default.forwarding" = true;
"net.ipv6.conf.all.forwarding" = true;
};
# expose for easier debugging
environment.systemPackages = lib.singleton config.services.nft-update-addresses.package;
# following settings are ordered according to the respective man pages
# configure "real" devices
systemd.network.links =
let
# defaults
linkConfig = {
MACAddressPolicy = "persistent";
NamePolicy = ""; # -> use Name=
AlternativeNamesPolicy = "mac slot path onboard";
AutoNegotiation = true;
};
in
{
# auto-generated files are using numbers "70" & higher
"10-${wanName}" = {
matchConfig.PermanentMACAddress = cfg.wanMAC;
linkConfig = linkConfig // {
Name = wanName;
Description = "WAN device";
};
};
"10-${lanName}" = {
matchConfig.PermanentMACAddress = cfg.lanMAC;
linkConfig = linkConfig // {
Name = lanName;
Description = "LAN device";
};
};
};
# virtual devices
systemd.network.netdevs = { };
# networks
systemd.network.networks = {
"10-${wanName}" = {
matchConfig.Name = wanName;
linkConfig = {
RequiredForOnline = "degraded"; # = has addresses assigned
RequiredFamilyForOnline = "any";
};
networkConfig = {
DHCP = "yes"; # both IPv4 & IPv6 (prefix delegation)
LinkLocalAddressing = "ipv6";
IPv6LinkLocalAddressGenerationMode = "eui64";
LLMNR = false;
MulticastDNS = false;
LLDP = "routers-only";
DNSDefaultRoute = true;
IPv6AcceptRA = true;
};
dhcpV6Config = {
PrefixDelegationHint = "::/60"; # TODO make configurable
};
ipv6AcceptRAConfig = {
PrefixDenyList = whitespaceList cfg.trustableIPv6Prefixes;
RouteDenyList = whitespaceList cfg.trustableIPv6Prefixes;
};
};
"10-${lanName}" = {
matchConfig.Name = lanName;
linkConfig = {
RequiredForOnline = false;
};
networkConfig = {
DHCPServer = true;
LinkLocalAddressing = "ipv6";
IPv6LinkLocalAddressGenerationMode = "eui64";
LLMNR = true;
MulticastDNS = true;
LLDP = "routers-only";
EmitLLDP = true;
IPv6AcceptRA = false;
IPv4ReversePathFilter = "strict";
IPv6SendRA = true;
DHCPPrefixDelegation = true;
};
address = lib.singleton "fe80::1/64"; # also link-local router
dhcpPrefixDelegationConfig = {
UplinkInterface = "${wanName}";
SubnetId = "0x0";
Announce = true;
Assign = true;
Token = "static:::1";
ManageTemporaryAddress = false;
};
dhcpServerConfig = {
ServerAddress = cfg.lanIPv4Address;
# pool automatically set from CIDR
UplinkInterface = ":none";
DNS = "_server_address";
# TODO emit domain ipv4 as well
SendOption = lib.singleton "15:string:${cfg.lanDomain}";
EmitNTP = false; # IPv6 ftw.
EmitSIP = false;
EmitPOP3 = false;
EmitSMTP = false;
EmitLPR = false;
};
dhcpServerStaticLeases =
filterMapAttrsToList cfg.lanDevices (dev: dev.enable && dev.staticIPv4 != null)
(dev: {
dhcpServerStaticLeaseConfig = {
MACAddress = dev.mac;
Address = dev.staticIPv4;
};
});
ipv6SendRAConfig = {
# TODO until https://github.com/systemd/systemd/issues/29651 is fixed:
RouterLifetimeSec = lib.mkDefault (5 * 60); # set so low in case of a prefix renewal
UplinkInterface = ":none";
DNS = "_link_local";
Domains = whitespaceList (lib.singleton cfg.lanDomain);
};
ipv6Prefixes = lib.singleton {
ipv6PrefixConfig = {
Prefix = cfg.lanIPv6ULAPrefix;
Assign = true;
Token = "static:::1";
};
};
};
};
# firewall
networking.firewall = {
enable = true;
checkReversePath = "strict";
filterForward = false; # replace with own chains
extraInputRules = ''
jump router-inbound
'';
};
networking.nftables = {
enable = true;
# TODO assert that openFirewall / allowedTCP/UDPPorts does not collide with DNAT rules
tables = {
# append to OS table
"nixos-fw".content = ''
set v6ula {
type ipv6_addr
flags interval
elements = { fc00::/7 }
}
${mapListJoin "\n" protoList (proto: ''
map ${lanName}v4dnat${proto} {
type inet_service : ipv4_addr . inet_service
${
dnatMapBuilder proto (dev: dev.staticIPv4) cfg.lanDevices (built: ''
elements = { ${built} }
'')
}
}
'')}
chain drop-reject {
ct state invalid drop comment "just drop early packages"
icmp type destination-unreachable drop comment "avoid virtual loop"
icmpv6 type destination-unreachable drop comment "avoid virtual loop"
${lib.optionalString cfg.lanEmitRejections ''
iifname ${lanName} ip saddr ${lanName}v4net reject comment "emit error to trusted net"
''}
drop comment "drop anything else"
}
chain global {
ct state established,related accept
ct state invalid jump drop-reject
ct state != { new, untracked } jump drop-reject
}
# TODO export/upstream
# these assume working connection state tracking
chain rfc4890-icmpv6-site-both {
# TODO required? icmpv6 type echo-reply accept "already established"
ip6 daddr fe80::/10 ip6 nexthdr icmpv6 drop comment "do not route link-local stuff, just in case"
ip6 daddr ff00::/8 icmpv6 type echo-reply drop comment "drop ping responses to multicast"
icmpv6 type parameter-problem icmpv6 code 0 accept comment "bad header"
icmpv6 type {
mld-listener-query,
mld-listener-report,
mld-listener-done,
mld-listener-reduction,
nd-router-solicit,
nd-router-advert,
nd-neighbor-solicit,
nd-neighbor-advert,
nd-redirect,
router-renumbering,
139, 140, # node information queries / replies
} jump drop-reject
ip6 nexthdr icmpv6 jump drop-reject
}
chain rfc4890-icmpv6-site-inbound {
# TODO allow pingable hosts (but not here)
icmpv6 type time-exceeded icmpv6 code 1 accept comment "reassembly failed"
jump rfc4890-icmpv6-site-both
}
chain rfc4890-icmpv6-site-outbound {
icmpv6 type { echo-request, destination-unreachable, packet-too-big } accept;
icmpv6 type time-exceeded icmpv6 code { 0, 1 } accept comment "transit/reassembly failed"
icmpv6 type parameter-problem icmpv6 code { 1, 2 } accept comment "unknown header-type/option"
jump rfc4890-icmpv6-site-both
}
chain router-inbound {
# stuff with .openFirewall is already accepted
# to global just in case
jump global
iifname ${wanName} jump ${wanName}-inbound
iifname ${lanName} jump ${lanName}-inbound
}
chain ${wanName}-inbound {
ip6 daddr @${lanName}v6net jump drop-reject comment "drop requests from outbound to router"
}
chain ${lanName}-inbound {
ip version 4 udp sport 68 udp dport 67 accept comment "DHCPv4"
icmpv6 type nd-router-solicit accept comment "IPv6 SLAAC"
udp dport 53 accept comment "DNS"
tcp dport 53 accept comment "DNS"
}
chain router-forward {
type filter hook forward priority filter; policy drop;
jump global
ct status dnat accept
iifname ${lanName} oifname ${wanName} jump ${lanName}-to-${wanName}
iifname ${wanName} oifname ${lanName} jump ${wanName}-to-${lanName}
jump drop-reject
}
chain ${wanName}-to-${lanName} {
jump rfc4890-icmpv6-site-inbound
${filterMapJoin "\n" cfg.lanDevices (dev: dev.nftablesForwardingRules)}
}
chain ${lanName}-to-${wanName} {
jump rfc4890-icmpv6-site-outbound
${lib.optionalString cfg.protectWanSubnets ''
# TODO try with & without mark
#meta mark set ${nftMarks.protectWanSubnets} comment "protect WAN subnets"
ip daddr @${wanName}v6net reject comment "protect ${wanName} subnet"
ip6 daddr @${wanName}v6net reject comment "protect ${wanName} subnet"
''}
ip6 saddr @v6ula reject comment "no general outbound with unique link locals"
ip6 daddr @v6ula reject comment "no general outbound to unique link locals"
ip saddr @${lanName}v4net accept comment "outbound allowed"
ip6 saddr @${lanName}v6net accept comment "outbound allowed"
}
chain router-prerouting {
type nat hook prerouting priority -100; policy accept;
${
mapListJoin "\n" protoList (proto: ''
iifname != ${wanName} ip daddr @${wanName}v4addr meta mark set ${
nftMarks."${wanName}_snatShortcutted"
}
iifname != ${wanName} ip6 daddr @${wanName}v6addr meta mark set ${
nftMarks."${wanName}_snatShortcutted"
}
ip daddr @${wanName}v4addr dnat ip to ${proto} dport map @${lanName}v4dnat${proto}
ip6 daddr @${wanName}v6addr dnat ip6 to ${proto} dport map @${lanName}v6dnat${proto}
'')
}
}
chain router-postrouting {
type nat hook postrouting priority 100; policy accept;
iifname ${lanName} oifname ${wanName} ip version 4 masquerade comment "persistent not required as only one source addr available, avoid randonmness"
meta mark ${nftMarks."${wanName}_snatShortcutted"} masquerade
}
'';
"router-netdev" = lib.mkIf cfg.protectWanSubnets {
family = "netdev";
content = ''
set wan_accepted {
typeof ether daddr
elements = { ${builtins.concatStringsSep ", " cfg.acceptableWanMACs} }
}
chain egress {
meta mark ${nftMarks.protectWanSubnets} ether daddr @wan_accepted accept
meta mark ${nftMarks.protectWanSubnets} jump drop-reject
}
'';
};
};
};
boot.blockedKernelModules = [
"ip_tables"
"iptable_nat"
];
# prefix updater
services.nft-update-addresses = {
enable = true;
settings =
let
inherit (builtins) attrValues;
inherit (lib.attrsets) genAttrs;
inherit (lib.lists) flatten;
flatMap = mapFun: list: flatten (map mapFun list);
destMap = dev: dev.mac;
portMap = proto: dev: (dev.nftUpdateAddressesConfig destMap).${proto};
combinePortCfg = devCfgs: {
exposed = flatMap (devCfg: devCfg.exposed) devCfgs;
forwarded = flatMap (devCfg: devCfg.forwarded) devCfgs;
};
combinePerProto =
devices: proto:
lib.trivial.pipe proto [
portMap
(filterMap (attrValues devices))
combinePortCfg
];
protoPortCfg = devices: genAttrs protoList (combinePerProto devices);
in
{
nftTable = "nixos-fw";
interfaces = {
${wanName} = { };
${lanName} = {
macs = lib.mapAttrsToList (_: dev: dev.mac) cfg.lanDevices;
ports = protoPortCfg cfg.lanDevices;
};
};
};
};
# DNS server
# TODO exclude router from filtering itself (without config)
# TODO add support for https://opennic.org/
# TODO change NixOS upstream module to remove yaml-merge dependency if not required
services.adguardhome = {
enable = true;
allowDHCP = false;
mutableSettings = false;
port = 3000;
settings = {
http = {
session_ttl = "5:00";
};
users = lib.singleton {
name = "admin";
password = ""; # TODO bcrypt htpasswd random stuff
};
auth_attempts = 5;
block_auth_min = 5;
dns = {
port = 53;
anonymize_client_ip = false;
ratelimit = 100; # queries / second
ratelimit_subnet_len_ipv4 = 32;
ratelimit_subnet_len_ipv6 = 128;
refuse_any = true;
upstream_dns = cfg.dns.upstreams;
fallback_dns = cfg.dns.fallbacks;
bootstrap_dns = cfg.dns.bootstraps;
local_ptr_upstreams = [
# TODO
];
upstream_mode = "load_balance";
upstream_timeout = "10s";
use_http3_upstream = true;
enable_dnssec = true;
cache_time = 36000;
resolve_clients = true;
serve_plain_dns = true;
hostsfile_enabled = false;
};
filtering = {
protection_enabled = true;
filtering_enabled = true;
blocking_mode = "nxdomain";
blocked_response_ttl = 60; # seconds
safe_search.enabled = false;
safebrowsing_enabled = true;
};
querylog = {
enabled = true;
file_enabled = true;
interval = "${toString (2 * 24)}h";
};
statistics = {
enabled = true;
interval = "${toString (7 * 24)}h";
};
filters = lib.trivial.flip map cfg.dns.filterlists (url: {
enabled = true;
inherit url;
name = url;
ID = builtins.hashString "sha1" url;
});
dhcp.enabled = false;
tls.enabled = false;
log.file = "syslog"; # journal
};
};
# fallback specialisation for trivial network access
specialisation.trivialNetwork.configuration = {
x-banananetwork.routerVM.enable = lib.mkForce false;
networking = {
# DHCP on all interfaces
useDHCP = lib.mkForce true;
useNetworkd = lib.mkForce true;
};
};
# order of specialisation v. older generation not clear
# and no plan which order is senseful, for now
boot.loader.systemd-boot.bootCounting.enable = false;
};
}

@ -1,8 +1,7 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
myOpts = config.x-banananetwork;
@ -10,6 +9,7 @@ let
in
{
options = {
x-banananetwork.secrix = {
@ -33,52 +33,44 @@ in
'';
type = with lib.types; nullOr str;
default = null;
example = "rsa";
example = lib.literalExpression "rsa";
};
};
};
config = lib.mkIf cfg.enable {
# cannot be part of upstream because secrets may also have individual keys
# but I will not use any individual keys
assertions =
let
inherit (builtins) attrValues concatLists;
secr = config.secrix;
systemSecrets = attrValues secr.system.secrets;
serviceSecrets = concatLists (map attrValues (attrValues secr.services));
allSecrets = concatLists [
systemSecrets
serviceSecrets
];
anySecretDefined = allSecrets != [ ];
in
[
assertions = [
{
assertion = anySecretDefined -> config.secrix.hostPubKey != null;
assertion = config.secrix.hostPubKey != null;
message = "secrix.hostPubKey must be defined";
}
];
secrix =
let
findHostKey =
keyType:
lib.lists.findSingle (key: key.type == keyType)
findHostKey = keyType: lib.lists.findSingle
(key: key.type == keyType)
(abort "cannot find generated OpenSSH host key with type ${keyType}")
(abort "found multiple generated OpenSSH host keys with type ${keyType}")
config.services.openssh.hostKeys;
hostKeyPrivate = (findHostKey cfg.hostKeyType).path;
in
{
defaultEncryptKeys."${myOpts.userName}" = myOpts.sshPublicKeys;
hostIdentityFile = lib.mkIf (cfg.hostKeyType != null) (lib.mkDefault hostKeyPrivate);
hostPubKey = myOpts.sshHostPublicKey;
};
};
}

@ -1,21 +1,23 @@
{
config,
lib,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.openssh;
in
{
options.services.openssh = {
authorizedKeysOnly = lib.mkEnableOption ''
options = {
services.openssh.authorizedKeysOnly = lib.mkEnableOption ''
only logins using ssh keys (improving over default settings)
'';
};
config = lib.mkIf cfg.enable {
services.openssh = {
@ -27,6 +29,9 @@ in
};
# TODO add tests
}

@ -1,14 +1,14 @@
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.useable;
in
{
options = {
x-banananetwork.useable = {
@ -23,47 +23,48 @@ in
};
config = lib.mkIf cfg.enable {
documentation = {
enable = lib.mkDefault true;
dev.enable = lib.mkDefault true;
doc.enable = lib.mkDefault true;
info.enable = lib.mkDefault true;
enable = true;
dev.enable = true;
doc.enable = true;
info.enable = true;
man = {
enable = lib.mkDefault true;
enable = true;
generateCaches = true;
man-db.enable = lib.mkDefault false; # see mandoc
man-db.enable = false; # see mandoc
mandoc = {
enable = lib.mkDefault true;
enable = true;
};
};
nixos = {
enable = lib.mkDefault true;
includeAllModules = lib.mkDefault true;
enable = true;
includeAllModules = true;
};
};
environment.systemPackages =
with pkgs;
let
environment.systemPackages = with pkgs; let
inherit (lib.lists) flatten optional optionals;
in
flatten [
(optional (
config.services.hardware.bolt.enable && config.services.desktopManager.plasma6.enable
) kdePackages.plasma-thunderbolt) # TODO upstream
(optional (config.services.hardware.bolt.enable && config.services.desktopManager.plasma6.enable) kdePackages.plasma-thunderbolt) # TODO upstream
(optionals config.hardware.graphics.amd.enable [ nvtopPackages.amd ])
(optionals config.hardware.graphics.amd.enable [
nvtopPackages.amd
])
(optionals config.hardware.graphics.intel.enable [
intel-gpu-tools
nvtopPackages.intel
@ -71,16 +72,22 @@ in
bat
batmon # TODO only on systems wich batteries
jq # JSON
manix
massren
nethogs
reptyr
pciutils
psitop
pv
unixtools.xxd
up # ultimate plumber
usbtop
usbutils
];
programs = {
bandwhich.enable = true;
@ -94,7 +101,7 @@ in
alias = {
lg1 = "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all";
lg2 = "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n'' %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all";
lg = ''!git lg1'';
lg = ''!"git lg1"'';
};
core = {
autocrlf = "input";
@ -105,9 +112,6 @@ in
pull = {
ff = "only";
};
push = {
autoSetupRemote = true;
};
};
};
@ -165,6 +169,7 @@ in
};
x-banananetwork = {
allCommon.enable = true;
@ -172,9 +177,12 @@ in
};
# TODO withlist:
# - update tmuxPlugins.sensible in nixpkgs (e.g. https://github.com/NixOS/nixpkgs/pull/272954)
};
}

@ -1,48 +1,55 @@
# applicable to all service VMs running on a hypervisor (currently Proxmox/QEMU assumed)
{
config,
lib,
pkgs,
...
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.x-banananetwork.vmCommon;
# Based on https://unix.stackexchange.com/questions/16578/resizable-serial-console-window
resize = pkgs.writeShellScriptBin "resize" ''
export PATH="${pkgs.coreutils}/bin"
if [ ! -t 0 ]; then
# not a interactive...
exit 0
fi
TTY="$(tty)"
if [[ "$TTY" != /dev/ttyS* ]] && [[ "$TTY" != /dev/ttyAMA* ]] && [[ "$TTY" != /dev/ttySIF* ]]; then
# probably not a known serial console, we could make this check more
# precise by using `setserial` but this would require some additional
# dependency
exit 0
fi
old=$(stty -g)
stty raw -echo min 0 time 5
printf '\0337\033[r\033[999;999H\033[6n\0338' > /dev/tty
IFS='[;R' read -r _ rows cols _ < /dev/tty
stty "$old"
stty cols "$cols" rows "$rows"
'';
in
{
options = {
x-banananetwork.vmCommon = {
enable = lib.mkEnableOption ''
settings for all my VMs
settings common to all hosts running in VMs
'';
userName = lib.mkOption {
description = ''
username of administrative user.
'';
type = lib.types.str;
example = "username";
};
hashedPassword = lib.mkOption {
description = ''
hash of password of adminstrative user.
This can e.g. be generated using mkpasswd.
'';
type = with lib.types; nullOr str;
default = null;
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
config = lib.mkIf cfg.enable (lib.mkMerge [
{
@ -64,17 +71,21 @@ in
nix.optimise = {
# should not take long because of auto-optimise-store
dates = lib.singleton "05:30";
dates = "05:30";
};
}
{
# all other options
boot = {
kernelParams = "console=ttyS0,115200";
loader = {
efi.canTouchEfiVariables = true;
grub.enable = false;
@ -88,20 +99,29 @@ in
};
console.keyMap = "de";
# for fast debugging of systems, keep small
environment.systemPackages = [
resize
];
networking = {
firewall = {
allowPing = lib.mkDefault true;
logRefusedConnections = lib.mkDefault false;
logRefusedConnections = false;
# TODO
};
useDHCP = lib.mkDefault true;
useDHCP = true;
useNetworkd = lib.mkDefault false;
usePredictableInterfaceNames = lib.mkDefault true;
usePredictableInterfaceNames = true;
};
nix = {
gc = {
@ -120,6 +140,7 @@ in
};
security = {
apparmor.enable = true;
@ -136,8 +157,12 @@ in
};
services = {
qemuGuest.enable = true;
openssh = {
enable = true;
authorizedKeysInHomedir = false;
@ -147,6 +172,10 @@ in
};
sound.enable = false;
system.autoUpgrade = {
enable = true;
allowReboot = true;
@ -156,24 +185,16 @@ in
"--no-use-registries"
"--no-update-lock-file"
];
flake = lib.mkDefault "git+https://git.bananet.work/banananetwork/server#${config.networking.fqdnOrHostName}"; # ===SYNC:general/meta/repo/url===
flake = lib.mkDefault "git+https://git.bananet.work/banananetwork/server"; #===SYNC:general/meta/repo/url===
operation = "boot"; # change only on reboots
};
users = {
mutableUsers = false;
users.${cfg.userName} = {
description = cfg.userName;
extraGroups = [
(lib.mkIf config.networking.networkmanager.enable "networkmanager")
"wheel"
];
inherit (cfg) hashedPassword;
isNormalUser = true;
openssh.authorizedKeys.keys = config.x-banananetwork.sshPublicKeys;
};
users.root.openssh.authorizedKeys.keys = config.x-banananetwork.sshPublicKeys;
};
systemd.services."serial-getty@".environment.TERM = "xterm-256color";
time.hardwareClockInLocalTime = false; # just to make sure
x-banananetwork = {
@ -184,6 +205,10 @@ in
};
# TODO disko config, see https://github.com/nix-community/disko/blob/master/docs/INDEX.md
# TODO wishlist items (in prio order):
# - ntfy.sh as mailer
# own script
@ -195,11 +220,13 @@ in
# - NixOS test: ssh-audit
# - networking.useNetworkd
# - networking.tcpcrypt
# environment.loginShellInit = "${lib.getExe resize}"; (see https://github.com/nix-community/srvos/blob/main/nixos/common/serial.nix)
# environment.loginShellInit = "${resize}/bin/resize"; (see https://github.com/nix-community/srvos/blob/main/nixos/common/serial.nix)
}
]
);
]);
}

@ -1,115 +0,0 @@
{
config,
lib,
options,
...
}:
let
self = options.x-banananetwork.vmDisko;
cfg = config.x-banananetwork.vmDisko;
in
{
# TODO upstream that to disko
options.x-banananetwork.vmDisko = {
enable = lib.mkEnableOption ''
VM disk configuration with disko.
Will be automatically enabled when option{x-banananetwork.vmDisko.generation} is manually set.
'';
mainDiskName = lib.mkOption {
description = ''
Name of the main disk.
Similar to {option}`system.stateVersion`,
**do not change this** unless you know what you are doing.
'';
type = lib.types.str;
};
generation = lib.mkOption {
description = ''
Disk generation to use.
Similar to {option}`system.stateVersion`,
**do not change this** unless you know what you are doing.
See option {option}`x-banananetwork.vmDisko.generations`
for a list of all generations available.
'';
type = with lib.types; nullOr str;
default = null;
example = self.recommendedGeneration.default;
};
recommendedGeneration = lib.mkOption {
description = ''
Disk generation recommended to use for new systems.
'';
default = "ext4-1";
readOnly = true;
};
generationsDir = lib.mkOption {
description = ''
Directories where to search for generations.
A generation must at least use one disk with the attr name `main`
(through its label name can be different)
because VMs are expected to have at least one disk as their primary available.
'';
type = lib.types.path;
default = ./.;
example = ./.;
};
generationPath = lib.mkOption {
description = "Path to selected generation template.";
readOnly = true;
type = lib.types.path;
default =
let
path = cfg.generationsDir + "/${cfg.generation}";
in
if builtins.pathExists path then path else path + ".nix";
defaultText = lib.literalExpression ''
with config.x-banananetwork.vmDisko;
generationsDir + ("/''${generation}" or "/''${generation}.nix")
'';
};
};
config = lib.mkMerge [
(lib.mkIf (cfg.generation != null) { x-banananetwork.vmDisko.enable = true; })
(lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.enable -> cfg.generation != null;
message = "x-banananetwork.vmDisko.generation must be set. Currently \"${cfg.recommendedGeneration}\" is recommended.";
}
{
assertion = cfg.generation != null -> builtins.pathExists cfg.generationPath;
message = "generation \"${cfg.generation}\" was not found in ${cfg.generationPath}";
}
];
disko.devices = lib.mkMerge [
(import cfg.generationPath)
{
# avoid mixing VM disks
# hint: mkOverride required because
# - cfg.generations from above is already type-checked, hence priorities are discharged
# - assigment from above has default priority of 100
#disk.main.name = lib.mkOverride 99 cfg.mainDiskName;
disk.main.name = cfg.mainDiskName;
}
];
})
];
}

@ -1,29 +0,0 @@
{
disk = {
main = {
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
type = "EF00";
size = "500M";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}

@ -1,105 +1,7 @@
{
inputs,
lib,
flake,
outputs,
...
}@flakeArg:
let
nixpkgs = inputs.nixpkgs;
nixosSystem =
{ modules, system }:
let
modsExtended = [
{
system.configurationRevision = toString (
flake.shortRev or flake.dirtyShortRev or flake.lastModified or "unknown"
);
}
outputs.nixosModules.myOptions
outputs.nixosModules.withDepends
{ home-manager.sharedModules = [ outputs.homeManagerModules.default ]; }
] ++ modules;
systemArgs = {
modules = modsExtended;
# be aware: specialArgs will break in my nixos integration tests
inherit system;
};
in
nixpkgs.lib.nixosSystem systemArgs
// {
# expose module cleanly
_banananetwork_systemArgs = systemArgs;
};
inherit (lib) importFlakeMod;
importSystem = path: nixosSystem (importFlakeMod path);
in
{
"x13yz" = nixosSystem {
modules = [
{
# TODO check if required & hide into modules
boot = {
initrd = {
availableKernelModules = [
"nvme" # nvme (probably required for booting)
"rtsx_pci_sdmmc" # probably for SD card (required for booting?)
"xhci_pci" # for USB 3.0 (required for booting?)
];
kernelModules = [
"dm-snapshot" # pseudo-required for LVM
];
};
kernelModules = [
"kvm-intel" # do not know if that is required here?
];
};
}
outputs.nixosProfiles.blade
inputs.nixos-hardware.nixosModules.lenovo-thinkpad-x13-yoga
{
# hardware
hardware.cpu.type = "intel";
hardware.graphics.intel.enable = true;
programs.captive-browser.interface = "wlp0s20f3";
x-banananetwork.frontend.convertable = true;
}
{
# as currently installed
boot.initrd.luks.devices."luks-herske.lvm.6nw.de" = {
device = "/dev/disk/by-uuid/16b8f83d-0450-4c4d-9964-788575a31eec";
preLVM = true;
allowDiscards = true;
};
fileSystems."/" = {
device = "/dev/disk/by-uuid/c93557db-e7c5-46ef-9cd8-87eb7c5753dc";
fsType = "ext4";
options = [ "relatime" ];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/5F9A-9A2D";
fsType = "vfat";
options = [
"uid=0"
"gid=0"
"fmask=0077"
"dmask=0077"
];
};
swapDevices = [ { device = "/dev/disk/by-uuid/8482463b-ceb3-40b3-abef-b49df2de88e5"; } ];
system.stateVersion = "24.05";
x-banananetwork.sshHostPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG71dtqG/c0AiFBN9OxoLD35TDQm3m8LXj/BQw60PE0h root@x13yz.pc.6nw.de 2024-07-01";
}
{
# host configuration
networking.domain = "pc.6nw.de";
networking.hostName = "x13yz";
services.fprintd.enable = true;
x-banananetwork.frontend.enable = true;
}
];
system = "x86_64-linux";
};
}@args: {
}

@ -1,14 +0,0 @@
# NixOS system profiles
In my case, those are to collect options common to a certain group of systems.
Their main goals & properties are:
- to make a system working for its intended platform / hypervisor
- also make it nice behaving (e.g. install optional agents)
- configuring stuff across the whole system
- do not introduce their own options
- do not introduce functionality which can be isolated
- each setup may import up to one profile
- but profiles can, if theyre compatible, import each other
Some of them are opioniated in some ways,
read their descriptions before using.

@ -1,68 +0,0 @@
# applicable to all systems running on bare hardware
{
config,
lib,
pkgs,
...
}:
{
imports = [
# from here
./common.nix
];
config = {
# EFI by default
boot.loader = {
efi.canTouchEfiVariables = lib.mkDefault true;
grub.memtest86.enable = lib.mkDefault true;
systemd-boot = {
enable = lib.mkDefault true;
editor = lib.mkDefault true;
memtest86.enable = lib.mkDefault true;
};
};
environment.systemPackages = with pkgs; [
pciutils
usbutils
];
hardware = {
cpu.updateMicrocode = lib.mkIf config.hardware.enableRedistributableFirmware true;
enableRedistributableFirmware = lib.mkDefault true;
};
powerManagement = {
cpuFreqGovernor = "ondemand";
enable = lib.mkDefault true;
};
services = {
fwupd = {
enable = true;
};
smartd = {
enable = true;
};
tlp = {
# 2024-08-14: tlp seems way better in my experience
# energy-saving daemon, similar to powertop --autotune, but adaptive to BAT / AC
enable = true;
};
};
x-banananetwork = {
# add docs & tools for emergencies
useable.enable = lib.mkDefault true;
};
};
}

@ -1,12 +0,0 @@
{ lib, ... }@flakeArg:
let
importProfile = path: import path;
importProfileMod = lib.importFlakeMod;
in
{
blade = importProfile ./blade.nix;
common = importProfile ./common.nix;
installer = importProfileMod ./installer.nix;
pveGuest = importProfile ./pveGuest.nix;
pveGuestHwSupport.nix = importProfile ./pveGuestHwSupport.nix;
}

@ -1,29 +0,0 @@
# applies to self-built installers, esp. auto installers
{ inputs, ... }@flakeArg:
{
config,
lib,
modulesPath,
...
}:
{
imports = [
# from nixpkgs
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" # includes allHardware configs
# from flake inputs
inputs.unattended-installer.nixosModules.default
# from here
./common.nix
./pveGuestHwSupport.nix
];
config = {
isoImage = {
isoBaseName = "nixos-${config.isoImage.edition}";
squashfsCompression = "zstd"; # more efficient
};
networking.domain = lib.mkDefault "temp.6nw.de"; # acceptable here because temporary
system.stateVersion = lib.versions.majorMinor config.system.nixos.version;
# installer does not necessarily need working SSH access & an extra user for that
};
}

@ -1,27 +0,0 @@
# makes for nice-behaving pve-guests
# extends pveGuestSupport by adding:
# - EFI booting
# ONLY for installed systems
{ lib, ... }:
{
imports = [
# from here
./common.nix
./pveGuestHwSupport.nix
];
config = {
# configure for EFI only
boot.loader = {
efi.canTouchEfiVariables = true;
grub.enable = lib.mkDefault false;
grub.efiSupport = true; # in case grub is preferred for some reason
systemd-boot.enable = lib.mkDefault true;
};
};
}

@ -1,71 +0,0 @@
# makes for nice-behaving pve-guests:
# - qemu-guest-agent & drivers
# - support for serial output (but graphic output should still work the same)
# works for installers as well (does NOT include common.nix)
{
lib,
modulesPath,
pkgs,
...
}:
let
# Based on https://unix.stackexchange.com/questions/16578/resizable-serial-console-window
resize = pkgs.writeShellScriptBin "resize" ''
export PATH="${lib.getBin pkgs.coreutils}/bin"
if [ ! -t 0 ]; then
# not a interactive...
exit 0
fi
TTY="$(tty)"
if [[ "$TTY" != /dev/ttyS* ]] && [[ "$TTY" != /dev/ttyAMA* ]] && [[ "$TTY" != /dev/ttySIF* ]]; then
# probably not a known serial console, we could make this check more
# precise by using `setserial` but this would require some additional
# dependency
exit 0
fi
old=$(stty -g)
stty raw -echo min 0 time 5
printf '\0337\033[r\033[999;999H\033[6n\0338' > /dev/tty
IFS='[;R' read -r _ rows cols _ < /dev/tty
stty "$old"
stty cols "$cols" rows "$rows"
'';
in
{
imports = [
# from nixpkgs
"${modulesPath}/profiles/qemu-guest.nix"
];
config = {
boot = {
# TODO probably until https://github.com/NixOS/nixpkgs/issues/340086
initrd.availableKernelModules = lib.singleton "virtio_iommu";
kernelParams = [
# show kernel log on serial
"console=ttyS0,115200"
# but use virtual tty as /dev/console (last entry)
"console=tty0"
];
};
environment.systemPackages = [ resize ];
services = {
qemuGuest.enable = true;
};
systemd.services."serial-getty@".environment.TERM = "xterm-256color";
time.hardwareClockInLocalTime = false; # just to make sure
};
}

@ -1,20 +0,0 @@
{ lib, ... }@flakeArg:
{ pkgs_unstable, ... }@systemArg:
final: prev:
let
list = [
# TODO until 24.11
"nixfmt-rfc-style"
"wcurl"
];
backport =
pkgAttrName:
let
alreadyStable = builtins.hasAttr pkgAttrName prev;
stableSource = lib.warn "consider removing ${pkgAttrName} from backports list as it is now available on stable" prev;
source = if alreadyStable then stableSource else pkgs_unstable;
pkg = builtins.getAttr pkgAttrName source;
in
pkg;
in
lib.genAttrs list backport

@ -1,18 +0,0 @@
{ lib, ... }@flakeArg:
let
inherit (lib) systemSpecificVars;
rawImport = path: import path flakeArg;
wrapOverlay =
overlay: final: prev:
overlay (systemSpecificVars prev.system) final prev;
importOverlay = path: wrapOverlay (rawImport path);
in
{
backports = importOverlay ./backports.nix;
fromFlake = importOverlay ./fromFlake.nix;
systemd-radv-fadeout = importOverlay ./systemd-radv-fadeout;
}

@ -1,9 +0,0 @@
{ outputs, ... }@flakeArg:
{ ... }@systemArg:
final: prev: {
inherit (outputs.packages.${prev.system})
# list all universally compatible packages from ./../packages
librespot-auth
nft-update-addresses
;
}

@ -1,8 +0,0 @@
# TODO until https://github.com/systemd/systemd/issues/29651 is fixed
{ outputs, ... }@flakeArg:
{ ... }@systemArg:
final: prev: {
systemd = prev.systemd.overrideAttrs (old: {
patches = old.patches ++ [ ./patch.patch ];
});
}

@ -1,55 +0,0 @@
commit b09851c2be354e592802fe9209b4cd6150bd818d
Author: Felix Stupp <felix.stupp@banananet.work>
Date: Tue Sep 3 20:33:03 2024 +0000
network/radv: fade out announced prefixes rather than just deleting them
Fixes https://github.com/systemd/systemd/issues/29651
diff --git a/src/libsystemd-network/sd-radv.c b/src/libsystemd-network/sd-radv.c
index c384d4e627..537203c13b 100644
--- a/src/libsystemd-network/sd-radv.c
+++ b/src/libsystemd-network/sd-radv.c
@@ -679,6 +679,8 @@ void sd_radv_remove_prefix(
if (!prefix)
return;
+ const char *addr_p = IN6_ADDR_PREFIX_TO_STRING(prefix, prefixlen);
+
LIST_FOREACH(prefix, cur, ra->prefixes) {
if (prefixlen != cur->opt.prefixlen)
continue;
@@ -686,9 +688,32 @@ void sd_radv_remove_prefix(
if (!in6_addr_equal(prefix, &cur->opt.in6_addr))
continue;
+ /* "Fade out" IPv6 prefix, i.e. informing clients about its sudden invalidity.
+ * Realized by announcing the prefix with preferred=0 & valid=2h.
+ * This makes clients rotating to a newer prefix if some is already defined.
+ */
+
+ // TODO is copying required here? (& does it do anything at all?)
+ sd_radv_prefix *p = cur;
+
+ // TODO replace hacky way to get current time
+ uint64_t current_time = cur->valid_until - cur->lifetime_valid_usec;
+ // TODO replace constant with setting (valid lifetime) ?
+ uint64_t two_hours_usec = (uint64_t) 2 * 60 * 60 * 1000000;
+ sd_radv_prefix_set_preferred_lifetime(p, 0, current_time);
+ sd_radv_prefix_set_valid_lifetime(p, two_hours_usec, current_time + two_hours_usec);
+
+ // TODO is full replacement procedure required or can we just edit the stored prefix without this?
+ // procedure cloned from sd_radv_add_prefix, if found. I do not call this here because of the duplicated search in the list & because of the different logging message
+ sd_radv_prefix_ref(p);
LIST_REMOVE(prefix, ra->prefixes, cur);
- ra->n_prefixes--;
sd_radv_prefix_unref(cur);
+ LIST_APPEND(prefix, ra->prefixes, p);
+
+ log_radv(ra, "Fade out IPv6 prefix %s (preferred: %s, valid: %s)",
+ addr_p,
+ FORMAT_TIMESPAN(p->lifetime_preferred_usec, USEC_PER_SEC),
+ FORMAT_TIMESPAN(p->lifetime_valid_usec, USEC_PER_SEC));
return;
}
}

@ -1,65 +0,0 @@
{
inputs,
lib,
outputs,
...
}@flakeArg:
{ pkgs, system, ... }@sysArg:
let
craneLib = inputs.crane.mkLib pkgs;
in
{
librespot-auth = pkgs.callPackage ./librespot-auth { inherit craneLib; };
nft-update-addresses = pkgs.callPackage ./nft-update-addresses { };
secrix-wrapper =
let
secrixExe = outputs.apps.${system}.secrix.program;
in
pkgs.writeShellApplication {
name = "secr";
text = ''
secrix() {
set -x
exec ${secrixExe} "$@"
}
help() {
echo "Usages:"
echo " $0 [create|rekey|edit|encrypt] <system> [<args> ] <file>"
echo " $0 decrypt [<args> ] <file>"
}
main() {
if [[ $# -lt 1 ]]; then
help
exit 0
fi
cmd="$1"
shift 1
case "$cmd" in
help|-h|--help)
help
;;
create)
secrix "$cmd" --all-users --system "$@"
;;
rekey|edit)
secrix "$cmd" --identity "$SECRIX_ID" --all-users --system "$@"
;;
encrypt)
secrix "$cmd" --all-users --system "$@"
;;
decrypt)
secrix "$cmd" --identity "$SECRIX_ID" "$@"
;;
esac
}
main "$@"
'';
};
}

@ -1,35 +0,0 @@
{
stdenv,
lib,
fetchFromGitHub,
# from flake
craneLib,
}:
# TODO temporary useful due to https://github.com/hrkfdn/ncspot/issues/1500
craneLib.buildPackage rec {
pname = "librespot-auth";
version = "0.1.1";
src = fetchFromGitHub {
owner = "dspearson";
repo = "librespot-auth";
rev = "v${version}";
hash = "sha256-IbbArRSKpnljhZSgL0b3EjVzKWN7bk6t0Bv7TkYr8FI=";
};
buildInputs = [ ];
enableParallelBuilding = true;
# Ensure Rust compiles for the right target
env.CARGO_BUILD_TARGET = stdenv.hostPlatform.rust.rustcTarget;
meta = with lib; {
description = "A simple program for populating a `credentials.json` for librespot via Spotify's zeroconf authentication.";
homepage = "https://github.com/dspearson/librespot-auth";
changelog = "https://github.com/dspearson/librespot-auth/releases/tag/v${version}";
license = licenses.isc;
mainProgram = "librespot-auth";
};
}

@ -1,110 +0,0 @@
{
lib,
writeText,
python3Packages,
iproute2,
mypy,
nftables,
}:
let
version = "2024.09.04";
project_toml = writeText "nft-update-addresses_pyproject" ''
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "nft-update-addresses"
version = ${lib.escapeShellArg version}
requires-python = ">= 3.11"
[project.scripts]
nft-update-addresses = "nft_update_addresses:main"
'';
in
python3Packages.buildPythonPackage {
name = "nft-update-addresses";
inherit version;
format = "pyproject";
build-system = lib.singleton python3Packages.setuptools;
dependencies = with python3Packages; [
attrs
setuptools
systemd
];
propagatedBuildInputs = [
iproute2
nftables
];
unpackPhase = ''
mkdir -p ./src/nft_update_addresses
cp ${project_toml} ./pyproject.toml
cp ${./nft-update-addresses.py} ./src/nft_update_addresses/__init__.py
${lib.getExe mypy} --strict ./src
'';
meta = {
description = "Auto-updates nftables sets to reflect dynamic IPs / nets used on dynamic setups, including SLAAC addresses of clients";
longDescription = ''
> (TL;DR in catchy:) This service will solve most of your problems in dynamic IP setups!!
This service is designed for networking setups without static IPs
and with IP rotations at runtime in mind.
It can be helpful when configuring a router based on NixOS or any other Linux distribution.
With this service, I already implemented a rather simple NixOS router setup
working with dynamically assigned IPs & prefixes via DHCP & DHCPv6 prefix delegation.
You can also find this router project in [my flake](https://git.banananet.work/banananetwork/server).
With this router setup, I also throughly test every change to this script before publishing
as these changes will also be automatically pushed to my server setup as well.
So while I cannot give any gurantee, this service should be fairly stable.
For each interface defined in its config file,
it continuously monitors for IP changes and reflects them into the sets
with `{ifname}v{ipVersion}` as their names prefix.
Most sets & maps should work flawless in multi IP/prefix setups as well.
Available sets & maps are:
- `{prefix}addr`: IP addresses of the host itself
- excluding link-locals
- including IPv4 private networks
- excluding IPv6 unique link local (to identify requests to public routable addresses)
- example use: `iifname != lan0 ip daddr @lan0v4addr drop`
- `{prefix}net`: IP networks of the hosts IP addresses
- excluding link-locals
- including IPv4 private networks
- including IPv6 unique link local (to identify target networks in forwarding rules)
- example use: `iifname lan0 ip saddr @lan0v6net accept`
- `{prefix}dnat{protocol}`: map of ports to IPs with destination port which might be DNATed to (v6 only)
- `protocol` means all OSI layer 4 protocols with ports supported by nftables
(currently `dccp`, `sctp`, `tcp, `udp`, `udplite`)
- example use: `dnat ip6 to tcp dport map @lan0v6dnattcp`
- `{prefix}exp{protocol}`: set of IPs with destination ports which might be exposed (v6 only)
- `protocol` means the same as for `dnat` map
- example use: `ip6 daddr . tcp dport @lan0v6exptcp accept`
- `{prefix}_{mac}`: modified EUI64 SLAAC address for that MAC using the prefix used by the host itself (v6 only)
- will only be created for MAC addresses listed in the config file
- WARNING: not stable on multi-prefix setups, fluctuates based on the latest update
- example use: `ip6 saddr @lan0v6_aabbccddeeff tcp dport 53 reject comment "no dns for this host"`
There is also a NixOS module available easing the configuration: {option}`services.nft-update-addresses`.
Looking into the Nix files can also be helpful for non NixOS setups
as they provide an example for a sandboxed systemd service implementation.
If you can & want to automate the creation of said sets & maps,
the script provides a CLI flag `--output-set-definitions`
with the definitions of all sets & maps supplied by the service when executed with the same config.
Importing these definitions before starting the service is required,
but also enables you to load rules using these before the service is fully operational
(especially perfect for NixOS setups).
This service is built crash anytime it experiences a probably fatal error
(i.e. unparsable IP updates or failing to populate nftables).
Therefore, a systemd service setup with aggressive restart policies (already included in my module)
and a monitoring of said systemd service are advisable.
'';
mainProgram = "nft-update-addresses";
};
}

@ -1,933 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
from abc import (
ABC,
abstractmethod,
)
import argparse
from collections import defaultdict
from collections.abc import (
Mapping,
Sequence,
)
from datetime import (
datetime,
timedelta,
)
from enum import (
Enum,
Flag,
auto,
)
from functools import cached_property
import io
from ipaddress import (
IPv4Interface,
IPv6Interface,
IPv6Network,
)
from itertools import chain
import json
import logging
from logging.handlers import SysLogHandler
import os
from pathlib import Path
import re
import shlex
from signal import SIGHUP, signal
from string import Template
import subprocess
import threading
from threading import (
RLock,
Timer,
)
import traceback
from typing import (
Any,
Iterable,
Literal,
NewType,
NoReturn,
Protocol,
TypeAlias,
TypeGuard,
TypeVar,
Union,
cast,
)
from attrs import (
define,
field,
)
from systemd import daemon # type: ignore[import-untyped]
from systemd.journal import JournalHandler # type: ignore[import-untyped]
logger = logging.getLogger(__name__)
def raise_and_exit(args: Any) -> None:
Timer(0.01, os._exit, args=(1,)).start()
logger.error(repr(args.exc_value))
logger.error(
"\n".join(traceback.format_tb(args.exc_traceback))
if args.exc_traceback != None
else "traceback from thread got lost!"
)
raise args.exc_value or Exception(f"{args.exc_type} (exception details got lost)")
# ensure exceptions in any thread brings the program down
# important for proper error detection via tests & in random cases in real world
threading.excepthook = raise_and_exit
JsonVal: TypeAlias = Union["JsonObj", "JsonList", str, int, bool]
JsonList: TypeAlias = Sequence[JsonVal]
JsonObj: TypeAlias = Mapping[str, JsonVal]
T = TypeVar("T", contravariant=True)
MACAddress = NewType("MACAddress", str)
"""format: aabbccddeeff (lower-case, without separators)"""
IPInterface: TypeAlias = IPv4Interface | IPv6Interface
NftProtocol = NewType("NftProtocol", str) # e.g. tcp, udp, …
Port = NewType("Port", int)
IfName = NewType("IfName", str)
NftTable = NewType("NftTable", str)
def to_mac(mac_str: str) -> MACAddress:
eui48 = re.sub(r"[.:_-]", "", mac_str.lower())
if not is_mac(eui48):
raise ValueError(f"invalid MAC address / EUI48: {mac_str}")
return MACAddress(eui48)
def is_mac(mac_str: str) -> TypeGuard[MACAddress]:
return re.match(r"^[0-9a-f]{12}$", mac_str) != None
def to_port(port_str: str | int) -> Port:
try:
port = int(port_str)
except ValueError as exc:
raise ValueError(f"invalid port number: {port_str}") from exc
if not is_port(port):
raise ValueError(f"invalid port number: {port_str}")
return Port(port)
def is_port(port: int) -> TypeGuard[Port]:
return 0 < port < 65536
def slaac_eui48(prefix: IPv6Network, eui48: MACAddress) -> IPv6Interface:
if prefix.prefixlen > 64:
raise ValueError(
f"a SLAAC IPv6 address requires a prefix with CIDR of at least /64, got {prefix}"
)
eui64 = eui48[0:6] + "fffe" + eui48[6:]
modified = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
euil = int(modified, 16)
return IPv6Interface(f"{prefix[euil].compressed}/{prefix.prefixlen}")
class UpdateHandler(Protocol[T]):
def update(self, data: T) -> None:
...
def update_stack(self, data: Sequence[T]) -> None:
...
class UpdateStackHandler(UpdateHandler[T], ABC):
def update(self, data: T) -> None:
return self._update_stack((data,))
def update_stack(self, data: Sequence[T]) -> None:
if len(data) <= 0:
logger.warning(
f"[bug, please report upstream] received empty data in update_stack. Traceback:\n{''.join(traceback.format_stack())}"
)
return
return self._update_stack(data)
@abstractmethod
def _update_stack(self, data: Sequence[T]) -> None:
...
class IgnoreHandler(UpdateStackHandler[object]):
def _update_stack(self, data: Sequence[object]) -> None:
return
@define(
kw_only=True,
slots=False,
)
class UpdateBurstHandler(UpdateStackHandler[T]):
burst_interval: float
handler: Sequence[UpdateHandler[T]]
__lock: RLock = field(factory=RLock)
__updates: list[T] = field(factory=list)
__timer: Timer | None = None
def _update_stack(self, data: Sequence[T]) -> None:
with self.__lock:
self.__updates.extend(data)
self.__refresh_timer()
def __refresh_timer(self) -> None:
with self.__lock:
if self.__timer is not None:
# try to cancel
# not a problem if timer already elapsed but before processing really started
# because due to using locks when accessing updates
self.__timer.cancel()
self.__timer = Timer(
interval=self.burst_interval,
function=self.__process_updates,
)
self.__timer.start()
def __process_updates(self) -> None:
with self.__lock:
self.__timer = None
if not self.__updates:
return
updates = self.__updates
self.__updates = []
for handler in self.handler:
handler.update_stack(updates)
class IpFlag(Flag):
dynamic = auto()
mngtmpaddr = auto()
noprefixroute = auto()
temporary = auto()
tentiative = auto()
@staticmethod
def parse_str(flags_str: Sequence[str], ignore_unknown: bool = True) -> IpFlag:
flags = IpFlag(0)
for flag in flags_str:
flag = flag.lower()
member = IpFlag.__members__.get(flag)
if member is not None:
flags |= member
elif not ignore_unknown:
raise Exception(f"Unrecognized IpFlag: {flag}")
return flags
IPv6_ULA_NET = IPv6Network("fc00::/7") # because ip.is_private is wrong
# parses output of "ip -o address" / "ip -o monitor address"
IP_MON_PATTERN = re.compile(
r"""(?x)^
(?P<deleted>[Dd]eleted\s+)?
(?P<ifindex>\d+):\s+
(?P<ifname>\S+)\s+
(?P<type>inet6?)\s+
(?P<ip>\S+)\s+
#(?:metric\s+\S+\s+)? # sometimes possible
#(?:brd\s+\S+\s+)? # broadcast IP on inet
(?:\S+\s+\S+\s+)* # abstracted irrelevant attributes
(?:scope\s+(?P<scope>\S+)\s+)
(?P<flags>(?:(\S+)\s)*) # (single spaces required for parser below to work correctly)
(?:\S+)? # random interface name repetition on inet
[\\]\s+
valid_lft\s+(
(?P<valid_lft_sec>\d+)sec
|
(?P<valid_lft_forever>forever)
)
\s+
preferred_lft\s+(
(?P<preferred_lft_sec>\d+)sec
|
(?P<preferred_lft_forever>forever)
)
\s*
$"""
)
class SpecialIpUpdate(Enum):
FLUSH_RULES = auto()
@define(
frozen=True,
kw_only=True,
)
class IpAddressUpdate:
deleted: bool
ifindex: int
ifname: IfName
ip: IPInterface
scope: str
flags: IpFlag
valid_until: datetime
preferred_until: datetime
@classmethod
def parse_line(cls, line: str) -> IpAddressUpdate:
m = IP_MON_PATTERN.search(line)
if not m:
raise Exception(f"Could not parse ip monitor output: {line!r}")
grp = m.groupdict()
ip_type: type[IPInterface] = (
IPv6Interface if grp["type"] == "inet6" else IPv4Interface
)
try:
ip = ip_type(grp["ip"])
except ValueError as e:
raise Exception(
f"Could not parse ip monitor output, invalid IP: {grp['ip']!r}"
) from e
flags = IpFlag.parse_str(grp["flags"].strip().split(" "))
return IpAddressUpdate(
deleted=grp["deleted"] != None,
ifindex=int(grp["ifindex"]),
ifname=IfName(grp["ifname"]),
ip=ip,
scope=grp["scope"],
flags=flags,
valid_until=cls.__parse_lifetime(grp, "valid_lft"),
preferred_until=cls.__parse_lifetime(grp, "preferred_lft"),
)
@staticmethod
def __parse_lifetime(grp: Mapping[str, str | None], name: str) -> datetime:
if grp[f"{name}_forever"] != None:
return datetime.now() + timedelta(days=30)
sec = grp[f"{name}_sec"]
if sec is None:
raise ValueError(
"IP address update parse error: expected regex group for seconds != None (bug in code)"
)
return datetime.now() + timedelta(seconds=int(sec))
def kickoff_ip(
ip_cmd: list[str],
handler: UpdateHandler[IpAddressUpdate],
) -> None:
res = subprocess.run(
ip_cmd + ["-o", "address", "show"],
check=True,
stdout=subprocess.PIPE,
text=True,
)
for line in res.stdout.splitlines(keepends=False):
line = line.rstrip()
if line == "":
continue
update = IpAddressUpdate.parse_line(line)
logger.debug(f"pass IP update: {update!r}")
handler.update(update)
def monitor_ip(
ip_cmd: list[str],
handler: UpdateHandler[IpAddressUpdate],
) -> NoReturn:
proc = subprocess.Popen(
ip_cmd + ["-o", "monitor", "address"],
stdout=subprocess.PIPE,
text=True,
)
# initial kickoff (AFTER starting monitoring, to not miss any update)
logger.info("kickoff IP monitoring with current data")
kickoff_ip(ip_cmd, handler)
logger.info("start regular monitoring")
while True:
rc = proc.poll()
if rc != None:
# flush stdout for easier debugging
logger.error("Last stdout of monitor process:")
logger.error(proc.stdout.read()) # type: ignore[union-attr]
raise Exception(f"Monitor process crashed with returncode {rc}")
line = proc.stdout.readline().rstrip() # type: ignore[union-attr]
if not line:
continue
logger.info("IP change detected")
update = IpAddressUpdate.parse_line(line)
logger.debug(f"pass IP update: {update!r}")
handler.update(update)
class InterfaceUpdateHandler(UpdateStackHandler[IpAddressUpdate | SpecialIpUpdate]):
# TODO regularly check (i.e. 1 hour) if stored lists are still correct
slaac_prefix: IPv6Interface | None
def __init__(
self,
config: InterfaceConfig,
nft_handler: UpdateHandler[NftUpdate],
) -> None:
self.nft_handler = nft_handler
self.lock = RLock()
self.config = config
self.addrs = dict[IPInterface, IpAddressUpdate]()
self.slaac_prefix = None
def _update_stack(self, data: Sequence[IpAddressUpdate | SpecialIpUpdate]) -> None:
nft_updates = tuple(
chain.from_iterable(self.__parse_update(single) for single in data)
)
if len(nft_updates) <= 0:
return
self.nft_handler.update_stack(nft_updates)
def __parse_update(
self, data: IpAddressUpdate | SpecialIpUpdate
) -> Iterable[NftUpdate]:
if isinstance(data, SpecialIpUpdate):
if data is not SpecialIpUpdate.FLUSH_RULES:
raise ValueError(f"unknown special update {data!r}")
# TODO maybe flush all sets completely, for good measure
for addr in self.addrs.keys():
yield from self.__update_network_sets(addr, deleted=True)
self.addrs = dict()
yield from self.__empty_slaac_sets()
self.slaac_prefix = None
return
if data.ifname != self.config.ifname:
return
if data.ip.is_link_local:
logger.debug(
f"{self.config.ifname}: ignore change for IP {data.ip} because link-local"
)
return
if IpFlag.temporary in data.flags:
logger.debug(
f"{self.config.ifname}: ignore change for IP {data.ip} because temporary"
)
return # ignore IPv6 privacy extension addresses
if IpFlag.tentiative in data.flags:
logger.debug(
f"{self.config.ifname}: ignore change for IP {data.ip} because tentiative"
)
return # ignore (yet) tentiative addresses
logger.debug(f"{self.config.ifname}: process change of IP {data.ip}")
with self.lock:
stored = data.ip in self.addrs
changed = stored != (not data.deleted)
if data.deleted:
if not changed:
return # no updates required
logger.info(f"{self.config.ifname}: deleted IP {data.ip}")
del self.addrs[data.ip]
else:
if not stored:
logger.info(f"{self.config.ifname}: discovered IP {data.ip}")
self.addrs[data.ip] = data # keep entry up to date
if changed:
yield from self.__update_network_sets(data.ip, data.deleted)
# even if "not changed", still check SLAAC rules because of lifetimes
slaac_prefix = self.__select_slaac_prefix()
if self.slaac_prefix == slaac_prefix:
return # no SLAAC updates required
self.slaac_prefix = slaac_prefix
logger.info(f"{self.config.ifname}: change main SLAAC prefix to {slaac_prefix}")
yield from (
self.__empty_slaac_sets()
if slaac_prefix is None
else self.__update_slaac_sets(slaac_prefix)
)
def __update_network_sets(
self,
ip: IPInterface,
deleted: bool = False,
) -> Iterable[NftUpdate]:
set_prefix = f"{self.config.ifname}v{ip.version}"
op = NftValueOperation.if_deleted(deleted)
yield NftUpdate(
obj_type="set",
obj_name=f"all_ipv{ip.version}net",
operation=op,
values=(f"{self.config.ifname} . {ip.network.compressed}",),
)
yield NftUpdate(
obj_type="set",
obj_name=f"{set_prefix}net",
operation=op,
values=(ip.network.compressed,),
)
yield NftUpdate(
obj_type="set",
obj_name=f"all_ipv{ip.version}addr",
operation=op,
values=(f"{self.config.ifname} . {ip.ip.compressed}",),
)
yield NftUpdate(
obj_type="set",
obj_name=f"{set_prefix}addr",
operation=op,
values=(ip.ip.compressed,),
)
def __update_slaac_sets(self, ip: IPv6Interface) -> Iterable[NftUpdate]:
set_prefix = f"{self.config.ifname}v6"
op = NftValueOperation.REPLACE
slaacs = {mac: slaac_eui48(ip.network, mac) for mac in self.config.macs}
for mac in self.config.macs:
yield NftUpdate(
obj_type="set",
obj_name=f"{set_prefix}_{mac}",
operation=op,
values=(slaacs[mac].ip.compressed,),
)
slaacs_sub = {
f"ipv6_{self.config.ifname}_{mac}": addr.ip.compressed
for mac, addr in slaacs.items()
}
for one_set in self.config.sets:
yield NftUpdate(
obj_type=one_set.set_type,
obj_name=one_set.name,
operation=op,
values=tuple(one_set.sub_elements(slaacs_sub)),
)
def __empty_slaac_sets(self) -> Iterable[NftUpdate]:
set_prefix = f"{self.config.ifname}v6"
op = NftValueOperation.EMPTY
for mac in self.config.macs:
yield NftUpdate(
obj_type="set",
obj_name=f"{set_prefix}_{mac}",
operation=op,
values=tuple(),
)
for one_set in self.config.sets:
yield NftUpdate(
obj_type=one_set.set_type,
obj_name=one_set.name,
operation=op,
values=tuple(),
)
def __select_slaac_prefix(self) -> IPv6Interface | None:
now = datetime.now()
valid = tuple(data for data in self.addrs.values() if data.ip.version == 6)
if len(valid) <= 0:
return None
selected = max(
valid,
key=lambda data: (
# prefer valid
1 if now < data.valid_until else 0,
# prefer global unicast addresses
1 if data.ip not in IPv6_ULA_NET else 0,
# if preferred, take longest preferred
max(now, data.preferred_until),
# otherwise longest valid
data.valid_until,
),
)
return cast(IPv6Interface, selected.ip)
def gen_set_definitions(self) -> str:
output = []
for ip_v in [4, 6]:
addr_type = f"ipv{ip_v}_addr"
set_prefix = f"{self.config.ifname}v{ip_v}"
output.append(gen_set_def("set", f"{set_prefix}addr", addr_type))
output.append(gen_set_def("set", f"{set_prefix}net", addr_type, "interval"))
if ip_v != 6:
continue
for mac in self.config.macs:
output.append(gen_set_def("set", f"{set_prefix}_{mac}", addr_type))
output.extend(s.definition for s in self.config.sets)
return "\n".join(output)
def gen_set_def(
set_type: str,
name: str,
data_type: str,
flags: str | None = None,
elements: Sequence[str] = tuple(),
) -> str:
return "\n".join(
line
for line in (
f"{set_type} {name} " + "{",
f" type {data_type}",
f" flags {flags}" if flags is not None else None,
" elements = { " + ", ".join(elements) + " }"
if len(elements) > 0
else None,
"}",
)
if line is not None
)
pass
class NftValueOperation(Enum):
ADD = auto()
DELETE = auto()
REPLACE = auto()
EMPTY = auto()
@staticmethod
def if_deleted(b: bool) -> NftValueOperation:
return NftValueOperation.DELETE if b else NftValueOperation.ADD
@staticmethod
def if_emptied(b: bool) -> NftValueOperation:
return NftValueOperation.EMPTY if b else NftValueOperation.REPLACE
@property
def set_operation(self) -> str:
assert self.passes_values
return "destroy" if self == NftValueOperation.DELETE else "add"
@property
def passes_values(self) -> bool:
return self in {
NftValueOperation.ADD,
NftValueOperation.REPLACE,
NftValueOperation.DELETE,
}
@property
def flushes_values(self) -> bool:
return self in {
NftValueOperation.REPLACE,
NftValueOperation.EMPTY,
}
@define(
frozen=True,
kw_only=True,
)
class NftUpdate:
obj_type: str
obj_name: str
operation: NftValueOperation
values: Sequence[str]
def to_script(self, table: NftTable) -> str:
lines = []
# inet family is the only which supports shared IPv4 & IPv6 entries
obj_id = f"inet {table} {self.obj_name}"
if self.operation.flushes_values:
lines.append(f"flush {self.obj_type} {obj_id}")
if self.operation.passes_values and len(self.values) > 0:
op_str = self.operation.set_operation
values_str = ", ".join(self.values)
lines.append(f"{op_str} element {obj_id} {{ {values_str} }}")
return "\n".join(lines)
class NftUpdateHandler(UpdateStackHandler[NftUpdate]):
def __init__(
self,
update_cmd: Sequence[str],
table: NftTable,
handler: UpdateHandler[None],
) -> None:
self.update_cmd = update_cmd
self.table = table
self.handler = handler
def _update_stack(self, data: Sequence[NftUpdate]) -> None:
logger.debug("compile stacked updates for nftables")
script = "\n".join(
map(
lambda u: u.to_script(table=self.table),
data,
)
)
logger.debug(f"pass updates to nftables:\n{script}")
subprocess.run(
list(self.update_cmd) + ["-f", "-"],
input=script,
check=True,
text=True,
)
self.handler.update(None)
class SystemdHandler(UpdateHandler[object]):
def update(self, data: object) -> None:
# TODO improve status updates
daemon.notify("READY=1\nSTATUS=operating …\n")
def update_stack(self, data: Sequence[object]) -> None:
self.update(None)
@define(
frozen=True,
kw_only=True,
)
class SetConfig:
ifname: str
set_type: str
name: str
data_type: str
flags: str | None
elements: Sequence[Template] = field()
@elements.validator
def __elem_validate(self, attribute: str, value: Sequence[Template]) -> None:
regex = self.__supported_vars
for temp in self.elements:
for var in temp.get_identifiers():
m = regex.search(var)
if m is None:
raise ValueError(
f"set {self.name!r} for if {self.ifname!r} uses invalid template variable {var!r}"
)
@property
def __supported_vars(self) -> re.Pattern[str]:
return re.compile(rf"^ipv6_{re.escape(self.ifname)}_(?P<mac>[0-9a-f]{{12}})$")
@property
def embedded_macs(self) -> Iterable[MACAddress]:
regex = self.__supported_vars
for temp in self.elements:
for var in temp.get_identifiers():
m = regex.search(var)
assert m != None
yield to_mac(m.group("mac")) # type: ignore[union-attr]
@property
def definition(self) -> str:
return gen_set_def(
set_type=self.set_type,
name=self.name,
data_type=self.data_type,
flags=self.flags,
# non matching rules at the beginning (in static part)
# to verify that all supplied patterns are correct
# undefined address should be safest to use here, because:
# - as src, it is valid, but if one can spoof this one, it can spoof other addresses (and routers should have simple anti-spoof mechanisms in place)
# - as dest, it is invalid
# - as NAT target, it is invalid
elements=self.sub_elements(defaultdict(lambda: "::")),
)
def sub_elements(self, substitutions: Mapping[str, str]) -> Sequence[str]:
return tuple(elem.substitute(substitutions) for elem in self.elements)
@classmethod
def from_json(cls, *, ifname: str, name: str, obj: JsonObj) -> SetConfig:
assert set(obj.keys()) <= set(("set_type", "name", "type", "flags", "elements"))
set_type = obj["set_type"]
assert isinstance(set_type, str)
data_type = obj["type"]
assert isinstance(data_type, str)
flags = obj.get("flags")
assert flags is None or isinstance(flags, str)
elements = obj["elements"]
assert isinstance(elements, Sequence) and all(
isinstance(elem, str) for elem in elements
)
templates = tuple(map(lambda s: Template(cast(str, s)), elements))
return SetConfig(
set_type=set_type,
ifname=ifname,
name=name,
data_type=data_type,
flags=flags,
elements=templates,
)
@define(
frozen=True,
kw_only=True,
)
class InterfaceConfig:
ifname: IfName
macs_direct: Sequence[MACAddress]
sets: Sequence[SetConfig]
@cached_property
def macs(self) -> Sequence[MACAddress]:
return tuple(
set(
chain(
self.macs_direct,
(mac for one_set in self.sets for mac in one_set.embedded_macs),
)
)
)
@staticmethod
def from_json(ifname: str, obj: JsonObj) -> InterfaceConfig:
assert set(obj.keys()) <= set(("macs", "sets"))
macs = obj.get("macs")
assert macs is None or isinstance(macs, Sequence)
sets = obj.get("sets")
assert sets is None or isinstance(sets, Mapping)
return InterfaceConfig(
ifname=IfName(ifname),
macs_direct=tuple()
if macs is None
else tuple(to_mac(cast(str, mac)) for mac in macs),
sets=tuple()
if sets is None
else tuple(
SetConfig.from_json(
ifname=ifname, name=name, obj=cast(JsonObj, one_set)
)
for name, one_set in sets.items()
),
)
@define(
frozen=True,
kw_only=True,
)
class AppConfig:
nft_table: NftTable
interfaces: Sequence[InterfaceConfig]
@staticmethod
def from_json(obj: JsonObj) -> AppConfig:
assert set(obj.keys()) <= set(("interfaces", "nftTable"))
nft_table = obj["nftTable"]
assert isinstance(nft_table, str)
interfaces = obj["interfaces"]
assert isinstance(interfaces, Mapping)
return AppConfig(
nft_table=NftTable(nft_table),
interfaces=tuple(
InterfaceConfig.from_json(ifname, cast(JsonObj, if_cfg))
for ifname, if_cfg in interfaces.items()
),
)
def read_config_file(path: Path) -> AppConfig:
with path.open("r") as fh:
json_data = json.load(fh)
logger.debug(repr(json_data))
return AppConfig.from_json(json_data)
LOG_LEVEL_MAP = {
"critical": logging.CRITICAL,
"error": logging.ERROR,
"warning": logging.WARNING,
"info": logging.INFO,
"debug": logging.DEBUG,
}
def _gen_if_updater(
configs: Sequence[InterfaceConfig], nft_updater: UpdateHandler[NftUpdate]
) -> Sequence[InterfaceUpdateHandler]:
return tuple(
InterfaceUpdateHandler(
config=if_cfg,
nft_handler=nft_updater,
)
for if_cfg in configs
)
def static_part_generation(config: AppConfig) -> None:
for ipV in [4, 6]:
print(gen_set_def("set", f"all_ipv{ipV}addr", f"ifname . ipv{ipV}_addr"))
print(
gen_set_def(
"set", f"all_ipv{ipV}net", f"ifname . ipv{ipV}_addr", "interval"
)
)
dummy = IgnoreHandler()
if_updater = _gen_if_updater(config.interfaces, dummy)
for if_up in if_updater:
print(if_up.gen_set_definitions())
def on_service_reload(
ip_cmd: list[str], handler: UpdateHandler[IpAddressUpdate | SpecialIpUpdate]
) -> None:
# for now, reloading is kind of a hack to be able to react to nftables.service reloadings
# because then we need to re-apply all of our rules again
logger.info(
"reload signal received; reapply all rules (config file will not be read on reload)"
)
daemon.notify("RELOADING=1\nSTATUS=reloading all rules …\n")
handler.update(SpecialIpUpdate.FLUSH_RULES)
kickoff_ip(ip_cmd, handler)
def service_execution(args: argparse.Namespace, config: AppConfig) -> NoReturn:
nft_updater = NftUpdateHandler(
table=config.nft_table,
update_cmd=shlex.split(args.nft_command),
handler=SystemdHandler(),
)
nft_burst_handler = UpdateBurstHandler[NftUpdate](
burst_interval=0.1,
handler=(nft_updater,),
)
if_updater = _gen_if_updater(config.interfaces, nft_burst_handler)
burst_handler = UpdateBurstHandler[IpAddressUpdate | SpecialIpUpdate](
burst_interval=0.1,
handler=if_updater,
)
ip_cmd = shlex.split(args.ip_command)
# in case of systemd service reload
signal(SIGHUP, lambda *_a, **_b: on_service_reload(ip_cmd, burst_handler))
monitor_ip(ip_cmd, burst_handler)
def setup_logging(args: Any) -> None:
systemd_service = os.environ.get("INVOCATION_ID") and Path("/dev/log").exists()
if systemd_service:
logger.setLevel(logging.DEBUG)
logger.addHandler(JournalHandler(SYSLOG_IDENTIFIER="nft-update-addresses"))
else:
logging.basicConfig() # get output to stdout/stderr
logger.setLevel(LOG_LEVEL_MAP[args.log_level])
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("-c", "--config-file", required=True)
parser.add_argument("--check-config", action="store_true")
parser.add_argument("--output-set-definitions", action="store_true")
parser.add_argument("--ip-command", default="/usr/bin/env ip")
parser.add_argument("--nft-command", default="/usr/bin/env nft")
parser.add_argument(
"-l",
"--log-level",
default="error",
choices=LOG_LEVEL_MAP.keys(),
help="Log level for outputs to stdout/stderr (ignored when launched in a systemd service)",
)
args = parser.parse_args()
setup_logging(args)
config = read_config_file(Path(args.config_file))
if args.check_config:
return
if args.output_set_definitions:
return static_part_generation(config)
service_execution(args, config)
if __name__ == "__main__":
main()
Loading…
Cancel
Save