diff --git a/home/common/default.nix b/home/common/default.nix index 3ecc455..cb46dc5 100644 --- a/home/common/default.nix +++ b/home/common/default.nix @@ -424,7 +424,6 @@ }; }; "apu" = { - hostname = "apu.tempel-vibes.ts.net"; user = "root"; }; }; diff --git a/hosts/apu/configuration.nix b/hosts/apu/configuration.nix index 9d37d42..99b33e5 100644 --- a/hosts/apu/configuration.nix +++ b/hosts/apu/configuration.nix @@ -258,61 +258,25 @@ openFirewall = true; }; - services.home-assistant = { + services.invidious-companion = { enable = true; - extraComponents = [ - # Components required to complete the onboarding - "esphome" - "met" - "radio_browser" - - "yeelight" - "xiaomi_aqara" - "shelly" - ]; - subdomain = "home"; - extraPackages = - python3Packages: with python3Packages; [ - gtts - numpy - ]; - config = { - homeassistant = { - name = "Koti"; - unit_system = "metric"; - time_zone = "Europe/Helsinki"; - }; - http = { - use_x_forwarded_for = true; - trusted_proxies = "127.0.0.1"; - }; - default_config = { }; - }; + host = "0.0.0.0"; + port = 8282; + secretKeyFile = config.age.secrets.invidious-companion.path; + binaryHash = "sha256-nZXKpExKCc2zgSdVT3qo05NyFdpM9H9NJB5UWo+MVWI="; }; - services = { - webserver = { - enable = true; - acme.dnsChallenge = true; - vHosts."koti.repomaa.com" = { - proxyBuffering = false; - locations."/".proxyPort = 8123; - }; - }; - - invidious = { - enable = true; - subdomain = "vid"; - }; + networking.firewall = { + enable = true; + interfaces.tailscale0.allowedTCPPorts = [ 8282 ]; }; security.acme.defaults.environmentFile = config.age.secrets.hetzner.path; networking = { nftables.enable = true; - firewall.enable = true; useDHCP = false; - domain = "repomaa.com"; + domain = "apu.home.arpa"; }; system.stateVersion = "24.05"; diff --git a/hosts/apu/secrets.nix b/hosts/apu/secrets.nix index 4e51070..9d9fde7 100644 --- a/hosts/apu/secrets.nix +++ b/hosts/apu/secrets.nix @@ -10,6 +10,7 @@ }) [ "hetzner" + "invidious-companion" ] ); } diff --git a/modules/services/default.nix b/modules/services/default.nix index b352a0a..49f22e8 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -32,5 +32,6 @@ ./voidauth.nix ./gitea.nix ./dhcp-dns-sync + ./invidious-companion.nix ]; } diff --git a/modules/services/dhcp-dns-sync/default.nix b/modules/services/dhcp-dns-sync/default.nix index 5443edf..3eda6fa 100644 --- a/modules/services/dhcp-dns-sync/default.nix +++ b/modules/services/dhcp-dns-sync/default.nix @@ -6,6 +6,11 @@ }: let cfg = config.modules.services.dhcp-dns-sync; + ownAddress = ( + lib.elemAt (lib.splitString "/" + config.systemd.network.networks."30-${cfg.interface}".networkConfig.Address + ) 0 + ); dhcp-leases-to-unbound = pkgs.runCommand "dhcp-leases-to-unbound" @@ -59,9 +64,10 @@ in users.groups.dhcp-dns-sync = { }; # Ensure directories and files exist with proper permissions + # Directory needs to be group-writable for unbound group systemd.tmpfiles.rules = [ - "d /var/lib/unbound 0755 unbound unbound -" - "f ${cfg.unboundConfigPath} 0644 dhcp-dns-sync dhcp-dns-sync -" + "d /var/lib/unbound 0775 unbound unbound -" + "f ${cfg.unboundConfigPath} 0644 dhcp-dns-sync unbound -" ]; # Extend Unbound configuration to include generated file @@ -69,6 +75,8 @@ in server = { local-zone = [ "${cfg.domain}. static" ]; include = cfg.unboundConfigPath; + local-data = [ ''"apu.home.arpa. IN A ${ownAddress}"'' ]; + local-data-ptr = [ ''"${ownAddress} apu.home.arpa."'' ]; }; }; @@ -88,7 +96,7 @@ in serviceConfig = { Type = "oneshot"; User = "dhcp-dns-sync"; - Group = "dhcp-dns-sync"; + Group = "unbound"; # Allow access to networkctl via D-Bus SupplementaryGroups = [ "systemd-network" ]; # Read/write paths diff --git a/modules/services/dhcp-dns-sync/dhcp-leases-to-unbound.cr b/modules/services/dhcp-dns-sync/dhcp-leases-to-unbound.cr index c5f46bc..42fada0 100644 --- a/modules/services/dhcp-dns-sync/dhcp-leases-to-unbound.cr +++ b/modules/services/dhcp-dns-sync/dhcp-leases-to-unbound.cr @@ -58,13 +58,6 @@ def sanitize_hostname(hostname : String) : String? sanitized end -def reverse_ptr(ip : String) : String? - parts = ip.split('.') - return nil unless parts.size == 4 - - "#{parts[3]}.#{parts[2]}.#{parts[1]}.#{parts[0]}.in-addr.arpa." -end - def generate_unbound_config(leases : Array(Lease), domain : String) : String lines = [] of String @@ -82,22 +75,29 @@ def generate_unbound_config(leases : Array(Lease), domain : String) : String # A record lines << %{local-data: "#{fqdn} IN A #{lease.address}"} - # PTR record - if ptr = reverse_ptr(lease.address) - lines << %{local-data-ptr: "#{ptr} #{fqdn}"} - end + # PTR record - local-data-ptr expects IP in normal form, unbound reverses it + lines << %{local-data-ptr: "#{lease.address} #{fqdn}"} end lines.join("\n") + "\n" end def get_leases(interface : String, networkctl_path : String? = nil) : Array(Lease) - cmd = networkctl_path ? "#{networkctl_path} status #{interface} --json=short" : "networkctl status #{interface} --json=short" - output = `#{cmd}` - raise "networkctl failed (exit code #{$?.exit_status}): #{output}" unless $?.success? + cmd = networkctl_path ? "#{networkctl_path}" : "networkctl" + args = ["status", interface, "--json=short"] - status = NetworkStatus.from_json(output) - status.dhcp_server.try(&.leases) || [] of Lease + Process.run(cmd, args, output: Process::Redirect::Pipe, error: Process::Redirect::Pipe) do |process| + result = process.wait + output = process.output.to_s + + unless result.success? + error = process.error.to_s + raise "networkctl failed (exit code #{result.exit_code}): #{error.empty? ? output : error}" + end + + status = NetworkStatus.from_json(output) + status.dhcp_server.try(&.leases) || [] of Lease + end end def write_if_changed(content : String, path : String) : Bool @@ -151,13 +151,18 @@ OptionParser.parse do |parser| end def reload_unbound(unbound_control_path : String?) - cmd = unbound_control_path ? "#{unbound_control_path} reload" : "unbound-control reload" + cmd = unbound_control_path ? "#{unbound_control_path}" : "unbound-control" puts "Reloading Unbound..." - result = system(cmd) - unless result - # Fallback to systemctl - system("systemctl reload unbound") + + Process.run(cmd, ["reload"], output: Process::Redirect::Pipe, error: Process::Redirect::Pipe) do |process| + result = process.wait + + unless result.success? + raise "unbound reload failed (exit code #{result.exit_code}): #{process.error}" + end end + + puts "Unbound reloaded successfully." end begin diff --git a/modules/services/invidious-companion.nix b/modules/services/invidious-companion.nix new file mode 100644 index 0000000..3c1814e --- /dev/null +++ b/modules/services/invidious-companion.nix @@ -0,0 +1,99 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.invidious-companion; + companionRelease = "release-master"; + hostPlatform = pkgs.stdenv.hostPlatform.system; + + # Invidious Companion package - fetches binary release and patches for NixOS + unwrappedCompanion = pkgs.stdenv.mkDerivation { + pname = "unwrapped-invidious-companion"; + version = companionRelease; + + src = + let + archMap = { + x86_64-linux = "x86_64-unknown-linux-gnu"; + aarch64-linux = "aarch64-unknown-linux-gnu"; + }; + platform = archMap.${hostPlatform} or (throw "Unsupported platform: ${hostPlatform}"); + in + pkgs.fetchzip { + url = "https://github.com/iv-org/invidious-companion/releases/download/${companionRelease}/invidious_companion-${platform}.tar.gz"; + sha256 = cfg.binaryHash; + }; + + dontStrip = true; + dontPatchELF = true; + + installPhase = '' + mkdir -p $out/bin + cp invidious_companion $out/bin/invidious_companion + chmod +x $out/bin/invidious_companion + ''; + }; + + invidiousCompanion = pkgs.buildFHSEnv { + name = "invidious-companion"; + targetPkgs = pkgs: [ unwrappedCompanion ]; + runScript = "invidious_companion"; + meta = { + description = "Invidious companion for handling video streams"; + homepage = "https://github.com/iv-org/invidious-companion"; + license = lib.licenses.agpl3Only; + }; + }; +in +{ + options.services.invidious-companion = { + enable = lib.mkEnableOption "Enable Invidious Companion service"; + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + }; + port = lib.mkOption { + type = lib.types.port; + default = 8282; + description = "Port for Invidious Companion to listen on"; + }; + secretKeyFile = lib.mkOption { + type = lib.types.str; + description = "Path to file containing the companion secret key"; + }; + binaryHash = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "SHA256 hash of the invidious companion binary release"; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.invidious-companion = { + description = "Invidious Companion - video stream handler"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + Type = "simple"; + User = "invidious"; + Group = "invidious"; + DynamicUser = true; + ExecStart = lib.getExe invidiousCompanion; + Environment = [ + "HOST=${cfg.host}" + "PORT=${toString cfg.port}" + "TMPDIR=/var/cache/invidious-companion" + ]; + EnvironmentFile = [ cfg.secretKeyFile ]; + CacheDirectory = "invidious-companion"; + WorkingDirectory = "%C/invidious-companion"; + Restart = "always"; + RestartSec = 5; + }; + }; + }; +} diff --git a/modules/services/invidious.nix b/modules/services/invidious.nix index 434df3c..c1df33f 100644 --- a/modules/services/invidious.nix +++ b/modules/services/invidious.nix @@ -1,4 +1,8 @@ -{ config, lib, ... }: +{ + config, + lib, + ... +}: let cfg = config.services.invidious; fqdn = "${cfg.subdomain}.${config.networking.domain}"; @@ -32,5 +36,14 @@ in vHosts.${fqdn}.locations."/".proxyPort = cfg.port; }; }; + + systemd.services.invidious.serviceConfig.DynamicUser = lib.mkForce false; + + users.groups.invidious = { }; + users.users.invidious = { + isSystemUser = true; + group = "invidious"; + description = "Invidious user"; + }; }; } diff --git a/secrets/invidious-companion.age b/secrets/invidious-companion.age new file mode 100644 index 0000000..ce7861b --- /dev/null +++ b/secrets/invidious-companion.age @@ -0,0 +1,12 @@ +age-encryption.org/v1 +-> ssh-ed25519 osOCZA 9wZ3G4vjwJhYungj/utZ/jgnQRD7qGHsMXM51gNFLyY +SvdeK7R1AxveXXFJng21JK1fy+y7lh6OINB4CtUdS1Q +-> ssh-ed25519 DFiohQ 1NIsoZWR4fY+bcROkw7iq+X0cYIE9g5IiWOqO0FvymQ +igfAuxzfUSlhE3jaTMjqCYeF8ccKVyuUW+uD8JdH75c +-> ssh-ed25519 wU682A g5y8TFpeJ0myejb8r7gL96JBk/q21KlDOBE6ZpCqv2A +I/3aFKq2ne3gVeg+/1LmlKoDyg723yyjUdVdzgFzhV4 +--- JsRdNjJ285V+RGktIxJv29Alef95kpB2TOnYH66Wr4Q +z¿ +n +–´²xÇ‘ Û¸"‹KA›x)ñÑ8 é… +bçµ.)9„è#Š?ØxÔfMW/<Òîy• ÔHÜçݶOøš \ No newline at end of file diff --git a/secrets/invidious.age b/secrets/invidious.age new file mode 100644 index 0000000..76db660 Binary files /dev/null and b/secrets/invidious.age differ