Compare commits
11 Commits
throwaway/
...
6f963920ec
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f963920ec | |||
| 21fa48d93d | |||
| cd602b74b6 | |||
| 34bf1c7c22 | |||
| 932dad911f | |||
| 2c6d586cf5 | |||
| 0781c7cb30 | |||
| 8838472001 | |||
| bd2c1583da | |||
| 4677dbe591 | |||
| c50e996851 |
@@ -101,3 +101,5 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
Keep fractional scaling sharp with Hyprland's `xwayland.force_zero_scaling` and Steam's own `STEAM_FORCE_DESKTOPUI_SCALING` instead.
|
||||
- AdGuard's LAN wildcard rewrite for `*.alexion.dev` points names at the reverse-proxy guest, including non-HTTP ports.
|
||||
Keep `git.alexion.dev:2022` working by proxying that TCP stream through the reverse-proxy guest to Gitea.
|
||||
- Remote deploy helpers that run target-side `sudo` over SSH need a TTY.
|
||||
Use `ssh -t` unless the target command is guaranteed not to prompt for sudo credentials.
|
||||
|
||||
329
flake.nix
329
flake.nix
@@ -118,6 +118,79 @@
|
||||
touch $out
|
||||
'';
|
||||
|
||||
copyparty-reverse-proxy-trust-config =
|
||||
let
|
||||
pikachuConfig = self.nixosConfigurations.pikachu.config;
|
||||
serviceConfig = pikachuConfig.containers.copyparty.config.systemd.services.copyparty.serviceConfig;
|
||||
configScript = builtins.readFile serviceConfig.ExecStartPre;
|
||||
plainConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/services/copyparty.nix
|
||||
{
|
||||
modules.services.copyparty = {
|
||||
enable = true;
|
||||
passwordFile = "/run/password";
|
||||
volumes."/" = {
|
||||
path = "/srv/files";
|
||||
access.A = [ "alexion" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
plainScript = builtins.readFile plainConfig.systemd.services.copyparty.serviceConfig.ExecStartPre;
|
||||
in
|
||||
pkgs.runCommand "copyparty-reverse-proxy-trust-config-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
lib.hasInfix " rproxy: 1" configScript
|
||||
&& lib.hasInfix " xff-hdr: x-forwarded-for" configScript
|
||||
&& lib.hasInfix " xff-src: 10.23.20.109/32" configScript
|
||||
&& !(lib.hasInfix "rproxy" plainScript)
|
||||
&& !(lib.hasInfix "xff-hdr" plainScript)
|
||||
&& !(lib.hasInfix "xff-src" plainScript)
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
copyparty-reverse-proxy-trust-requires-source =
|
||||
let
|
||||
testConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/services/copyparty.nix
|
||||
{
|
||||
modules.services.copyparty = {
|
||||
enable = true;
|
||||
passwordFile = "/run/password";
|
||||
reverseProxy.enable = true;
|
||||
volumes."/" = {
|
||||
path = "/srv/files";
|
||||
access.A = [ "alexion" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
hasFailedTrustedSourcesAssertion = lib.any (
|
||||
assertion:
|
||||
assertion.assertion == false
|
||||
&& assertion.message == "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled."
|
||||
) testConfig.assertions;
|
||||
in
|
||||
pkgs.runCommand "copyparty-reverse-proxy-trust-requires-source-check" { } ''
|
||||
${lib.optionalString (!hasFailedTrustedSourcesAssertion) "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-basic-route =
|
||||
let
|
||||
testConfig =
|
||||
@@ -150,24 +223,254 @@
|
||||
|
||||
reverse-proxy-actualbudget-guest-route =
|
||||
let
|
||||
testConfig =
|
||||
(self.nixosConfigurations.pikachu.extendModules {
|
||||
modules = [
|
||||
{
|
||||
guests.actualbudget.reverseProxy = {
|
||||
enable = true;
|
||||
host = "budget.alexion.dev";
|
||||
backend = "http://10.23.20.42:5006";
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
testConfig = self.nixosConfigurations.pikachu.config;
|
||||
route = testConfig.modules.reverse-proxy.routes.actualbudget;
|
||||
vhost = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts."budget.alexion.dev";
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-actualbudget-guest-route-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(route.host == "budget.alexion.dev" && route.backend == "http://10.23.20.42:5006")
|
||||
!(
|
||||
route.host == "budget.alexion.dev"
|
||||
&& route.backend == null
|
||||
&& route.endpoint == "pikachu-actualbudget"
|
||||
&& route.port == 5006
|
||||
&& route.tls
|
||||
&& route.aliases == [ ]
|
||||
&& route.path == "/"
|
||||
&& !route.websockets
|
||||
&& vhost.locations."/".proxyPass == "http://10.23.20.42:5006"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
generic-registry-fleet-view =
|
||||
let
|
||||
hosts = {
|
||||
alpha = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.registry = registry;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.alpha.service = "10.0.0.5";
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
beta = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.registry = registry;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
(
|
||||
{ registry, ... }:
|
||||
{
|
||||
environment.etc."registry-address-check".text = registry.address.alpha.service;
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
};
|
||||
registry = self.lib.registry.collect { inherit hosts; };
|
||||
betaConfig = hosts.beta.config;
|
||||
in
|
||||
pkgs.runCommand "generic-registry-fleet-view-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
registry.address.alpha.service == "10.0.0.5"
|
||||
&& betaConfig.environment.etc."registry-address-check".text == "10.0.0.5"
|
||||
&& !(registry.address ? alpha-service)
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
network-endpoint-fleet-registry =
|
||||
let
|
||||
hostEndpointConfig =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/network.nix
|
||||
{
|
||||
modules.network.endpoint.address = "10.23.50.146";
|
||||
}
|
||||
];
|
||||
};
|
||||
guestEndpointConfig =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
./modules/network.nix
|
||||
./modules/reverse-proxy.nix
|
||||
(self.lib.guest { name = "copyparty"; })
|
||||
{
|
||||
modules.network.vlans = [ 20 ];
|
||||
guests.copyparty = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
endpoint.address = "10.23.20.126";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
fleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints {
|
||||
neogaia = hostEndpointConfig;
|
||||
pikachu = guestEndpointConfig;
|
||||
};
|
||||
in
|
||||
pkgs.runCommand "network-endpoint-fleet-registry-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
fleetEndpoints.neogaia.address == "10.23.50.146"
|
||||
&& fleetEndpoints."pikachu-copyparty".address == "10.23.20.126"
|
||||
&& !(fleetEndpoints ? pikachu)
|
||||
&& guestEndpointConfig.config.containers.copyparty.config.systemd.network.networks."20-eth0".networkConfig.DHCP == "yes"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-fleet-routes =
|
||||
let
|
||||
routeHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy.routes.app = {
|
||||
host = "worker.alexion.dev";
|
||||
backend = "http://10.23.20.126:3923";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
endpoints = self.lib.networkEndpoints.collectFleetEndpoints {
|
||||
pikachu = self.nixosConfigurations.pikachu;
|
||||
};
|
||||
fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes {
|
||||
inherit routeHost;
|
||||
pikachu = self.nixosConfigurations.pikachu;
|
||||
} endpoints;
|
||||
testConfig =
|
||||
(self.nixosConfigurations.pikachu.extendModules {
|
||||
specialArgs.reverseProxyFleetRoutes = fleetRoutes;
|
||||
}).config;
|
||||
vhosts = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts;
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-fleet-routes-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
fleetRoutes ? routeHost-app
|
||||
&& fleetRoutes ? pikachu-actualbudget
|
||||
&& vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923"
|
||||
&& vhosts."budget.alexion.dev".locations."/".proxyPass == "http://10.23.20.42:5006"
|
||||
&& vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-endpoint-backends =
|
||||
let
|
||||
endpointHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/network.nix
|
||||
{
|
||||
modules.network.endpoint.address = "10.23.50.146";
|
||||
}
|
||||
];
|
||||
};
|
||||
routeHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy.routes.app = {
|
||||
host = "worker.alexion.dev";
|
||||
endpoint = "worker";
|
||||
port = 8080;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
guestRouteHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
./modules/network.nix
|
||||
./modules/reverse-proxy.nix
|
||||
(self.lib.guest { name = "files"; })
|
||||
{
|
||||
networking.hostName = "pikachu";
|
||||
modules.network.vlans = [ 20 ];
|
||||
guests.files = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
endpoint.address = "10.23.20.126";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
host = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
endpoints = self.lib.networkEndpoints.collectFleetEndpoints {
|
||||
worker = endpointHost;
|
||||
pikachu = guestRouteHost;
|
||||
};
|
||||
fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes {
|
||||
inherit routeHost;
|
||||
pikachu = guestRouteHost;
|
||||
} endpoints;
|
||||
testConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy = {
|
||||
enable = true;
|
||||
routes = fleetRoutes;
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
vhosts = testConfig.services.nginx.virtualHosts;
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-endpoint-backends-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.50.146:8080"
|
||||
&& fleetRoutes."pikachu-files".endpoint == "pikachu-files"
|
||||
&& vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
|
||||
@@ -43,6 +43,10 @@ in
|
||||
enable = true;
|
||||
inherit (cfg) accountName stateDir;
|
||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||
reverseProxy = {
|
||||
enable = cfg.reverseProxy.enable;
|
||||
inherit (cfg.reverseProxy) trustedSources;
|
||||
};
|
||||
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||
inherit (volume) path access flags;
|
||||
}) cfg.volumes;
|
||||
@@ -77,6 +81,12 @@ in
|
||||
description = "Host path bind-mounted as Copyparty's state directory.";
|
||||
};
|
||||
|
||||
reverseProxy.trustedSources = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
||||
};
|
||||
|
||||
volumes = lib.mkOption {
|
||||
type = lib.types.attrsOf volumeType;
|
||||
default = { };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
reverseProxyFleetRoutes ? config.modules.reverse-proxy.routes,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -34,7 +35,7 @@ in
|
||||
interior = {
|
||||
modules.reverse-proxy = {
|
||||
enable = true;
|
||||
routes = config.modules.reverse-proxy.routes // legacyRoutes;
|
||||
routes = reverseProxyFleetRoutes // legacyRoutes;
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 2022 ];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ pkgs, ... }:
|
||||
{ config, pkgs, ... }:
|
||||
# pikachu — AZW ME Pro server.
|
||||
# Disk layout is in ./disk.nix.
|
||||
# `fileSystems` for the root disk are derived from it.
|
||||
@@ -79,10 +79,11 @@
|
||||
vlan = 20;
|
||||
mac = "BC:24:11:C1:CD:28";
|
||||
dataPath = "/pikachu/data/actualbudget";
|
||||
endpoint.address = "10.23.20.42";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
host = "budget.alexion.dev";
|
||||
backend = "http://10.23.20.42:5006";
|
||||
port = 5006;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -90,6 +91,7 @@
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
mac = "02:98:64:5a:07:4f";
|
||||
endpoint.address = "10.23.20.109";
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
@@ -97,10 +99,12 @@
|
||||
vlan = 20;
|
||||
mac = "02:23:20:39:23:01";
|
||||
statePath = "/pikachu/data/copyparty";
|
||||
endpoint.address = "10.23.20.126";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
host = "files.alexion.dev";
|
||||
backend = "http://10.23.20.126:3923";
|
||||
port = 3923;
|
||||
trustedSources = [ "${config.modules.network.endpoints."reverse-proxy".address}/32" ];
|
||||
};
|
||||
volumes = {
|
||||
"/inbox" = {
|
||||
|
||||
@@ -14,6 +14,8 @@ let
|
||||
;
|
||||
|
||||
serviceIdentityRegistry = import ./service-identities.nix { inherit lib; };
|
||||
registry = import ./registry.nix { inherit lib self; };
|
||||
networkEndpoints = import ./network-endpoints.nix { inherit lib; };
|
||||
reverseProxyRoutes = import ./reverse-proxy-routes.nix { inherit lib; };
|
||||
|
||||
collectNixFiles =
|
||||
@@ -33,10 +35,16 @@ let
|
||||
) (builtins.readDir dir)
|
||||
);
|
||||
|
||||
specialArgs = {
|
||||
inherit inputs;
|
||||
my = self.lib;
|
||||
};
|
||||
specialArgs =
|
||||
let
|
||||
networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations;
|
||||
in
|
||||
{
|
||||
inherit inputs networkFleetEndpoints self;
|
||||
my = self.lib;
|
||||
registry = self.lib.registry.collect { hosts = self.nixosConfigurations; };
|
||||
reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations networkFleetEndpoints;
|
||||
};
|
||||
|
||||
bridgeName = id: "br-vlan${toString id}";
|
||||
|
||||
@@ -65,6 +73,7 @@ let
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
registry.module
|
||||
(self + "/system.nix")
|
||||
(self + "/hosts/${hostName}")
|
||||
{ networking.hostName = hostName; }
|
||||
@@ -84,6 +93,8 @@ in
|
||||
mkHost
|
||||
mkHosts
|
||||
bridgeName
|
||||
registry
|
||||
networkEndpoints
|
||||
reverseProxyRoutes
|
||||
;
|
||||
|
||||
|
||||
@@ -131,6 +131,18 @@ let
|
||||
guest takes its address by DHCP, keeping IP management at the router.
|
||||
'';
|
||||
};
|
||||
endpoint = {
|
||||
address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.23.20.126";
|
||||
description = ''
|
||||
Expected IP address assigned outside NixOS for this guest's
|
||||
network endpoint. Left null, this guest contributes no endpoint to
|
||||
the fleet endpoint registry.
|
||||
'';
|
||||
};
|
||||
};
|
||||
reverseProxy = reverseProxyRoutes.mkRouteOptions {
|
||||
subject = "guest route";
|
||||
enableDefault = false;
|
||||
@@ -278,8 +290,17 @@ let
|
||||
config = lib.mkIf cfg.enable {
|
||||
sops.secrets = lib.genAttrs cfg.secrets (_: { });
|
||||
|
||||
modules.network.endpoints.${machineName} = lib.mkIf (cfg.endpoint.address != null) {
|
||||
inherit (cfg.endpoint) address;
|
||||
};
|
||||
|
||||
modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable (
|
||||
reverseProxyRoutes.routeFields cfg.reverseProxy
|
||||
reverseProxyRoutes.routeFields (
|
||||
cfg.reverseProxy
|
||||
// lib.optionalAttrs (cfg.reverseProxy.backend == null && cfg.reverseProxy.endpoint == null) {
|
||||
endpoint = "${config.networking.hostName}-${machineName}";
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
assertions = [
|
||||
|
||||
42
lib/network-endpoints.nix
Normal file
42
lib/network-endpoints.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
{ lib }:
|
||||
let
|
||||
endpointOptions = {
|
||||
address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "10.23.20.126";
|
||||
description = "Expected IP address assigned outside NixOS for this network endpoint.";
|
||||
};
|
||||
};
|
||||
|
||||
endpointType = lib.types.submodule {
|
||||
options = endpointOptions;
|
||||
};
|
||||
|
||||
endpointFields = endpoint: {
|
||||
inherit (endpoint) address;
|
||||
};
|
||||
|
||||
collectFleetEndpoints =
|
||||
hosts:
|
||||
lib.concatMapAttrs (
|
||||
hostName: host:
|
||||
let
|
||||
cfg = host.config.modules.network;
|
||||
hostEndpoint = lib.optionalAttrs (cfg.endpoint.address != null) {
|
||||
${hostName} = endpointFields cfg.endpoint;
|
||||
};
|
||||
guestEndpoints = lib.mapAttrs' (
|
||||
endpointName: endpoint: lib.nameValuePair "${hostName}-${endpointName}" (endpointFields endpoint)
|
||||
) cfg.endpoints;
|
||||
in
|
||||
hostEndpoint // guestEndpoints
|
||||
) hosts;
|
||||
in
|
||||
{
|
||||
inherit
|
||||
collectFleetEndpoints
|
||||
endpointFields
|
||||
endpointOptions
|
||||
endpointType
|
||||
;
|
||||
}
|
||||
45
lib/registry.nix
Normal file
45
lib/registry.nix
Normal file
@@ -0,0 +1,45 @@
|
||||
{ lib, self }:
|
||||
let
|
||||
inherit (lib)
|
||||
filterAttrs
|
||||
hasSuffix
|
||||
mapAttrs
|
||||
mapAttrs'
|
||||
mkOption
|
||||
nameValuePair
|
||||
removeSuffix
|
||||
types
|
||||
;
|
||||
|
||||
registryName = fileName: removeSuffix ".nix" fileName;
|
||||
|
||||
registryOption = registry: mkOption {
|
||||
type = types.attrsOf (types.attrsOf registry.type);
|
||||
default = { };
|
||||
description = ''
|
||||
Lazily collected fleet-visible registry entries, nested by host and entry name.
|
||||
'';
|
||||
};
|
||||
|
||||
definitions = mapAttrs' (
|
||||
name: type:
|
||||
let
|
||||
path = self + "/registry/${name}";
|
||||
in
|
||||
nameValuePair (registryName name) (import path { inherit lib; })
|
||||
) (filterAttrs (name: type: type == "regular" && hasSuffix ".nix" name) (builtins.readDir (self + "/registry")));
|
||||
in
|
||||
{
|
||||
inherit definitions;
|
||||
|
||||
module = {
|
||||
options.registry = mapAttrs (_name: registry: registryOption registry) definitions;
|
||||
};
|
||||
|
||||
collect =
|
||||
{ hosts }:
|
||||
mapAttrs (
|
||||
registryName: _registry:
|
||||
mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts
|
||||
) definitions;
|
||||
}
|
||||
@@ -37,10 +37,30 @@ let
|
||||
};
|
||||
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Full HTTP backend URL that receives proxied traffic.";
|
||||
};
|
||||
|
||||
endpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Fleet network endpoint key used to derive the backend URL.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.ints.between 1 65535);
|
||||
default = null;
|
||||
example = 3923;
|
||||
description = "Backend TCP port used with the fleet network endpoint.";
|
||||
};
|
||||
|
||||
scheme = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "http";
|
||||
description = "Backend URL scheme used with the fleet network endpoint.";
|
||||
};
|
||||
|
||||
tls = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
@@ -60,13 +80,45 @@ let
|
||||
aliases
|
||||
path
|
||||
backend
|
||||
endpoint
|
||||
port
|
||||
scheme
|
||||
tls
|
||||
websockets
|
||||
;
|
||||
};
|
||||
|
||||
endpointBackend = endpoints: route:
|
||||
let
|
||||
endpoint = endpoints.${route.endpoint} or (
|
||||
throw "Unknown reverse-proxy endpoint `${route.endpoint}`. Known endpoints: ${lib.concatStringsSep ", " (lib.attrNames endpoints)}"
|
||||
);
|
||||
in
|
||||
"${route.scheme}://${endpoint.address}:${toString route.port}";
|
||||
|
||||
resolvedRouteFields = endpoints: route:
|
||||
let
|
||||
routeWithBackend =
|
||||
if route.backend != null then
|
||||
route
|
||||
else if route.endpoint != null && route.port != null then
|
||||
route // { backend = endpointBackend endpoints route; }
|
||||
else
|
||||
throw "Reverse-proxy route `${route.host}${route.path}` needs either backend or endpoint plus port.";
|
||||
in
|
||||
routeFields routeWithBackend;
|
||||
|
||||
collectFleetRoutes =
|
||||
hosts: endpoints:
|
||||
lib.concatMapAttrs (
|
||||
hostName: host:
|
||||
lib.mapAttrs' (
|
||||
routeName: route: lib.nameValuePair "${hostName}-${routeName}" (resolvedRouteFields endpoints route)
|
||||
) host.config.modules.reverse-proxy.routes
|
||||
) hosts;
|
||||
in
|
||||
{
|
||||
inherit mkRouteOptions routeFields;
|
||||
inherit collectFleetRoutes mkRouteOptions resolvedRouteFields routeFields;
|
||||
|
||||
routeType = lib.types.submodule (
|
||||
{ name, ... }:
|
||||
|
||||
@@ -11,6 +11,38 @@ let
|
||||
|
||||
# The hyprlock this module installs, used by the lock keybind below.
|
||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||
|
||||
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||
|
||||
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
||||
unit=lockscreen-idle-inhibit.service
|
||||
|
||||
refresh_waybar() {
|
||||
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ${pkgs.systemd}/bin/systemd-run \
|
||||
--user \
|
||||
--collect \
|
||||
--unit=lockscreen-idle-inhibit \
|
||||
--description="Disable idle-triggered screen locking" \
|
||||
${pkgs.systemd}/bin/systemd-inhibit \
|
||||
--what=idle \
|
||||
--who=lockscreen-hotkey \
|
||||
--why="lock screen disabled by hotkey" \
|
||||
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.desktop.hyprland.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen";
|
||||
@@ -55,6 +87,7 @@ in
|
||||
# hyprlock never stacks over the first.
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||
"$mod CTRL, X, exec, ${idleLockToggle}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ let
|
||||
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||
|
||||
# Renders and toggles mako's do-not-disturb mode.
|
||||
# With no mako daemon answering, the status reads "notifications on", keeping
|
||||
@@ -38,6 +39,14 @@ let
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
fi
|
||||
'';
|
||||
|
||||
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet lockscreen-idle-inhibit.service; then
|
||||
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
||||
else
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
||||
@@ -63,6 +72,7 @@ in
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [
|
||||
"custom/recording"
|
||||
"custom/idle-lock"
|
||||
"mpris"
|
||||
"wireplumber"
|
||||
"network"
|
||||
@@ -155,6 +165,13 @@ in
|
||||
exec = "${recStatus}";
|
||||
interval = 1;
|
||||
};
|
||||
|
||||
"custom/idle-lock" = {
|
||||
return-type = "json";
|
||||
exec = "${idleLockStatus}";
|
||||
interval = "once";
|
||||
signal = 9;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ in
|
||||
vim = "nvim";
|
||||
cp = "cp -v";
|
||||
tmx = "tmux new-session -A -s";
|
||||
hs = "herdr --session";
|
||||
|
||||
tarnow = "tar -acf ";
|
||||
untar = "tar -zxvf ";
|
||||
@@ -120,6 +121,11 @@ in
|
||||
description = "Copy a file, or recursively copy a source directory into a destination";
|
||||
body = builtins.readFile ./functions/copy.fish;
|
||||
};
|
||||
|
||||
deploy-to = {
|
||||
description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild";
|
||||
body = builtins.readFile ./functions/deploy-to.fish;
|
||||
};
|
||||
};
|
||||
|
||||
# Rendered by home-manager into ~/.config/fish/config.fish.
|
||||
|
||||
53
modules/fish/functions/deploy-to.fish
Normal file
53
modules/fish/functions/deploy-to.fish
Normal file
@@ -0,0 +1,53 @@
|
||||
if test (count $argv) -lt 1; or test (count $argv) -gt 2
|
||||
echo "usage: deploy-to <host> [branch]" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
set host $argv[1]
|
||||
set branch $argv[2]
|
||||
|
||||
if test -z "$branch"
|
||||
set branch (git branch --show-current)
|
||||
end
|
||||
|
||||
if test -z "$branch"
|
||||
echo "deploy-to: could not determine the current branch" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||
or begin
|
||||
echo "deploy-to: run this from a git checkout" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
set dirty_lines (git status --porcelain)
|
||||
if test (count $dirty_lines) -gt 0
|
||||
echo "deploy-to: refusing to deploy with a dirty local worktree" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
git fetch origin
|
||||
or return $status
|
||||
|
||||
set remote_ref refs/remotes/origin/$branch
|
||||
git rev-parse --verify --quiet $remote_ref >/dev/null
|
||||
or begin
|
||||
echo "deploy-to: origin/$branch does not exist" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set local_head (git rev-parse HEAD)
|
||||
set remote_head (git rev-parse $remote_ref)
|
||||
|
||||
if test "$local_head" != "$remote_head"
|
||||
echo "deploy-to: local HEAD differs from origin/$branch" >&2
|
||||
echo "deploy-to: push or reset the branch before deploying" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set escaped_branch (string escape -- $branch)
|
||||
set escaped_host (string escape -- $host)
|
||||
set remote_command "cd ~/wrk/dotfiles && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
|
||||
|
||||
ssh -t $host $remote_command
|
||||
@@ -43,6 +43,28 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
endpoint = {
|
||||
address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.23.50.146";
|
||||
description = ''
|
||||
Expected IP address assigned outside NixOS for this host's network
|
||||
endpoint. Left null, this host contributes no host endpoint to the
|
||||
fleet endpoint registry.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
endpoints = lib.mkOption {
|
||||
type = lib.types.attrsOf my.networkEndpoints.endpointType;
|
||||
default = { };
|
||||
description = ''
|
||||
Network endpoint declarations contributed by placed workloads on this
|
||||
host, keyed by their local stable endpoint name.
|
||||
'';
|
||||
};
|
||||
|
||||
management = {
|
||||
vlan = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.ints.between 1 4094);
|
||||
|
||||
@@ -20,6 +20,8 @@ let
|
||||
in
|
||||
lib.length paths != lib.length (lib.unique paths);
|
||||
|
||||
hasUnresolvedBackend = route: route.backend == null;
|
||||
|
||||
mkLocation = route: {
|
||||
proxyPass = route.backend;
|
||||
proxyWebsockets = route.websockets;
|
||||
@@ -34,7 +36,9 @@ let
|
||||
enableACME = rootRoute.tls;
|
||||
serverAliases = rootRoute.aliases;
|
||||
locations = lib.listToAttrs (
|
||||
map (route: lib.nameValuePair route.path (mkLocation route)) routes
|
||||
map (route: lib.nameValuePair route.path (mkLocation route)) (
|
||||
builtins.filter (route: route.backend != null) routes
|
||||
)
|
||||
);
|
||||
};
|
||||
in
|
||||
@@ -58,7 +62,11 @@ in
|
||||
++ lib.mapAttrsToList (host: routes: {
|
||||
assertion = !hasDuplicatePaths routes;
|
||||
message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once.";
|
||||
}) routesByHost;
|
||||
}) routesByHost
|
||||
++ map (route: {
|
||||
assertion = !hasUnresolvedBackend route;
|
||||
message = "modules.reverse-proxy.routes for ${route.host}${route.path} must resolve to a backend URL before actualization.";
|
||||
}) (builtins.filter hasUnresolvedBackend (lib.attrValues enabledRoutes));
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
@@ -72,6 +80,13 @@ in
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
commonHttpConfig = ''
|
||||
server {
|
||||
listen 0.0.0.0:443 ssl default_server;
|
||||
listen [::0]:443 ssl default_server;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
'';
|
||||
recommendedGzipSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedProxySettings = true;
|
||||
|
||||
@@ -48,6 +48,14 @@ let
|
||||
''}
|
||||
'';
|
||||
|
||||
renderTrustedSource = source: " xff-src: ${source}";
|
||||
|
||||
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
||||
rproxy: 1
|
||||
xff-hdr: x-forwarded-for
|
||||
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
|
||||
'';
|
||||
|
||||
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
||||
|
||||
configScript = pkgs.writeShellScript "copyparty-config" ''
|
||||
@@ -68,7 +76,7 @@ let
|
||||
ses-db: ${cfg.stateDir}/sessions.db
|
||||
chpw-db: ${cfg.stateDir}/chpw.json
|
||||
shr-db: ${cfg.stateDir}/shares.db
|
||||
|
||||
${renderedReverseProxy}
|
||||
[accounts]
|
||||
${cfg.accountName}: $password
|
||||
|
||||
@@ -128,6 +136,16 @@ in
|
||||
default = { };
|
||||
description = "Copyparty volumes keyed by their URL path.";
|
||||
};
|
||||
|
||||
reverseProxy = {
|
||||
enable = lib.mkEnableOption "reverse-proxy trust settings for Copyparty";
|
||||
|
||||
trustedSources = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
@@ -136,6 +154,10 @@ in
|
||||
assertion = cfg.volumes != { };
|
||||
message = "modules.services.copyparty.volumes must declare at least one served volume.";
|
||||
}
|
||||
{
|
||||
assertion = !cfg.reverseProxy.enable || cfg.reverseProxy.trustedSources != [ ];
|
||||
message = "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled.";
|
||||
}
|
||||
];
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
|
||||
4
registry/address.nix
Normal file
4
registry/address.nix
Normal file
@@ -0,0 +1,4 @@
|
||||
{ lib }:
|
||||
{
|
||||
type = lib.types.str;
|
||||
}
|
||||
Reference in New Issue
Block a user