From cfac122324e3d05f2925673e99feacf8c3602e0c Mon Sep 17 00:00:00 2001 From: Linnnus Date: Tue, 13 Feb 2024 17:38:25 +0100 Subject: tmp: Move modules/nixos to services/ for shared and hosts/ahmed for private --- flake.nix | 8 +- hosts/ahmed/cloudflare-proxy/default.nix | 109 +++++++ hosts/ahmed/configuration.nix | 9 + hosts/ahmed/disable-screen/default.nix | 61 ++++ hosts/ahmed/duksebot/default.nix | 72 +++++ hosts/ahmed/forsvarsarper/default.nix | 65 ++++ hosts/ahmed/forsvarsarper/script.py | 28 ++ hosts/ahmed/git.linus.onl/about.html | 5 + hosts/ahmed/git.linus.onl/default.nix | 95 ++++++ hosts/ahmed/hellohtml.linus.onl/default.nix | 60 ++++ hosts/ahmed/linus.onl/default.nix | 100 ++++++ hosts/ahmed/nofitications.linus.onl/default.nix | 34 +++ modules/nixos/cloudflare-proxy/default.nix | 109 ------- modules/nixos/default.nix | 19 -- modules/nixos/disable-screen/default.nix | 61 ---- modules/nixos/duksebot/default.nix | 72 ----- modules/nixos/forsvarsarper/default.nix | 65 ---- modules/nixos/forsvarsarper/script.py | 28 -- modules/nixos/git.linus.onl/about.html | 5 - modules/nixos/git.linus.onl/default.nix | 95 ------ modules/nixos/graphics/default.nix | 37 --- modules/nixos/hellohtml.linus.onl/default.nix | 60 ---- modules/nixos/hellohtml/default.nix | 121 -------- modules/nixos/linus.onl/default.nix | 100 ------ modules/nixos/nofitications.linus.onl/default.nix | 34 --- modules/nixos/on-demand-minecraft/default.nix | 355 ---------------------- services/default.nix | 4 + services/hellohtml/default.nix | 121 ++++++++ services/on-demand-minecraft/default.nix | 355 ++++++++++++++++++++++ 29 files changed, 1120 insertions(+), 1167 deletions(-) create mode 100644 hosts/ahmed/cloudflare-proxy/default.nix create mode 100644 hosts/ahmed/disable-screen/default.nix create mode 100644 hosts/ahmed/duksebot/default.nix create mode 100644 hosts/ahmed/forsvarsarper/default.nix create mode 100644 hosts/ahmed/forsvarsarper/script.py create mode 100644 hosts/ahmed/git.linus.onl/about.html create mode 100644 hosts/ahmed/git.linus.onl/default.nix create mode 100644 hosts/ahmed/hellohtml.linus.onl/default.nix create mode 100644 hosts/ahmed/linus.onl/default.nix create mode 100644 hosts/ahmed/nofitications.linus.onl/default.nix delete mode 100644 modules/nixos/cloudflare-proxy/default.nix delete mode 100644 modules/nixos/default.nix delete mode 100644 modules/nixos/disable-screen/default.nix delete mode 100644 modules/nixos/duksebot/default.nix delete mode 100644 modules/nixos/forsvarsarper/default.nix delete mode 100644 modules/nixos/forsvarsarper/script.py delete mode 100644 modules/nixos/git.linus.onl/about.html delete mode 100644 modules/nixos/git.linus.onl/default.nix delete mode 100644 modules/nixos/graphics/default.nix delete mode 100644 modules/nixos/hellohtml.linus.onl/default.nix delete mode 100644 modules/nixos/hellohtml/default.nix delete mode 100644 modules/nixos/linus.onl/default.nix delete mode 100644 modules/nixos/nofitications.linus.onl/default.nix delete mode 100644 modules/nixos/on-demand-minecraft/default.nix create mode 100644 services/default.nix create mode 100644 services/hellohtml/default.nix create mode 100644 services/on-demand-minecraft/default.nix diff --git a/flake.nix b/flake.nix index 588bf98..daa98d4 100644 --- a/flake.nix +++ b/flake.nix @@ -47,9 +47,6 @@ darwinModules = builtins.attrValues (import ./modules/darwin).general ++ builtins.attrValues (import ./modules/darwin).personal; - nixosModules = - builtins.attrValues (import ./modules/nixos).general - ++ builtins.attrValues (import ./modules/nixos).personal; # This is a function that generates an attribute by calling a function # you pass to it, with each system as an argument. `systems` lists all @@ -91,8 +88,7 @@ ./hosts/ahmed/configuration.nix ./hosts/common.nix ./home - ] - ++ nixosModules; + ] ++ builtins.attrValues (import ./services); }; }; @@ -112,7 +108,7 @@ # We export the generally applicable modules. darwinModules = (import ./modules/darwin).geneal; - nixosModules = (import ./modules/nixos).general; homeModules = import ./modules/home-manager; + nixosModules = import ./services; }; } diff --git a/hosts/ahmed/cloudflare-proxy/default.nix b/hosts/ahmed/cloudflare-proxy/default.nix new file mode 100644 index 0000000..657722d --- /dev/null +++ b/hosts/ahmed/cloudflare-proxy/default.nix @@ -0,0 +1,109 @@ +# This module adds some extra configuration useful when running behid a Cloudflare Proxy. +# +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib.options) mkEnableOption mkOption; + inherit (lib.modules) mkIf; + inherit (lib.types) listOf nonEmptyStr port; + + # TODO: What happens when these get out of date??? Huh??? You little pissbaby + fileToList = x: lib.strings.splitString "\n" (builtins.readFile x); + cfipv4 = fileToList (pkgs.fetchurl { + url = "https://www.cloudflare.com/ips-v4"; + hash = "sha256-8Cxtg7wBqwroV3Fg4DbXAMdFU1m84FTfiE5dfZ5Onns="; + }); + cfipv6 = fileToList (pkgs.fetchurl { + url = "https://www.cloudflare.com/ips-v6"; + hash = "sha256-np054+g7rQDE3sr9U8Y/piAp89ldto3pN9K+KCNMoKk="; + }); + + cfg = config.modules.cloudflare-proxy; +in { + options.modules.cloudflare-proxy = { + enable = mkEnableOption "Cloudflare proxy IP extraction for NGINX"; + + firewall = { + IPv4Whitelist = mkOption { + description = "List of IPv4 addresses (or ranges) added to the whitelist."; + type = listOf nonEmptyStr; + default = []; + }; + + IPv6Whitelist = mkOption { + description = "List of IPv6 addresses (or ranges) added to the whitelist."; + type = listOf nonEmptyStr; + default = []; + }; + }; + }; + + config = mkIf cfg.enable { + # Teach NGINX how to extract the proxied IP from proxied requests. + # + # See: https://nixos.wiki/wiki/Nginx#Using_realIP_when_behind_CloudFlare_or_other_CDN + services.nginx.commonHttpConfig = let + realIpsFromList = lib.strings.concatMapStringsSep "\n" (x: "set_real_ip_from ${x};"); + in '' + ${realIpsFromList cfipv4} + ${realIpsFromList cfipv6} + real_ip_header CF-Connecting-IP; + ''; + + # Block non-Cloudflare IP addresses. + networking.firewall = let + chain = "cloudflare-whitelist"; + in { + extraCommands = let + allow-interface = lib.strings.concatMapStringsSep "\n" (i: ''ip46tables --append ${chain} --in-interface ${i} --jump RETURN''); + allow-ip = cmd: lib.strings.concatMapStringsSep "\n" (r: ''${cmd} --append ${chain} --source ${r} --jump RETURN''); + in '' + # Flush the old firewall rules. This behavior mirrors the default firewall service. + # See: https://github.com/NixOS/nixpkgs/blob/ac911bf685eecc17c2df5b21bdf32678b9f88c92/nixos/modules/services/networking/firewall-iptables.nix#L59-L66 + # TEMP: Removed 2>/dev/null + ip46tables --delete INPUT --protocol tcp --destination-port 80 --syn --jump ${chain} || true + ip46tables --delete INPUT --protocol tcp --destination-port 443 --syn --jump ${chain} || true + ip46tables --flush ${chain} || true + ip46tables --delete-chain ${chain} || true + + # Create a chain that only allows whitelisted IPs through. + ip46tables --new-chain ${chain} + + # Allow trusted interfaces through. + ${allow-interface config.networking.firewall.trustedInterfaces} + + # Allow local whitelisted IPs through + ${allow-ip "iptables" cfg.firewall.IPv4Whitelist} + ${allow-ip "ip6tables" cfg.firewall.IPv6Whitelist} + + # Allow Cloudflare's IP ranges through. + ${allow-ip "iptables" cfipv4} + ${allow-ip "ip6tables" cfipv6} + + # Everything else is dropped. + # + # TODO: I would like to use `nixos-fw-log-refuse` here, but I keep + # running into weird issues when reloading the firewall. + # Something about the table not being deleted properly. + ip46tables --append ${chain} --jump DROP + + # Inject our chain as the first check in INPUT (before nixos-fw). + # We want to capture any new incomming TCP connections. + ip46tables --insert INPUT 1 --protocol tcp --destination-port 80 --syn --jump ${chain} + ip46tables --insert INPUT 1 --protocol tcp --destination-port 443 --syn --jump ${chain} + ''; + extraStopCommands = '' + # Clean up added rulesets (${chain}). This mirrors the behavior of the + # default firewall at the time of writing. + # + # See: https://github.com/NixOS/nixpkgs/blob/ac911bf685eecc17c2df5b21bdf32678b9f88c92/nixos/modules/services/networking/firewall-iptables.nix#L218-L219 + # TEMP: Removed 2>/dev/null + ip46tables --delete INPUT --protocol tcp --destination-port 80 --syn --jump ${chain} || true + ip46tables --delete INPUT --protocol tcp --destination-port 443 --syn --jump ${chain} || true + ''; + }; + }; +} diff --git a/hosts/ahmed/configuration.nix b/hosts/ahmed/configuration.nix index 02ef997..12f4f10 100644 --- a/hosts/ahmed/configuration.nix +++ b/hosts/ahmed/configuration.nix @@ -8,6 +8,15 @@ }: { imports = [ ./hardware-configuration.nix + + ./cloudflare-proxy + ./disable-screen + ./duksebot + ./forsvarsarper + ./git.linus.onl + ./hellohtml.linus.onl + ./linus.onl + ./nofitications.linus.onl ./ssh ]; diff --git a/hosts/ahmed/disable-screen/default.nix b/hosts/ahmed/disable-screen/default.nix new file mode 100644 index 0000000..638437a --- /dev/null +++ b/hosts/ahmed/disable-screen/default.nix @@ -0,0 +1,61 @@ +# This file defines some configuration options which disable the screen. This +# is only relevant because this host is an old laptop running as a server. +{ + lib, + config, + ... +}: let + inherit (lib) mkEnableOption mkOption mkIf types; + + cfg = config.services.disable-screen; +in { + options.services.disable-screen = { + enable = mkEnableOption "disable screen"; + + device-path = mkOption { + description = "Path to the device in the `/sys` file system."; + type = types.str; + example = "/sys/class/backlight/intel_backlight"; + }; + + device-unit = mkOption { + description = "The systemd device unit that corresponds to the device speciefied in `device-path`."; + type = types.str; + example = "sys-devices-pci...-intel_backligt.device"; + }; + }; + + config = mkIf cfg.enable { + # Disable sleep on lid close. + services.logind = let + lidSwitchAction = "ignore"; + in { + lidSwitchExternalPower = lidSwitchAction; + lidSwitchDocked = lidSwitchAction; + lidSwitch = lidSwitchAction; + }; + + # Don't store screen brightness between boots. We always want to turn off the + # screen. + # + # See: https://wiki.archlinux.org/title/backlight#Save_and_restore_functionality + # See: https://github.com/NixOS/nixpkgs/blob/990398921f677615c0732d704857484b84c6c888/nixos/modules/system/boot/systemd.nix#L97-L101 + systemd.suppressedSystemUnits = ["systemd-backlight@.service"]; + + # FIXME: Figure out how to enable screen when on-device debugging is necessary. + # Create a new service which turns off the display on boot. + # + # See: https://nixos.wiki/wiki/Backlight#.2Fsys.2Fclass.2Fbacklight.2F... + # See: https://superuser.com/questions/851846/how-to-write-a-systemd-service-that-depends-on-a-device-being-present + systemd.services.disable-screen = { + requires = [cfg.device-unit]; + after = [cfg.device-unit]; + wantedBy = [cfg.device-unit]; + + serviceConfig.Type = "oneshot"; + script = '' + tee ${cfg.device-path}/brightness <<<0 + ''; + }; + }; +} diff --git a/hosts/ahmed/duksebot/default.nix b/hosts/ahmed/duksebot/default.nix new file mode 100644 index 0000000..4c10cd8 --- /dev/null +++ b/hosts/ahmed/duksebot/default.nix @@ -0,0 +1,72 @@ +# This module defines an on-demand minecraft server service which turns off the +# server when it's not being used. +{ + config, + lib, + pkgs, + modulesPath, + ... +}: let + inherit (lib) mkIf mkOption mkEnableOption types; + + cfg = config.services.duksebot; +in { + options.services.duksebot = { + enable = mkEnableOption "duksebot daily reminder"; + + package = mkOption { + description = "What package to use"; + default = pkgs.duksebot; + type = types.package; + }; + }; + + config = mkIf cfg.enable { + # Create a user to run the server under. + users.users.duksebot = { + description = "Runs daily dukse reminder"; + group = "duksebot"; + isSystemUser = true; + home = "/srv/duksebot"; + createHome = true; + }; + users.groups.duksebot = {}; + + age.secrets.duksebot-env = { + file = ../../../secrets/duksebot.env.age; + owner = config.users.users.duksebot.name; + group = config.users.users.duksebot.group; + mode = "0440"; + }; + + # Create a service which simply runs script. This will be invoked by our timer. + systemd.services.duksebot = { + serviceConfig = { + # We only want to run this once every time the timer triggers it. + Type = "oneshot"; + # Run as the user we created above. + User = "duksebot"; + Group = "duksebot"; + WorkingDirectory = config.users.users.duksebot.home; + }; + script = '' + # Load the secret environment variables. + export $(grep -v '^#' ${config.age.secrets.duksebot-env.path} | xargs) + # Kick off. + exec "${cfg.package}"/bin/duksebot + ''; + }; + + # Create a timer to activate our oneshot service. + systemd.timers.duksebot = { + wantedBy = ["timers.target"]; + partOf = ["duksebot.service"]; + after = ["network-online.target"]; + wants = ["network-online.target"]; + timerConfig = { + OnCalendar = "*-*-* 7:00:00"; + Unit = "duksebot.service"; + }; + }; + }; +} diff --git a/hosts/ahmed/forsvarsarper/default.nix b/hosts/ahmed/forsvarsarper/default.nix new file mode 100644 index 0000000..7052f04 --- /dev/null +++ b/hosts/ahmed/forsvarsarper/default.nix @@ -0,0 +1,65 @@ +# This module defines an on-demand minecraft server service which turns off the +# server when it's not being used. +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib) mkIf mkEnableOption; + + cfg = config.services.forsvarsarper; +in { + options.services.forsvarsarper.enable = mkEnableOption "daily scan for tests"; + + config = mkIf cfg.enable { + # Create a user to run the server under. + users.users.forsvarsarper = { + description = "Runs daily scan for tests"; + group = "forsvarsarper"; + isSystemUser = true; + home = "/srv/forsvarsarper"; + createHome = true; + }; + users.groups.forsvarsarper = {}; + + age.secrets.forsvarsarper-env = { + file = ../../../secrets/forsvarsarper.env.age; + owner = config.users.users.forsvarsarper.name; + group = config.users.users.forsvarsarper.group; + mode = "0440"; + }; + + # Create a service which simply runs script. This will be invoked by our timer. + systemd.services.forsvarsarper = { + serviceConfig = { + # We only want to run this once every time the timer triggers it. + Type = "oneshot"; + # Run as the user we created above. + User = "forsvarsarper"; + Group = "forsvarsarper"; + WorkingDirectory = config.users.users.forsvarsarper.home; + }; + script = let + python3' = pkgs.python3.withPackages (ps: [ps.requests]); + in '' + # Load the secret environment variables. + export $(grep -v '^#' ${config.age.secrets.forsvarsarper-env.path} | xargs) + # Kick off. + exec ${python3'}/bin/python3 ${./script.py} + ''; + }; + + # Create a timer to activate our oneshot service. + systemd.timers.forsvarsarper = { + wantedBy = ["timers.target"]; + partOf = ["forsvarsarper.service"]; + after = ["network-online.target"]; + wants = ["network-online.target"]; + timerConfig = { + OnCalendar = "*-*-* 8:00:00"; + Unit = "forsvarsarper.service"; + }; + }; + }; +} diff --git a/hosts/ahmed/forsvarsarper/script.py b/hosts/ahmed/forsvarsarper/script.py new file mode 100644 index 0000000..7f12508 --- /dev/null +++ b/hosts/ahmed/forsvarsarper/script.py @@ -0,0 +1,28 @@ +import requests +import os + +URL = "https://karriere.forsvaret.dk/varnepligt/varnepligten/cybervarnepligt/" +TARGET_PHRASE = "Der er på nuværende tidspunkt ikke planlagt nogen afprøvninger." + +try: + response = requests.get(URL); + print(f"Forespørgsel til {URL} gav status {response.status_code}") +except: + message = "nejj den er ødelagt" +else: + if TARGET_PHRASE in response.text: + message = "der er stadig ikke planlagt nogle afprøvninger" + else: + message = "noget har ændret sig på siden!!" + print(response.text) + +token = os.getenv("TOKEN") +data = { + "title": "forsvaret status", + "message": message, + "url": URL, +} +response = requests.post(f"https://notifications.linus.onl/api/send-notification/{token}", json=data) +print(f"Forespørgsel til at sende notifikation gav status {response.status_code}") +response.raise_for_status() + diff --git a/hosts/ahmed/git.linus.onl/about.html b/hosts/ahmed/git.linus.onl/about.html new file mode 100644 index 0000000..2d18ca4 --- /dev/null +++ b/hosts/ahmed/git.linus.onl/about.html @@ -0,0 +1,5 @@ +

Welcome! This is where i keep my public repositories.

+
+
+

idk.

+

what do i say here?

diff --git a/hosts/ahmed/git.linus.onl/default.nix b/hosts/ahmed/git.linus.onl/default.nix new file mode 100644 index 0000000..88e4f6f --- /dev/null +++ b/hosts/ahmed/git.linus.onl/default.nix @@ -0,0 +1,95 @@ +{ + lib, + config, + pkgs, + options, + metadata, + ... +}: let + inherit (lib) mkEnableOption mkOption types mkIf; + + git-shell = "${pkgs.gitMinimal}/bin/git-shell"; + + cfg = config.modules."git.linus.onl"; +in { + options.modules."git.linus.onl" = { + enable = mkEnableOption "git.linus.onl static site"; + + useACME = mkEnableOption "built-in HTTPS stuff"; + + location = mkOption { + description = "Where repositories will be stored."; + type = types.path; + default = "/srv/git"; + }; + }; + + config = mkIf cfg.enable { + # Create a user which + # See: https://git-scm.com/book/en/v2/Git-on-the-Server-Setting-Up-the-Server + users.users.git = { + description = "Git server user"; + isSystemUser = true; + group = "git"; + + # FIXME: Is serving the home-directory of a user (indirectly through CGit) a bad idea? + home = cfg.location; + createHome = false; + + # Restrict this user to Git-related activities. + # See: https://git-scm.com/docs/git-shell + shell = git-shell; + + # List of users who can ssh into this server and write to stuff. We add + # some restrictions on what users can do on the server. This works in + # tandem with the custom shell. + openssh.authorizedKeys.keys = + map (key: "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ${key}") + [ + metadata.hosts.muhammed.sshPubKey + ]; + }; + users.groups.git = {}; + + environment.shells = [git-shell]; + + # Create repo directory. It must be readable to NGINX. + # See: https://git.zx2c4.com/cgit/about/faq#why-doesnt-cgit-findshow-my-repo + system.activationScripts.create-cgit-scan-path = mkIf (cfg.location == options.modules."git.linus.onl".location.default) '' + mkdir -p ${cfg.location} + chown ${toString config.users.users.git.name} ${cfg.location} + chgrp ${toString config.users.groups.git.name} ${cfg.location} + chmod 755 ${cfg.location} + ''; + + # Public git viewer. + services.cgit."git.linus.onl" = { + enable = true; + scanPath = cfg.location; + settings = { + root-title = "Linus' public projects"; + root-desc = "hello yes this is the git server"; + root-readme = toString ./about.html; + }; + extraConfig = '' + readme=:README.md + readme=:README.rst + readme=:README.text + readme=:README.txt + readme=:readme.md + readme=:readme.rst + readme=:readme.text + readme=:readme.txt + ''; + }; + + # Register domain name. + services.cloudflare-dyndns.domains = ["git.linus.onl"]; + + # The CGit service creates the virtual host, but it does not enable ACME. + services.nginx.virtualHosts."git.linus.onl" = { + enableACME = cfg.useACME; + forceSSL = cfg.useACME; + }; + }; +} diff --git a/hosts/ahmed/hellohtml.linus.onl/default.nix b/hosts/ahmed/hellohtml.linus.onl/default.nix new file mode 100644 index 0000000..feb56ba --- /dev/null +++ b/hosts/ahmed/hellohtml.linus.onl/default.nix @@ -0,0 +1,60 @@ +{ + lib, + config, + ... +}: let + inherit (lib) mkEnableOption mkIf; + + cfg = config.modules."hellohtml.linus.onl"; +in { + options.modules."hellohtml.linus.onl" = { + enable = mkEnableOption "hellohtml.linus.onl site"; + + useACME = mkEnableOption "built-in HTTPS stuff"; + }; + + config = mkIf cfg.enable { + # Start service listening on socket /tmp/hellohtml.sock + services.hellohtml = { + enable = true; + }; + + # Register domain name. + services.cloudflare-dyndns.domains = ["hellohtml.linus.onl"]; + + # Use NGINX as reverse proxy. + services.nginx.virtualHosts."hellohtml.linus.onl" = { + enableACME = cfg.useACME; + forceSSL = cfg.useACME; + locations."/" = rec { + proxyPass = "http://localhost:8538"; + # Disable settings that might mess with the text/event-stream response of the /listen/:id endpoint. + # NOTE: These settings work in tanden with Cloudflare Proxy settings descibed here: + # https://blog.devops.dev/implementing-server-sent-events-with-fastapi-nginx-and-cloudflare-10ede1dffc18 + extraConfig = '' + location /listen/ { + # Have to duplicate this here, as this directive is not inherited. + # See: https://blog.martinfjordvald.com/understanding-the-nginx-configuration-inheritance-model/ + # See: https://serverfault.com/q/1082562 + proxy_pass ${proxyPass}; + # Disable connection header. + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection + # See: https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives + proxy_set_header Connection \'\'; + # Disable buffering. This is crucial for SSE to ensure that + # messages are sent immediately without waiting for a buffer to + # fill. + proxy_buffering off; + # Disable caching to ensure that all messages are sent and received + # in real-time without being cached by the proxy. + proxy_cache off; + # Set a long timeout for reading from the proxy to prevent the + # connection from timing out. You may need to adjust this value + # based on your specific requirements. + proxy_read_timeout 86400; + } + ''; + }; + }; + }; +} diff --git a/hosts/ahmed/linus.onl/default.nix b/hosts/ahmed/linus.onl/default.nix new file mode 100644 index 0000000..52703fe --- /dev/null +++ b/hosts/ahmed/linus.onl/default.nix @@ -0,0 +1,100 @@ +{ + pkgs, + lib, + config, + ... +}: let + inherit (lib) mkEnableOption mkOption types mkIf optional; + + domain = "linus.onl"; + + cfg = config.modules."${domain}"; +in { + options.modules."${domain}" = { + enable = mkEnableOption "${domain} static site"; + + useACME = mkEnableOption "built-in HTTPS stuff"; + }; + + config = mkIf cfg.enable { + # Create a user to run the build script under. + users.users."${domain}-builder" = { + description = "builds ${domain}"; + group = "${domain}-builder"; + isSystemUser = true; + }; + users.groups."${domain}-builder" = {}; + + # Create the output directory. + system.activationScripts."${domain}-create-www" = lib.stringAfter ["var"] '' + mkdir -p /var/www/${domain} + chown ${domain}-builder /var/www/${domain} + chgrp ${domain}-builder /var/www/${domain} + chmod 0755 /var/www/${domain} + ''; + + # Create a systemd service which rebuild the site regularly. + # + # This can't be done using Nix because the site relies on the git build and + # there are some inherent difficulties with including .git/ in the + # inputSource for derivations. + # + # See: https://github.com/NixOS/nix/issues/6900 + # See: https://github.com/NixOS/nixpkgs/issues/8567 + # + # TODO: Integrate rebuilding with GitHub webhooks to rebuild on push. + systemd.services."${domain}-source" = { + description = "generate https://${domain} source"; + + serviceConfig = { + Type = "oneshot"; + User = "${domain}-builder"; + Group = "${domain}-builder"; + }; + startAt = "*-*-* *:00/5:00"; + + path = with pkgs; [ + git + rsync + coreutils-full + tcl-8_5 + gnumake + ]; + environment.TCLLIBPATH = "$TCLLIBPATH ${pkgs.tcl-cmark}/lib/tclcmark1.0"; + script = '' + set -ex + tmpdir="$(mktemp -d -t linus.onl-source.XXXXXXXXXXXX)" + cd "$tmpdir" + trap 'rm -rf $tmpdir' EXIT + # TODO: Only do minimal possible cloning + git clone https://github.com/linnnus/${domain} . + make _build + rsync --archive --delete _build/ /var/www/${domain} + ''; + + # TODO: Harden service + + # Network must be online for us to check. + after = ["network-online.target"]; + requires = ["network-online.target"]; + + # We must generate some files for NGINX to serve, so this should be run + # before NGINX. + before = ["nginx.service"]; + wantedBy = ["nginx.service"]; + }; + + # Register domain name with ddns. + services.cloudflare-dyndns.domains = [domain]; + + # Register virtual host. + services.nginx = { + virtualHosts."${domain}" = { + # NOTE: 'forceSSL' will cause an infite loop, if the cloudflare proxy does NOT connect over HTTPS. + enableACME = cfg.useACME; + forceSSL = cfg.useACME; + root = "/var/www/${domain}"; + }; + }; + }; +} diff --git a/hosts/ahmed/nofitications.linus.onl/default.nix b/hosts/ahmed/nofitications.linus.onl/default.nix new file mode 100644 index 0000000..c050ef4 --- /dev/null +++ b/hosts/ahmed/nofitications.linus.onl/default.nix @@ -0,0 +1,34 @@ +{ + lib, + config, + ... +}: let + inherit (lib) mkEnableOption mkIf; + + cfg = config.modules."notifications.linus.onl"; +in { + options.modules."notifications.linus.onl" = { + enable = mkEnableOption "notifications.linus.onl static site"; + + useACME = mkEnableOption "built-in HTTPS stuff"; + }; + + config = mkIf cfg.enable { + services.push-notification-api = { + enable = true; + }; + + # Register domain name. + services.cloudflare-dyndns.domains = ["notifications.linus.onl"]; + + # Use NGINX as reverse proxy. + services.nginx.virtualHosts."notifications.linus.onl" = { + enableACME = cfg.useACME; + forceSSL = cfg.useACME; + locations."/" = { + recommendedProxySettings = true; + proxyPass = "http://unix:/run/push-notification-api.sock"; + }; + }; + }; +} diff --git a/modules/nixos/cloudflare-proxy/default.nix b/modules/nixos/cloudflare-proxy/default.nix deleted file mode 100644 index 657722d..0000000 --- a/modules/nixos/cloudflare-proxy/default.nix +++ /dev/null @@ -1,109 +0,0 @@ -# This module adds some extra configuration useful when running behid a Cloudflare Proxy. -# -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib.options) mkEnableOption mkOption; - inherit (lib.modules) mkIf; - inherit (lib.types) listOf nonEmptyStr port; - - # TODO: What happens when these get out of date??? Huh??? You little pissbaby - fileToList = x: lib.strings.splitString "\n" (builtins.readFile x); - cfipv4 = fileToList (pkgs.fetchurl { - url = "https://www.cloudflare.com/ips-v4"; - hash = "sha256-8Cxtg7wBqwroV3Fg4DbXAMdFU1m84FTfiE5dfZ5Onns="; - }); - cfipv6 = fileToList (pkgs.fetchurl { - url = "https://www.cloudflare.com/ips-v6"; - hash = "sha256-np054+g7rQDE3sr9U8Y/piAp89ldto3pN9K+KCNMoKk="; - }); - - cfg = config.modules.cloudflare-proxy; -in { - options.modules.cloudflare-proxy = { - enable = mkEnableOption "Cloudflare proxy IP extraction for NGINX"; - - firewall = { - IPv4Whitelist = mkOption { - description = "List of IPv4 addresses (or ranges) added to the whitelist."; - type = listOf nonEmptyStr; - default = []; - }; - - IPv6Whitelist = mkOption { - description = "List of IPv6 addresses (or ranges) added to the whitelist."; - type = listOf nonEmptyStr; - default = []; - }; - }; - }; - - config = mkIf cfg.enable { - # Teach NGINX how to extract the proxied IP from proxied requests. - # - # See: https://nixos.wiki/wiki/Nginx#Using_realIP_when_behind_CloudFlare_or_other_CDN - services.nginx.commonHttpConfig = let - realIpsFromList = lib.strings.concatMapStringsSep "\n" (x: "set_real_ip_from ${x};"); - in '' - ${realIpsFromList cfipv4} - ${realIpsFromList cfipv6} - real_ip_header CF-Connecting-IP; - ''; - - # Block non-Cloudflare IP addresses. - networking.firewall = let - chain = "cloudflare-whitelist"; - in { - extraCommands = let - allow-interface = lib.strings.concatMapStringsSep "\n" (i: ''ip46tables --append ${chain} --in-interface ${i} --jump RETURN''); - allow-ip = cmd: lib.strings.concatMapStringsSep "\n" (r: ''${cmd} --append ${chain} --source ${r} --jump RETURN''); - in '' - # Flush the old firewall rules. This behavior mirrors the default firewall service. - # See: https://github.com/NixOS/nixpkgs/blob/ac911bf685eecc17c2df5b21bdf32678b9f88c92/nixos/modules/services/networking/firewall-iptables.nix#L59-L66 - # TEMP: Removed 2>/dev/null - ip46tables --delete INPUT --protocol tcp --destination-port 80 --syn --jump ${chain} || true - ip46tables --delete INPUT --protocol tcp --destination-port 443 --syn --jump ${chain} || true - ip46tables --flush ${chain} || true - ip46tables --delete-chain ${chain} || true - - # Create a chain that only allows whitelisted IPs through. - ip46tables --new-chain ${chain} - - # Allow trusted interfaces through. - ${allow-interface config.networking.firewall.trustedInterfaces} - - # Allow local whitelisted IPs through - ${allow-ip "iptables" cfg.firewall.IPv4Whitelist} - ${allow-ip "ip6tables" cfg.firewall.IPv6Whitelist} - - # Allow Cloudflare's IP ranges through. - ${allow-ip "iptables" cfipv4} - ${allow-ip "ip6tables" cfipv6} - - # Everything else is dropped. - # - # TODO: I would like to use `nixos-fw-log-refuse` here, but I keep - # running into weird issues when reloading the firewall. - # Something about the table not being deleted properly. - ip46tables --append ${chain} --jump DROP - - # Inject our chain as the first check in INPUT (before nixos-fw). - # We want to capture any new incomming TCP connections. - ip46tables --insert INPUT 1 --protocol tcp --destination-port 80 --syn --jump ${chain} - ip46tables --insert INPUT 1 --protocol tcp --destination-port 443 --syn --jump ${chain} - ''; - extraStopCommands = '' - # Clean up added rulesets (${chain}). This mirrors the behavior of the - # default firewall at the time of writing. - # - # See: https://github.com/NixOS/nixpkgs/blob/ac911bf685eecc17c2df5b21bdf32678b9f88c92/nixos/modules/services/networking/firewall-iptables.nix#L218-L219 - # TEMP: Removed 2>/dev/null - ip46tables --delete INPUT --protocol tcp --destination-port 80 --syn --jump ${chain} || true - ip46tables --delete INPUT --protocol tcp --destination-port 443 --syn --jump ${chain} || true - ''; - }; - }; -} diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix deleted file mode 100644 index 1393627..0000000 --- a/modules/nixos/default.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - # These components are - general = { - on-demand-minecraft = import ./on-demand-minecraft; - cloudflare-proxy = import ./cloudflare-proxy; - disable-screen = import ./disable-screen; - hellohtml = import ./hellohtml; - }; - - personal = { - duksebot = import ./duksebot; - graphics = import ./graphics; - "linus.onl" = import ./linus.onl; - "notifications.linus.onl" = import ./nofitications.linus.onl; - "git.linus.onl" = import ./git.linus.onl; - "hellohtml.linus.onl" = import ./hellohtml.linus.onl; - forsvarsarper = import ./forsvarsarper; - }; -} diff --git a/modules/nixos/disable-screen/default.nix b/modules/nixos/disable-screen/default.nix deleted file mode 100644 index 638437a..0000000 --- a/modules/nixos/disable-screen/default.nix +++ /dev/null @@ -1,61 +0,0 @@ -# This file defines some configuration options which disable the screen. This -# is only relevant because this host is an old laptop running as a server. -{ - lib, - config, - ... -}: let - inherit (lib) mkEnableOption mkOption mkIf types; - - cfg = config.services.disable-screen; -in { - options.services.disable-screen = { - enable = mkEnableOption "disable screen"; - - device-path = mkOption { - description = "Path to the device in the `/sys` file system."; - type = types.str; - example = "/sys/class/backlight/intel_backlight"; - }; - - device-unit = mkOption { - description = "The systemd device unit that corresponds to the device speciefied in `device-path`."; - type = types.str; - example = "sys-devices-pci...-intel_backligt.device"; - }; - }; - - config = mkIf cfg.enable { - # Disable sleep on lid close. - services.logind = let - lidSwitchAction = "ignore"; - in { - lidSwitchExternalPower = lidSwitchAction; - lidSwitchDocked = lidSwitchAction; - lidSwitch = lidSwitchAction; - }; - - # Don't store screen brightness between boots. We always want to turn off the - # screen. - # - # See: https://wiki.archlinux.org/title/backlight#Save_and_restore_functionality - # See: https://github.com/NixOS/nixpkgs/blob/990398921f677615c0732d704857484b84c6c888/nixos/modules/system/boot/systemd.nix#L97-L101 - systemd.suppressedSystemUnits = ["systemd-backlight@.service"]; - - # FIXME: Figure out how to enable screen when on-device debugging is necessary. - # Create a new service which turns off the display on boot. - # - # See: https://nixos.wiki/wiki/Backlight#.2Fsys.2Fclass.2Fbacklight.2F... - # See: https://superuser.com/questions/851846/how-to-write-a-systemd-service-that-depends-on-a-device-being-present - systemd.services.disable-screen = { - requires = [cfg.device-unit]; - after = [cfg.device-unit]; - wantedBy = [cfg.device-unit]; - - serviceConfig.Type = "oneshot"; - script = '' - tee ${cfg.device-path}/brightness <<<0 - ''; - }; - }; -} diff --git a/modules/nixos/duksebot/default.nix b/modules/nixos/duksebot/default.nix deleted file mode 100644 index 4c10cd8..0000000 --- a/modules/nixos/duksebot/default.nix +++ /dev/null @@ -1,72 +0,0 @@ -# This module defines an on-demand minecraft server service which turns off the -# server when it's not being used. -{ - config, - lib, - pkgs, - modulesPath, - ... -}: let - inherit (lib) mkIf mkOption mkEnableOption types; - - cfg = config.services.duksebot; -in { - options.services.duksebot = { - enable = mkEnableOption "duksebot daily reminder"; - - package = mkOption { - description = "What package to use"; - default = pkgs.duksebot; - type = types.package; - }; - }; - - config = mkIf cfg.enable { - # Create a user to run the server under. - users.users.duksebot = { - description = "Runs daily dukse reminder"; - group = "duksebot"; - isSystemUser = true; - home = "/srv/duksebot"; - createHome = true; - }; - users.groups.duksebot = {}; - - age.secrets.duksebot-env = { - file = ../../../secrets/duksebot.env.age; - owner = config.users.users.duksebot.name; - group = config.users.users.duksebot.group; - mode = "0440"; - }; - - # Create a service which simply runs script. This will be invoked by our timer. - systemd.services.duksebot = { - serviceConfig = { - # We only want to run this once every time the timer triggers it. - Type = "oneshot"; - # Run as the user we created above. - User = "duksebot"; - Group = "duksebot"; - WorkingDirectory = config.users.users.duksebot.home; - }; - script = '' - # Load the secret environment variables. - export $(grep -v '^#' ${config.age.secrets.duksebot-env.path} | xargs) - # Kick off. - exec "${cfg.package}"/bin/duksebot - ''; - }; - - # Create a timer to activate our oneshot service. - systemd.timers.duksebot = { - wantedBy = ["timers.target"]; - partOf = ["duksebot.service"]; - after = ["network-online.target"]; - wants = ["network-online.target"]; - timerConfig = { - OnCalendar = "*-*-* 7:00:00"; - Unit = "duksebot.service"; - }; - }; - }; -} diff --git a/modules/nixos/forsvarsarper/default.nix b/modules/nixos/forsvarsarper/default.nix deleted file mode 100644 index 7052f04..0000000 --- a/modules/nixos/forsvarsarper/default.nix +++ /dev/null @@ -1,65 +0,0 @@ -# This module defines an on-demand minecraft server service which turns off the -# server when it's not being used. -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.services.forsvarsarper; -in { - options.services.forsvarsarper.enable = mkEnableOption "daily scan for tests"; - - config = mkIf cfg.enable { - # Create a user to run the server under. - users.users.forsvarsarper = { - description = "Runs daily scan for tests"; - group = "forsvarsarper"; - isSystemUser = true; - home = "/srv/forsvarsarper"; - createHome = true; - }; - users.groups.forsvarsarper = {}; - - age.secrets.forsvarsarper-env = { - file = ../../../secrets/forsvarsarper.env.age; - owner = config.users.users.forsvarsarper.name; - group = config.users.users.forsvarsarper.group; - mode = "0440"; - }; - - # Create a service which simply runs script. This will be invoked by our timer. - systemd.services.forsvarsarper = { - serviceConfig = { - # We only want to run this once every time the timer triggers it. - Type = "oneshot"; - # Run as the user we created above. - User = "forsvarsarper"; - Group = "forsvarsarper"; - WorkingDirectory = config.users.users.forsvarsarper.home; - }; - script = let - python3' = pkgs.python3.withPackages (ps: [ps.requests]); - in '' - # Load the secret environment variables. - export $(grep -v '^#' ${config.age.secrets.forsvarsarper-env.path} | xargs) - # Kick off. - exec ${python3'}/bin/python3 ${./script.py} - ''; - }; - - # Create a timer to activate our oneshot service. - systemd.timers.forsvarsarper = { - wantedBy = ["timers.target"]; - partOf = ["forsvarsarper.service"]; - after = ["network-online.target"]; - wants = ["network-online.target"]; - timerConfig = { - OnCalendar = "*-*-* 8:00:00"; - Unit = "forsvarsarper.service"; - }; - }; - }; -} diff --git a/modules/nixos/forsvarsarper/script.py b/modules/nixos/forsvarsarper/script.py deleted file mode 100644 index 7f12508..0000000 --- a/modules/nixos/forsvarsarper/script.py +++ /dev/null @@ -1,28 +0,0 @@ -import requests -import os - -URL = "https://karriere.forsvaret.dk/varnepligt/varnepligten/cybervarnepligt/" -TARGET_PHRASE = "Der er på nuværende tidspunkt ikke planlagt nogen afprøvninger." - -try: - response = requests.get(URL); - print(f"Forespørgsel til {URL} gav status {response.status_code}") -except: - message = "nejj den er ødelagt" -else: - if TARGET_PHRASE in response.text: - message = "der er stadig ikke planlagt nogle afprøvninger" - else: - message = "noget har ændret sig på siden!!" - print(response.text) - -token = os.getenv("TOKEN") -data = { - "title": "forsvaret status", - "message": message, - "url": URL, -} -response = requests.post(f"https://notifications.linus.onl/api/send-notification/{token}", json=data) -print(f"Forespørgsel til at sende notifikation gav status {response.status_code}") -response.raise_for_status() - diff --git a/modules/nixos/git.linus.onl/about.html b/modules/nixos/git.linus.onl/about.html deleted file mode 100644 index 2d18ca4..0000000 --- a/modules/nixos/git.linus.onl/about.html +++ /dev/null @@ -1,5 +0,0 @@ -

Welcome! This is where i keep my public repositories.

-
-
-

idk.

-

what do i say here?

diff --git a/modules/nixos/git.linus.onl/default.nix b/modules/nixos/git.linus.onl/default.nix deleted file mode 100644 index 88e4f6f..0000000 --- a/modules/nixos/git.linus.onl/default.nix +++ /dev/null @@ -1,95 +0,0 @@ -{ - lib, - config, - pkgs, - options, - metadata, - ... -}: let - inherit (lib) mkEnableOption mkOption types mkIf; - - git-shell = "${pkgs.gitMinimal}/bin/git-shell"; - - cfg = config.modules."git.linus.onl"; -in { - options.modules."git.linus.onl" = { - enable = mkEnableOption "git.linus.onl static site"; - - useACME = mkEnableOption "built-in HTTPS stuff"; - - location = mkOption { - description = "Where repositories will be stored."; - type = types.path; - default = "/srv/git"; - }; - }; - - config = mkIf cfg.enable { - # Create a user which - # See: https://git-scm.com/book/en/v2/Git-on-the-Server-Setting-Up-the-Server - users.users.git = { - description = "Git server user"; - isSystemUser = true; - group = "git"; - - # FIXME: Is serving the home-directory of a user (indirectly through CGit) a bad idea? - home = cfg.location; - createHome = false; - - # Restrict this user to Git-related activities. - # See: https://git-scm.com/docs/git-shell - shell = git-shell; - - # List of users who can ssh into this server and write to stuff. We add - # some restrictions on what users can do on the server. This works in - # tandem with the custom shell. - openssh.authorizedKeys.keys = - map (key: "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ${key}") - [ - metadata.hosts.muhammed.sshPubKey - ]; - }; - users.groups.git = {}; - - environment.shells = [git-shell]; - - # Create repo directory. It must be readable to NGINX. - # See: https://git.zx2c4.com/cgit/about/faq#why-doesnt-cgit-findshow-my-repo - system.activationScripts.create-cgit-scan-path = mkIf (cfg.location == options.modules."git.linus.onl".location.default) '' - mkdir -p ${cfg.location} - chown ${toString config.users.users.git.name} ${cfg.location} - chgrp ${toString config.users.groups.git.name} ${cfg.location} - chmod 755 ${cfg.location} - ''; - - # Public git viewer. - services.cgit."git.linus.onl" = { - enable = true; - scanPath = cfg.location; - settings = { - root-title = "Linus' public projects"; - root-desc = "hello yes this is the git server"; - root-readme = toString ./about.html; - }; - extraConfig = '' - readme=:README.md - readme=:README.rst - readme=:README.text - readme=:README.txt - readme=:readme.md - readme=:readme.rst - readme=:readme.text - readme=:readme.txt - ''; - }; - - # Register domain name. - services.cloudflare-dyndns.domains = ["git.linus.onl"]; - - # The CGit service creates the virtual host, but it does not enable ACME. - services.nginx.virtualHosts."git.linus.onl" = { - enableACME = cfg.useACME; - forceSSL = cfg.useACME; - }; - }; -} diff --git a/modules/nixos/graphics/default.nix b/modules/nixos/graphics/default.nix deleted file mode 100644 index f54d043..0000000 --- a/modules/nixos/graphics/default.nix +++ /dev/null @@ -1,37 +0,0 @@ -# This module configures a basic graphical environment. I use this sometimes for -# ahmed when muhammed is being repaired. -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.modules.graphics; -in { - options.modules.graphics.enable = mkEnableOption "basic graphical environment"; - - config = mkIf cfg.enable { - services.xserver.enable = true; - - # Match console keyboard layout but swap capslock and escape. - # TODO: Create a custom keymap with esc/capslock swap so console can use it. - services.xserver.layout = config.console.keyMap; - services.xserver.xkbOptions = "caps:swapescape"; - - # Enable touchpad support. - services.xserver.libinput.enable = true; - - services.xserver.windowManager.dwm.enable = true; - - # Enable sound. - sound.enable = true; - hardware.pulseaudio.enable = true; - - environment.systemPackages = with pkgs; [ - st # suckless terminal - dwm is pretty sucky without this - dmenu # application launcher - ]; - }; -} diff --git a/modules/nixos/hellohtml.linus.onl/default.nix b/modules/nixos/hellohtml.linus.onl/default.nix deleted file mode 100644 index feb56ba..0000000 --- a/modules/nixos/hellohtml.linus.onl/default.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ - lib, - config, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.modules."hellohtml.linus.onl"; -in { - options.modules."hellohtml.linus.onl" = { - enable = mkEnableOption "hellohtml.linus.onl site"; - - useACME = mkEnableOption "built-in HTTPS stuff"; - }; - - config = mkIf cfg.enable { - # Start service listening on socket /tmp/hellohtml.sock - services.hellohtml = { - enable = true; - }; - - # Register domain name. - services.cloudflare-dyndns.domains = ["hellohtml.linus.onl"]; - - # Use NGINX as reverse proxy. - services.nginx.virtualHosts."hellohtml.linus.onl" = { - enableACME = cfg.useACME; - forceSSL = cfg.useACME; - locations."/" = rec { - proxyPass = "http://localhost:8538"; - # Disable settings that might mess with the text/event-stream response of the /listen/:id endpoint. - # NOTE: These settings work in tanden with Cloudflare Proxy settings descibed here: - # https://blog.devops.dev/implementing-server-sent-events-with-fastapi-nginx-and-cloudflare-10ede1dffc18 - extraConfig = '' - location /listen/ { - # Have to duplicate this here, as this directive is not inherited. - # See: https://blog.martinfjordvald.com/understanding-the-nginx-configuration-inheritance-model/ - # See: https://serverfault.com/q/1082562 - proxy_pass ${proxyPass}; - # Disable connection header. - # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection - # See: https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives - proxy_set_header Connection \'\'; - # Disable buffering. This is crucial for SSE to ensure that - # messages are sent immediately without waiting for a buffer to - # fill. - proxy_buffering off; - # Disable caching to ensure that all messages are sent and received - # in real-time without being cached by the proxy. - proxy_cache off; - # Set a long timeout for reading from the proxy to prevent the - # connection from timing out. You may need to adjust this value - # based on your specific requirements. - proxy_read_timeout 86400; - } - ''; - }; - }; - }; -} diff --git a/modules/nixos/hellohtml/default.nix b/modules/nixos/hellohtml/default.nix deleted file mode 100644 index cc15779..0000000 --- a/modules/nixos/hellohtml/default.nix +++ /dev/null @@ -1,121 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -# FIXME: It is wasteful to always run the service. We should run on-demand instead. -# This is usually achieved using SystemD sockets [4] but we are blocked on missing -# features in Deno [1, 5]. -# -# We have to be able to listen on a socket that's already been created by binding -# an open file descriptor (or listening on stdin) [3]. This is not possible in Deno -# as it is now [1, 2, 6]. -# -# Once it becomes a possibility, we should mirror the way push-notification-api works -# as of b9ed407 [8, 7]. -# -# [1]: https://github.com/denoland/deno/issues/6529 -# [2]: https://github.com/denoland/deno/blob/1dd1aba2448c6c8a5a0370c4066a68aca06b859b/ext/net/ops_unix.rs#L207C34-L207C34 -# [3]: https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#Description:~:text=Note%20that%20the,the%20service%20file). -# [4]: https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#Description:~:text=Socket%20units%20may%20be%20used%20to%20implement%20on%2Ddemand%20starting%20of%20services%2C%20as%20well%20as%20parallelized%20starting%20of%20services.%20See%20the%20blog%20stories%20linked%20at%20the%20end%20for%20an%20introduction. -# [5]: https://github.com/denoland/deno/issues/14214 -# [6]: https://github.com/tokio-rs/tokio/issues/5678 -# [7]: https://github.com/benoitc/gunicorn/blob/660fd8d850f9424d5adcd50065e6060832a200d4/gunicorn/arbiter.py#L142-L155 -# [8]: https://github.com/linnnus/push-notification-api/tree/b9ed4071a4500a26b3b348a7f5fbc549e9694562 -let - cfg = config.services.hellohtml; -in { - options.services.hellohtml = { - enable = lib.mkEnableOption "hellohtml service"; - - port = lib.mkOption { - description = "The port where hellohtml should listen."; - type = lib.types.port; - default = 8538; - }; - }; - - config = lib.mkIf cfg.enable { - # Create a user for running service. - users.users.hellohtml = { - group = "hellohtml"; - description = "Runs hellohtml service"; - isSystemUser = true; - home = "/srv/hellohtml"; - createHome = true; # Store DB here. - }; - users.groups.hellohtml = {}; - - # Create hellohtml service. - systemd.services.hellohtml = { - description = "HelloHTML server!!!"; - - wantedBy = ["multi-user.target"]; - after = ["network.target"]; - - serviceConfig = let - src = pkgs.fetchFromGitHub { - owner = "linnnus"; - repo = "hellohtml"; - rev = "97f00500712d8551d7bbf497ec442083c63384d0"; - hash = "sha256-6nbL2B26dc83F2gSLXadyfS8etuPhhlFy9ivG5l6Tog"; - }; - - hellohtml-vendor = pkgs.stdenv.mkDerivation { - name = "hellohtml-vendor"; - nativeBuildInputs = [pkgs.unstable.deno]; - inherit src; - buildCommand = '' - # Deno wants to create cache directories. - HOME="$(mktemp -d)" - # Thought this wasn't necessary??? - cd $src - # Build directory containing offline deps + import map. - deno vendor --output=$out ./src/server.ts - ''; - outputHashAlgo = "sha256"; - outputHashMode = "recursive"; - outputHash = "sha256-0TGLkEvJaBpI7IlTyuYRzA20Bw/TMSMz3q8wm5oPsBM"; - }; - - hellohtml-drv = pkgs.writeShellScript "hellohtml" '' - export HELLOHTML_DB_PATH="${config.users.users.hellohtml.home}"/hello.db - export HELLOHTML_PORT=${toString cfg.port} - export HELLOHTML_BASE_DIR="${src}" - - ${pkgs.unstable.deno}/bin/deno run \ - --allow-read=$HELLOHTML_BASE_DIR,$HELLOHTML_DB_PATH,. \ - --allow-write=$HELLOHTML_DB_PATH \ - --allow-net=0.0.0.0:$HELLOHTML_PORT \ - --allow-env \ - --no-prompt \ - --unstable-kv \ - --import-map=${hellohtml-vendor}/import_map.json \ - --no-remote \ - ${src}/src/server.ts - ''; - in { - Type = "simple"; - User = config.users.users.hellohtml.name; - Group = config.users.users.hellohtml.group; - ExecStart = "${hellohtml-drv}"; - - # Harden service - # NoNewPrivileges = "yes"; - # PrivateTmp = "yes"; - # PrivateDevices = "yes"; - # DevicePolicy = "closed"; - # ProtectControlGroups = "yes"; - # ProtectKernelModules = "yes"; - # ProtectKernelTunables = "yes"; - # RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; - # RestrictNamespaces = "yes"; - # RestrictRealtime = "yes"; - # RestrictSUIDSGID = "yes"; - # MemoryDenyWriteExecute = "yes"; - # LockPersonality = "yes"; - }; - }; - }; -} diff --git a/modules/nixos/linus.onl/default.nix b/modules/nixos/linus.onl/default.nix deleted file mode 100644 index 52703fe..0000000 --- a/modules/nixos/linus.onl/default.nix +++ /dev/null @@ -1,100 +0,0 @@ -{ - pkgs, - lib, - config, - ... -}: let - inherit (lib) mkEnableOption mkOption types mkIf optional; - - domain = "linus.onl"; - - cfg = config.modules."${domain}"; -in { - options.modules."${domain}" = { - enable = mkEnableOption "${domain} static site"; - - useACME = mkEnableOption "built-in HTTPS stuff"; - }; - - config = mkIf cfg.enable { - # Create a user to run the build script under. - users.users."${domain}-builder" = { - description = "builds ${domain}"; - group = "${domain}-builder"; - isSystemUser = true; - }; - users.groups."${domain}-builder" = {}; - - # Create the output directory. - system.activationScripts."${domain}-create-www" = lib.stringAfter ["var"] '' - mkdir -p /var/www/${domain} - chown ${domain}-builder /var/www/${domain} - chgrp ${domain}-builder /var/www/${domain} - chmod 0755 /var/www/${domain} - ''; - - # Create a systemd service which rebuild the site regularly. - # - # This can't be done using Nix because the site relies on the git build and - # there are some inherent difficulties with including .git/ in the - # inputSource for derivations. - # - # See: https://github.com/NixOS/nix/issues/6900 - # See: https://github.com/NixOS/nixpkgs/issues/8567 - # - # TODO: Integrate rebuilding with GitHub webhooks to rebuild on push. - systemd.services."${domain}-source" = { - description = "generate https://${domain} source"; - - serviceConfig = { - Type = "oneshot"; - User = "${domain}-builder"; - Group = "${domain}-builder"; - }; - startAt = "*-*-* *:00/5:00"; - - path = with pkgs; [ - git - rsync - coreutils-full - tcl-8_5 - gnumake - ]; - environment.TCLLIBPATH = "$TCLLIBPATH ${pkgs.tcl-cmark}/lib/tclcmark1.0"; - script = '' - set -ex - tmpdir="$(mktemp -d -t linus.onl-source.XXXXXXXXXXXX)" - cd "$tmpdir" - trap 'rm -rf $tmpdir' EXIT - # TODO: Only do minimal possible cloning - git clone https://github.com/linnnus/${domain} . - make _build - rsync --archive --delete _build/ /var/www/${domain} - ''; - - # TODO: Harden service - - # Network must be online for us to check. - after = ["network-online.target"]; - requires = ["network-online.target"]; - - # We must generate some files for NGINX to serve, so this should be run - # before NGINX. - before = ["nginx.service"]; - wantedBy = ["nginx.service"]; - }; - - # Register domain name with ddns. - services.cloudflare-dyndns.domains = [domain]; - - # Register virtual host. - services.nginx = { - virtualHosts."${domain}" = { - # NOTE: 'forceSSL' will cause an infite loop, if the cloudflare proxy does NOT connect over HTTPS. - enableACME = cfg.useACME; - forceSSL = cfg.useACME; - root = "/var/www/${domain}"; - }; - }; - }; -} diff --git a/modules/nixos/nofitications.linus.onl/default.nix b/modules/nixos/nofitications.linus.onl/default.nix deleted file mode 100644 index c050ef4..0000000 --- a/modules/nixos/nofitications.linus.onl/default.nix +++ /dev/null @@ -1,34 +0,0 @@ -{ - lib, - config, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.modules."notifications.linus.onl"; -in { - options.modules."notifications.linus.onl" = { - enable = mkEnableOption "notifications.linus.onl static site"; - - useACME = mkEnableOption "built-in HTTPS stuff"; - }; - - config = mkIf cfg.enable { - services.push-notification-api = { - enable = true; - }; - - # Register domain name. - services.cloudflare-dyndns.domains = ["notifications.linus.onl"]; - - # Use NGINX as reverse proxy. - services.nginx.virtualHosts."notifications.linus.onl" = { - enableACME = cfg.useACME; - forceSSL = cfg.useACME; - locations."/" = { - recommendedProxySettings = true; - proxyPass = "http://unix:/run/push-notification-api.sock"; - }; - }; - }; -} diff --git a/modules/nixos/on-demand-minecraft/default.nix b/modules/nixos/on-demand-minecraft/default.nix deleted file mode 100644 index 3ccbec9..0000000 --- a/modules/nixos/on-demand-minecraft/default.nix +++ /dev/null @@ -1,355 +0,0 @@ -# This module defines an on-demand minecraft server service which turns off the -# server when it's not being used. -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib) mkIf mkOption mkEnableOption types; - - cfg = config.services.on-demand-minecraft; -in { - options.services.on-demand-minecraft = { - enable = mkEnableOption "local minecraft server"; - - eula = mkOption { - description = '' - Whether you agree to [Mojangs EULA](https://account.mojang.com/documents/minecraft_eula). - This option must be set to `true` to run a Minecraft™️ server (??). - ''; - type = types.bool; - default = false; - }; - - frequency-check-players = mkOption { - description = '' - How often to check the number of players using the server. If - no players are using the server, it is shut down. - - This should be a valid value for systemd's `onCalendar` - property. - ''; - type = types.nonEmptyStr; - default = "*-*-* *:*:0/20"; - }; - - minimum-server-lifetime = mkOption { - description = '' - Minimum required time to pass from the server is started - before it is allowed to be killed. This should ensure the - server has time to start up before it is killed. - - The option is specified as a number of seconds. - ''; - type = types.ints.positive; - default = 300; - }; - - internal-port = mkOption { - description = '' - The internal port which the minecraft server will listen to. - This port does not need to be exposed to the network. - ''; - type = types.port; - default = cfg.external-port + 1; - }; - - external-port = mkOption { - description = '' - The external port of the socket which is forwarded to the - Minecraft server. This is the one users will connect to. You - will need to add it to `networking.firewall.allowedTCPPorts` - to open it in the firewall. - - You may also have to set up port forwarding if you want to - play with friends who are not on the same LAN. - ''; - type = types.port; - default = 25565; - }; - - openFirewall = mkOption { - description = '' - Open holes in the firewall so clients on LAN can connect. You must - set up port forwarding if you want to play over WAN. - ''; - type = types.bool; - default = true; - }; - - package = mkOption { - description = "What Minecraft server to run."; - default = pkgs.minecraft-server; - type = types.package; - }; - - server-properties = mkOption { - description = '' - Minecraft server properties for the server.properties file. See - - for documentation on these values. Note that some options like - `server-port` will be forced on because they are required for the - server to work. - ''; - type = with types; attrsOf (oneOf [bool int str]); - default = {}; - example = lib.literalExpression '' - { - difficulty = 3; - gamemode = 1; - motd = "My NixOS server!"; - } - ''; - }; - - jvm-options = mkOption { - description = "JVM options for the Minecraft server. List of command line arguments."; - type = types.listOf lib.types.str; - default = ["-Xmx2048M" "-Xms2048M"]; - }; - }; - - config = mkIf cfg.enable { - # Create a user to run the server under. - users.users.minecrafter = { - description = "On-demand minecraft server service user"; - home = "/srv/minecrafter"; - createHome = true; - group = "minecrafter"; - isSystemUser = true; - }; - users.groups.minecrafter = {}; - - # Create an internal socket and hook it up to minecraft-server process as - # stdin. That way we can send commands to it. - systemd.sockets.minecraft-server = { - bindsTo = ["minecraft-server.service"]; - socketConfig = { - ListenFIFO = "/run/minecraft-server.stdin"; - SocketMode = "0660"; - SocketUser = "minecrafter"; - SocketGroup = "minecrafter"; - RemoveOnStop = true; - FlushPending = true; - }; - }; - - # Create a service which runs the server. - systemd.services.minecraft-server = let - server-properties = - cfg.server-properties - // { - server-port = cfg.internal-port; - }; - cfg-to-str = v: - if builtins.isBool v - then - ( - if v - then "true" - else "false" - ) - else toString v; - server-properties-file = pkgs.writeText "server.properties" ('' - # server.properties managed by NixOS configuration. - '' - + lib.concatStringsSep "\n" (lib.mapAttrsToList - (n: v: "${n}=${cfg-to-str v}") - server-properties)); - - # We don't allow eula=false anyways - eula-file = builtins.toFile "eula.txt" '' - # eula.txt managed by NixOS Configuration - eula=true - ''; - - # HACK: Each server is given its own subdirectory so - # incompatibilities between servers don't cause complaints. - start-server = pkgs.writeShellScript "minecraft-server-start" '' - # Switch to runtime directory. - export RUNTIME_DIR="${config.users.users.minecrafter.home}/${cfg.package.name}/" - ${pkgs.busybox}/bin/mkdir -p "$RUNTIME_DIR" - ${pkgs.busybox}/bin/chown minecrafter:minecrafter "$RUNTIME_DIR" - cd "$RUNTIME_DIR" - - # Set up/update environment for server - ln -sf ${eula-file} eula.txt - cp -f ${server-properties-file} server.properties - chmod u+w server.properties # Must be writable because server regenerates it. - - exec ${cfg.package}/bin/minecraft-server "$@" - ''; - - stop-server = pkgs.writeShellScript "minecraft-server-stop" '' - # Send the 'stop' command to the server. It listens for commands on stdin. - echo stop > ${config.systemd.sockets.minecraft-server.socketConfig.ListenFIFO} - # Wait for the PID of the minecraft server to disappear before - # returning, so systemd doesn't attempt to SIGKILL it. - while kill -0 "$1" 2> /dev/null; do - sleep 1s - done - ''; - in { - description = "Actually runs the Minecraft server"; - requires = ["minecraft-server.socket"]; - after = ["networking.target" "minecraft-server.socket"]; - wantedBy = []; # TEMP: Does this do anything? - - serviceConfig = { - ExecStart = "${start-server} ${lib.escapeShellArgs cfg.jvm-options}"; - ExecStop = "${stop-server} $MAINPID"; - Restart = "always"; - - User = "minecrafter"; - Group = "minecrafter"; - - StandardInput = "socket"; - StandardOutput = "journal"; - StandardError = "journal"; - - # Hardening - CapabilityBoundingSet = [""]; - DeviceAllow = [""]; - LockPersonality = true; - PrivateDevices = true; - PrivateTmp = true; - PrivateUsers = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - UMask = "0077"; - }; - }; - - # This socket listens for connections on the public port and - # triggers `listen-minecraft.service` when a connection is made. - systemd.sockets.listen-minecraft = { - enable = true; - wantedBy = ["sockets.target"]; - requires = ["network.target"]; - listenStreams = [(toString cfg.external-port)]; - }; - - # This service is triggered by a TCP connection on the public - # port. It starts hook-minecraft.service if it is not running - # already and waits for it to return (using `after`). Then it proxifies the TCP - # connection to the real (local) Minecraft port. - systemd.services.listen-minecraft = { - enable = true; - path = with pkgs; [systemd]; - requires = ["hook-minecraft.service" "listen-minecraft.socket"]; - after = ["hook-minecraft.service" "listen-minecraft.socket"]; - serviceConfig.ExecStart = '' - ${pkgs.systemd.out}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString cfg.internal-port} - ''; - }; - - # This starts Minecraft if required and waits for it to be - # available over TCP to unlock the `listen-minecraft.service` - # proxy. - systemd.services.hook-minecraft = { - enable = true; - # Add tools used by scripts to path. - path = with pkgs; [systemd libressl busybox]; - serviceConfig = let - # Start the Minecraft server and the timer regularly - # checking whether it should stop. - start-mc = pkgs.writeShellScriptBin "start-mc" '' - echo "Starting server and stop-timer..." - systemctl start minecraft-server.service - systemctl start stop-minecraft.timer - ''; - # Wait for the internal port to be accessible for max. - # 60 seconds before complaining. - wait-tcp = pkgs.writeShellScriptBin "wait-tcp" '' - echo "Waiting for server to start listening on port ${toString cfg.internal-port}..." - for i in `seq 60`; do - if ${pkgs.netcat.nc}/bin/nc -z 127.0.0.1 ${toString cfg.internal-port} >/dev/null; then - echo "Yay! ${toString cfg.internal-port} is now available. hook-minecraft is finished." - exit 0 - fi - sleep 1 - done - echo "${toString cfg.internal-port} did not become available in time." - exit 1 - ''; - in { - # First we start the server, then we wait for it to become available. - ExecStart = "${start-mc}/bin/start-mc"; - ExecStartPost = "${wait-tcp}/bin/wait-tcp"; - }; - }; - - # This timer runs the service of the same name, that checks if - # the server needs to be stopped. - systemd.timers.stop-minecraft = { - enable = true; - timerConfig = { - OnCalendar = cfg.frequency-check-players; - #Unit = "stop-minecraft.service"; - }; - }; - - systemd.services.stop-minecraft = let - # Script that returns true (exit code 0) if the server can be shut - # down. It uses mcping to get the player list. It does not continue if - # the server was started less than `minimum-server-lifetime` seconds - # ago. - # - # NOTE: `pkgs.mcping` is declared my personal monorepo. Hopefully - # everything just works out through the magic of flakes, but if you are - # getting errors like "missing attribute 'mcping'" that's probably why. - no-player-connected = pkgs.writeShellScriptBin "no-player-connected" '' - servicestartsec="$(date -d "$(systemctl show --property=ActiveEnterTimestamp minecraft-server.service | cut -d= -f2)" +%s)" - serviceelapsedsec="$(( $(date +%s) - servicestartsec))" - - if [ $serviceelapsedsec -lt ${toString cfg.minimum-server-lifetime} ]; then - echo "Server is too young to be stopped (minimum lifetime is ${toString cfg.minimum-server-lifetime}s, current is ''${serviceelapsedsec}s)" - exit 1 - fi - - PLAYERS="$(${pkgs.mcping}/bin/mcping 127.0.0.1 ${toString cfg.internal-port} | ${pkgs.jq}/bin/jq .players.online)" - echo "There are $PLAYERS active players" - if [ $PLAYERS -eq 0 ]; then - exit 0 - else - exit 1 - fi - ''; - in { - enable = true; - serviceConfig.Type = "oneshot"; - script = '' - if ${no-player-connected}/bin/no-player-connected; then - echo "Stopping minecraft server..." - systemctl stop minecraft-server.service - systemctl stop hook-minecraft.service - systemctl stop stop-minecraft.timer - fi - ''; - }; - - networking.firewall = mkIf cfg.openFirewall { - allowedUDPPorts = [cfg.external-port]; - allowedTCPPorts = [cfg.external-port]; - }; - - assertions = [ - { - assertion = cfg.eula; - message = "You must agree to Mojangs EULA to run minecraft-server. Read https://account.mojang.com/documents/minecraft_eula and set `services.minecraft-server.eula` to `true` if you agree."; - } - ]; - }; -} diff --git a/services/default.nix b/services/default.nix new file mode 100644 index 0000000..b813155 --- /dev/null +++ b/services/default.nix @@ -0,0 +1,4 @@ +{ + on-demand-minecraft = import ./on-demand-minecraft; + hellohtml = import ./hellohtml; +} diff --git a/services/hellohtml/default.nix b/services/hellohtml/default.nix new file mode 100644 index 0000000..cc15779 --- /dev/null +++ b/services/hellohtml/default.nix @@ -0,0 +1,121 @@ +{ + config, + lib, + pkgs, + ... +}: +# FIXME: It is wasteful to always run the service. We should run on-demand instead. +# This is usually achieved using SystemD sockets [4] but we are blocked on missing +# features in Deno [1, 5]. +# +# We have to be able to listen on a socket that's already been created by binding +# an open file descriptor (or listening on stdin) [3]. This is not possible in Deno +# as it is now [1, 2, 6]. +# +# Once it becomes a possibility, we should mirror the way push-notification-api works +# as of b9ed407 [8, 7]. +# +# [1]: https://github.com/denoland/deno/issues/6529 +# [2]: https://github.com/denoland/deno/blob/1dd1aba2448c6c8a5a0370c4066a68aca06b859b/ext/net/ops_unix.rs#L207C34-L207C34 +# [3]: https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#Description:~:text=Note%20that%20the,the%20service%20file). +# [4]: https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#Description:~:text=Socket%20units%20may%20be%20used%20to%20implement%20on%2Ddemand%20starting%20of%20services%2C%20as%20well%20as%20parallelized%20starting%20of%20services.%20See%20the%20blog%20stories%20linked%20at%20the%20end%20for%20an%20introduction. +# [5]: https://github.com/denoland/deno/issues/14214 +# [6]: https://github.com/tokio-rs/tokio/issues/5678 +# [7]: https://github.com/benoitc/gunicorn/blob/660fd8d850f9424d5adcd50065e6060832a200d4/gunicorn/arbiter.py#L142-L155 +# [8]: https://github.com/linnnus/push-notification-api/tree/b9ed4071a4500a26b3b348a7f5fbc549e9694562 +let + cfg = config.services.hellohtml; +in { + options.services.hellohtml = { + enable = lib.mkEnableOption "hellohtml service"; + + port = lib.mkOption { + description = "The port where hellohtml should listen."; + type = lib.types.port; + default = 8538; + }; + }; + + config = lib.mkIf cfg.enable { + # Create a user for running service. + users.users.hellohtml = { + group = "hellohtml"; + description = "Runs hellohtml service"; + isSystemUser = true; + home = "/srv/hellohtml"; + createHome = true; # Store DB here. + }; + users.groups.hellohtml = {}; + + # Create hellohtml service. + systemd.services.hellohtml = { + description = "HelloHTML server!!!"; + + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = let + src = pkgs.fetchFromGitHub { + owner = "linnnus"; + repo = "hellohtml"; + rev = "97f00500712d8551d7bbf497ec442083c63384d0"; + hash = "sha256-6nbL2B26dc83F2gSLXadyfS8etuPhhlFy9ivG5l6Tog"; + }; + + hellohtml-vendor = pkgs.stdenv.mkDerivation { + name = "hellohtml-vendor"; + nativeBuildInputs = [pkgs.unstable.deno]; + inherit src; + buildCommand = '' + # Deno wants to create cache directories. + HOME="$(mktemp -d)" + # Thought this wasn't necessary??? + cd $src + # Build directory containing offline deps + import map. + deno vendor --output=$out ./src/server.ts + ''; + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = "sha256-0TGLkEvJaBpI7IlTyuYRzA20Bw/TMSMz3q8wm5oPsBM"; + }; + + hellohtml-drv = pkgs.writeShellScript "hellohtml" '' + export HELLOHTML_DB_PATH="${config.users.users.hellohtml.home}"/hello.db + export HELLOHTML_PORT=${toString cfg.port} + export HELLOHTML_BASE_DIR="${src}" + + ${pkgs.unstable.deno}/bin/deno run \ + --allow-read=$HELLOHTML_BASE_DIR,$HELLOHTML_DB_PATH,. \ + --allow-write=$HELLOHTML_DB_PATH \ + --allow-net=0.0.0.0:$HELLOHTML_PORT \ + --allow-env \ + --no-prompt \ + --unstable-kv \ + --import-map=${hellohtml-vendor}/import_map.json \ + --no-remote \ + ${src}/src/server.ts + ''; + in { + Type = "simple"; + User = config.users.users.hellohtml.name; + Group = config.users.users.hellohtml.group; + ExecStart = "${hellohtml-drv}"; + + # Harden service + # NoNewPrivileges = "yes"; + # PrivateTmp = "yes"; + # PrivateDevices = "yes"; + # DevicePolicy = "closed"; + # ProtectControlGroups = "yes"; + # ProtectKernelModules = "yes"; + # ProtectKernelTunables = "yes"; + # RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + # RestrictNamespaces = "yes"; + # RestrictRealtime = "yes"; + # RestrictSUIDSGID = "yes"; + # MemoryDenyWriteExecute = "yes"; + # LockPersonality = "yes"; + }; + }; + }; +} diff --git a/services/on-demand-minecraft/default.nix b/services/on-demand-minecraft/default.nix new file mode 100644 index 0000000..3ccbec9 --- /dev/null +++ b/services/on-demand-minecraft/default.nix @@ -0,0 +1,355 @@ +# This module defines an on-demand minecraft server service which turns off the +# server when it's not being used. +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib) mkIf mkOption mkEnableOption types; + + cfg = config.services.on-demand-minecraft; +in { + options.services.on-demand-minecraft = { + enable = mkEnableOption "local minecraft server"; + + eula = mkOption { + description = '' + Whether you agree to [Mojangs EULA](https://account.mojang.com/documents/minecraft_eula). + This option must be set to `true` to run a Minecraft™️ server (??). + ''; + type = types.bool; + default = false; + }; + + frequency-check-players = mkOption { + description = '' + How often to check the number of players using the server. If + no players are using the server, it is shut down. + + This should be a valid value for systemd's `onCalendar` + property. + ''; + type = types.nonEmptyStr; + default = "*-*-* *:*:0/20"; + }; + + minimum-server-lifetime = mkOption { + description = '' + Minimum required time to pass from the server is started + before it is allowed to be killed. This should ensure the + server has time to start up before it is killed. + + The option is specified as a number of seconds. + ''; + type = types.ints.positive; + default = 300; + }; + + internal-port = mkOption { + description = '' + The internal port which the minecraft server will listen to. + This port does not need to be exposed to the network. + ''; + type = types.port; + default = cfg.external-port + 1; + }; + + external-port = mkOption { + description = '' + The external port of the socket which is forwarded to the + Minecraft server. This is the one users will connect to. You + will need to add it to `networking.firewall.allowedTCPPorts` + to open it in the firewall. + + You may also have to set up port forwarding if you want to + play with friends who are not on the same LAN. + ''; + type = types.port; + default = 25565; + }; + + openFirewall = mkOption { + description = '' + Open holes in the firewall so clients on LAN can connect. You must + set up port forwarding if you want to play over WAN. + ''; + type = types.bool; + default = true; + }; + + package = mkOption { + description = "What Minecraft server to run."; + default = pkgs.minecraft-server; + type = types.package; + }; + + server-properties = mkOption { + description = '' + Minecraft server properties for the server.properties file. See + + for documentation on these values. Note that some options like + `server-port` will be forced on because they are required for the + server to work. + ''; + type = with types; attrsOf (oneOf [bool int str]); + default = {}; + example = lib.literalExpression '' + { + difficulty = 3; + gamemode = 1; + motd = "My NixOS server!"; + } + ''; + }; + + jvm-options = mkOption { + description = "JVM options for the Minecraft server. List of command line arguments."; + type = types.listOf lib.types.str; + default = ["-Xmx2048M" "-Xms2048M"]; + }; + }; + + config = mkIf cfg.enable { + # Create a user to run the server under. + users.users.minecrafter = { + description = "On-demand minecraft server service user"; + home = "/srv/minecrafter"; + createHome = true; + group = "minecrafter"; + isSystemUser = true; + }; + users.groups.minecrafter = {}; + + # Create an internal socket and hook it up to minecraft-server process as + # stdin. That way we can send commands to it. + systemd.sockets.minecraft-server = { + bindsTo = ["minecraft-server.service"]; + socketConfig = { + ListenFIFO = "/run/minecraft-server.stdin"; + SocketMode = "0660"; + SocketUser = "minecrafter"; + SocketGroup = "minecrafter"; + RemoveOnStop = true; + FlushPending = true; + }; + }; + + # Create a service which runs the server. + systemd.services.minecraft-server = let + server-properties = + cfg.server-properties + // { + server-port = cfg.internal-port; + }; + cfg-to-str = v: + if builtins.isBool v + then + ( + if v + then "true" + else "false" + ) + else toString v; + server-properties-file = pkgs.writeText "server.properties" ('' + # server.properties managed by NixOS configuration. + '' + + lib.concatStringsSep "\n" (lib.mapAttrsToList + (n: v: "${n}=${cfg-to-str v}") + server-properties)); + + # We don't allow eula=false anyways + eula-file = builtins.toFile "eula.txt" '' + # eula.txt managed by NixOS Configuration + eula=true + ''; + + # HACK: Each server is given its own subdirectory so + # incompatibilities between servers don't cause complaints. + start-server = pkgs.writeShellScript "minecraft-server-start" '' + # Switch to runtime directory. + export RUNTIME_DIR="${config.users.users.minecrafter.home}/${cfg.package.name}/" + ${pkgs.busybox}/bin/mkdir -p "$RUNTIME_DIR" + ${pkgs.busybox}/bin/chown minecrafter:minecrafter "$RUNTIME_DIR" + cd "$RUNTIME_DIR" + + # Set up/update environment for server + ln -sf ${eula-file} eula.txt + cp -f ${server-properties-file} server.properties + chmod u+w server.properties # Must be writable because server regenerates it. + + exec ${cfg.package}/bin/minecraft-server "$@" + ''; + + stop-server = pkgs.writeShellScript "minecraft-server-stop" '' + # Send the 'stop' command to the server. It listens for commands on stdin. + echo stop > ${config.systemd.sockets.minecraft-server.socketConfig.ListenFIFO} + # Wait for the PID of the minecraft server to disappear before + # returning, so systemd doesn't attempt to SIGKILL it. + while kill -0 "$1" 2> /dev/null; do + sleep 1s + done + ''; + in { + description = "Actually runs the Minecraft server"; + requires = ["minecraft-server.socket"]; + after = ["networking.target" "minecraft-server.socket"]; + wantedBy = []; # TEMP: Does this do anything? + + serviceConfig = { + ExecStart = "${start-server} ${lib.escapeShellArgs cfg.jvm-options}"; + ExecStop = "${stop-server} $MAINPID"; + Restart = "always"; + + User = "minecrafter"; + Group = "minecrafter"; + + StandardInput = "socket"; + StandardOutput = "journal"; + StandardError = "journal"; + + # Hardening + CapabilityBoundingSet = [""]; + DeviceAllow = [""]; + LockPersonality = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + UMask = "0077"; + }; + }; + + # This socket listens for connections on the public port and + # triggers `listen-minecraft.service` when a connection is made. + systemd.sockets.listen-minecraft = { + enable = true; + wantedBy = ["sockets.target"]; + requires = ["network.target"]; + listenStreams = [(toString cfg.external-port)]; + }; + + # This service is triggered by a TCP connection on the public + # port. It starts hook-minecraft.service if it is not running + # already and waits for it to return (using `after`). Then it proxifies the TCP + # connection to the real (local) Minecraft port. + systemd.services.listen-minecraft = { + enable = true; + path = with pkgs; [systemd]; + requires = ["hook-minecraft.service" "listen-minecraft.socket"]; + after = ["hook-minecraft.service" "listen-minecraft.socket"]; + serviceConfig.ExecStart = '' + ${pkgs.systemd.out}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString cfg.internal-port} + ''; + }; + + # This starts Minecraft if required and waits for it to be + # available over TCP to unlock the `listen-minecraft.service` + # proxy. + systemd.services.hook-minecraft = { + enable = true; + # Add tools used by scripts to path. + path = with pkgs; [systemd libressl busybox]; + serviceConfig = let + # Start the Minecraft server and the timer regularly + # checking whether it should stop. + start-mc = pkgs.writeShellScriptBin "start-mc" '' + echo "Starting server and stop-timer..." + systemctl start minecraft-server.service + systemctl start stop-minecraft.timer + ''; + # Wait for the internal port to be accessible for max. + # 60 seconds before complaining. + wait-tcp = pkgs.writeShellScriptBin "wait-tcp" '' + echo "Waiting for server to start listening on port ${toString cfg.internal-port}..." + for i in `seq 60`; do + if ${pkgs.netcat.nc}/bin/nc -z 127.0.0.1 ${toString cfg.internal-port} >/dev/null; then + echo "Yay! ${toString cfg.internal-port} is now available. hook-minecraft is finished." + exit 0 + fi + sleep 1 + done + echo "${toString cfg.internal-port} did not become available in time." + exit 1 + ''; + in { + # First we start the server, then we wait for it to become available. + ExecStart = "${start-mc}/bin/start-mc"; + ExecStartPost = "${wait-tcp}/bin/wait-tcp"; + }; + }; + + # This timer runs the service of the same name, that checks if + # the server needs to be stopped. + systemd.timers.stop-minecraft = { + enable = true; + timerConfig = { + OnCalendar = cfg.frequency-check-players; + #Unit = "stop-minecraft.service"; + }; + }; + + systemd.services.stop-minecraft = let + # Script that returns true (exit code 0) if the server can be shut + # down. It uses mcping to get the player list. It does not continue if + # the server was started less than `minimum-server-lifetime` seconds + # ago. + # + # NOTE: `pkgs.mcping` is declared my personal monorepo. Hopefully + # everything just works out through the magic of flakes, but if you are + # getting errors like "missing attribute 'mcping'" that's probably why. + no-player-connected = pkgs.writeShellScriptBin "no-player-connected" '' + servicestartsec="$(date -d "$(systemctl show --property=ActiveEnterTimestamp minecraft-server.service | cut -d= -f2)" +%s)" + serviceelapsedsec="$(( $(date +%s) - servicestartsec))" + + if [ $serviceelapsedsec -lt ${toString cfg.minimum-server-lifetime} ]; then + echo "Server is too young to be stopped (minimum lifetime is ${toString cfg.minimum-server-lifetime}s, current is ''${serviceelapsedsec}s)" + exit 1 + fi + + PLAYERS="$(${pkgs.mcping}/bin/mcping 127.0.0.1 ${toString cfg.internal-port} | ${pkgs.jq}/bin/jq .players.online)" + echo "There are $PLAYERS active players" + if [ $PLAYERS -eq 0 ]; then + exit 0 + else + exit 1 + fi + ''; + in { + enable = true; + serviceConfig.Type = "oneshot"; + script = '' + if ${no-player-connected}/bin/no-player-connected; then + echo "Stopping minecraft server..." + systemctl stop minecraft-server.service + systemctl stop hook-minecraft.service + systemctl stop stop-minecraft.timer + fi + ''; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedUDPPorts = [cfg.external-port]; + allowedTCPPorts = [cfg.external-port]; + }; + + assertions = [ + { + assertion = cfg.eula; + message = "You must agree to Mojangs EULA to run minecraft-server. Read https://account.mojang.com/documents/minecraft_eula and set `services.minecraft-server.eula` to `true` if you agree."; + } + ]; + }; +} -- cgit v1.2.3