diff --git a/src/system/machines/server/hardware.nix b/src/system/machines/server/hardware.nix index 8a9ebe5..fb45e7f 100644 --- a/src/system/machines/server/hardware.nix +++ b/src/system/machines/server/hardware.nix @@ -1,4 +1,4 @@ -{ config, lib, modulesPath, ... }: +{ config, lib, pkgs, modulesPath, ... }: { imports = [ @@ -17,6 +17,14 @@ hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; + # Enable VAAPI for hardware video acceleration + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ + intel-vaapi-driver # i965 driver for Haswell + ]; + }; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; } diff --git a/src/system/machines/server/system.nix b/src/system/machines/server/system.nix index 5278443..c290f9d 100644 --- a/src/system/machines/server/system.nix +++ b/src/system/machines/server/system.nix @@ -7,13 +7,13 @@ modules.system = { nginx.enable = true; forgejo.enable = true; - frigate.enable = false; + frigate.enable = true; immich.enable = true; - bitcoin = { - enable = true; - electrum.enable = true; - clightning.enable = true; - }; + # bitcoin = { + # enable = true; + # electrum.enable = true; + # clightning.enable = true; + # }; backup = { enable = true; @@ -120,6 +120,19 @@ firewall = { enable = true; allowedTCPPorts = [ 22 ]; + allowedUDPPorts = [ 53 67 ]; # DNS + DHCP + extraCommands = '' + # Block specific camera MACs from forwarding (instant DROP, no timeouts) + # Add each camera MAC here as you set them up + iptables -A FORWARD -m mac --mac-source 00:1f:54:c2:d1:b1 -j DROP # parking_lot + iptables -A FORWARD -m mac --mac-source 00:1f:54:b2:9b:1d -j DROP # living_room/kitchen + iptables -A FORWARD -m mac --mac-source 00:1f:54:a9:81:d1 -j DROP # doorbell + ''; + extraStopCommands = '' + iptables -D FORWARD -m mac --mac-source 00:1f:54:c2:d1:b1 -j DROP || true + iptables -D FORWARD -m mac --mac-source 00:1f:54:b2:9b:1d -j DROP || true + iptables -D FORWARD -m mac --mac-source 00:1f:54:a9:81:d1 -j DROP || true + ''; }; }; @@ -143,12 +156,16 @@ interface = "enp2s0f1"; bind-interfaces = true; dhcp-range = "192.168.1.100,192.168.1.200,24h"; - # No gateway option = cameras can't route to internet + + # Static DHCP reservations for cameras + dhcp-host = [ + "00:1f:54:c2:d1:b1,192.168.1.194,parking_lot" + "00:1f:54:b2:9b:1d,192.168.1.147,living_room_kitchen" + "00:1f:54:a9:81:d1,192.168.1.167,doorbell" + ]; }; }; - networking.firewall.allowedUDPPorts = [ 53 ]; - services.fail2ban = { enable = true; maxretry = 5; diff --git a/src/system/modules/backup/default.nix b/src/system/modules/backup/default.nix index 07a3895..511b332 100644 --- a/src/system/modules/backup/default.nix +++ b/src/system/modules/backup/default.nix @@ -9,6 +9,7 @@ let # Convert absolute paths to relative for tar, preserving structure # e.g., /var/lib/forgejo -> var/lib/forgejo tarPaths = map (p: removePrefix "/" p) cfg.paths; + excludeArgs = concatMapStrings (e: "--exclude='${e}' ") cfg.exclude; backupScript = pkgs.writeShellScript "backup" '' set -euo pipefail @@ -22,7 +23,7 @@ let echo "Paths: ${concatStringsSep " " cfg.paths}" export PATH="${pkgs.age-plugin-yubikey}/bin:$PATH" - ${pkgs.gnutar}/bin/tar -C / -cf - ${concatStringsSep " " tarPaths} | \ + ${pkgs.gnutar}/bin/tar -C / ${excludeArgs}-cf - ${concatStringsSep " " tarPaths} | \ ${pkgs.age}/bin/age ${recipientArgs} -o "$TEMP_DIR/$BACKUP_NAME" ${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf copy "$TEMP_DIR/$BACKUP_NAME" "${cfg.destination}" @@ -49,6 +50,12 @@ in description = "Absolute paths to include in backup (structure preserved)"; }; + exclude = mkOption { + type = types.listOf types.str; + default = []; + description = "Patterns to exclude (passed to tar --exclude)"; + }; + recipients = mkOption { type = types.listOf types.str; default = []; diff --git a/src/system/modules/frigate/README.md b/src/system/modules/frigate/README.md new file mode 100644 index 0000000..0166264 --- /dev/null +++ b/src/system/modules/frigate/README.md @@ -0,0 +1,119 @@ +# Frigate Camera Setup + +## Camera Models + +| Camera | Model | MAC | IP | +|--------|-------|-----|-----| +| parking_lot | W461ASC | 00:1f:54:c2:d1:b1 | 192.168.1.194 | +| doorbell | B463AJ | 00:1f:54:a9:81:d1 | 192.168.1.167 | +| living_room | W463AQ (ch1) | 00:1f:54:b2:9b:1d | 192.168.1.147 | +| kitchen | W463AQ (ch2) | 00:1f:54:b2:9b:1d | 192.168.1.147 | +| porch | SL300 | | | | + +## Network Architecture + +- Camera network: 192.168.1.0/24 (isolated, no internet) +- Server NIC: enp2s0f1 @ 192.168.1.1 +- WiFi AP: TP-Link RE315 @ 192.168.1.254 +- DHCP range: 192.168.1.100-200 + +## RTSP URL Format + +``` +rtsp://admin:ocu?u3Su@/cam/realmonitor?channel=&subtype=0 +``` + +- channel=1 for single-camera devices +- channel=1,2 for dual-camera devices (W463AQ) +- subtype=0 for main stream, subtype=1 for sub stream + +## Camera Reset Procedures + +### W461ASC (parking_lot) +1. Keep camera powered on +2. Reset button is on the back of the camera +3. Press and hold reset button for 30-60 seconds until chime sounds + +### B463AJ (doorbell) +1. Remove doorbell from mount +2. Locate reset button on the back +3. Press and hold until you hear chime reset sound +4. Reconnect via Lorex app as new device + +### W463AQ (living_room/kitchen) +1. Keep camera powered on +2. Rotate the lens upwards to reveal hidden reset button +3. Press and hold reset button until you hear audio prompt +4. Flashing green Smart Security Lighting confirms reset +5. Solid green = not fully reset, repeat if needed + +### SL300 (porch) +1. Keep camera powered on +2. Tilt camera lens upwards to reveal reset/microSD card cover +3. Remove the cover +4. Press and hold reset button until audio prompt +5. Replace cover quickly +6. Wait for green LED flash + audio confirmation + +## Initial Setup + +1. Temporarily enable internet for camera network: + ```bash + sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o enp2s0f0 -j MASQUERADE + sudo sysctl -w net.ipv4.ip_forward=1 + ``` + +2. Connect camera to "cams" WiFi network + +3. Use Lorex app to configure camera (requires cloud - CCP middleman) + +4. Get camera MAC from DHCP leases: + ```bash + cat /var/lib/dnsmasq/dnsmasq.leases + ``` + +5. Add DHCP reservation in `system.nix`: + ```nix + dhcp-host = [ + "aa:bb:cc:dd:ee:ff,192.168.1.XXX,camera_name" + ]; + ``` + +6. Add MAC to firewall block list in `system.nix`: + ```nix + iptables -A FORWARD -m mac --mac-source aa:bb:cc:dd:ee:ff -j DROP + ``` + +7. Update camera IP in `frigate/default.nix` and enable + +8. Deploy and disable internet: + ```bash + nixos-rebuild switch --flake .#server --target-host server + sudo iptables -t nat -D POSTROUTING -s 192.168.1.0/24 -o enp2s0f0 -j MASQUERADE + sudo sysctl -w net.ipv4.ip_forward=0 + ``` + +## Storage + +Frigate data is stored on /data to avoid filling root partition: + +| Path | Bind Mount | Contents | +|------|------------|----------| +| /var/lib/frigate | /data/frigate/lib | Database, recordings, clips | +| /var/cache/frigate | /data/frigate/cache | Temporary cache | +| /var/cache/nginx/frigate | /data/frigate/nginx-cache | API response cache | + +## Notes + +- Lorex cameras are cloud-only for configuration (no local web UI responds) +- RTSP works locally without internet +- Cameras phone home aggressively when internet is available - keep isolated +- Haswell CPU cannot hardware decode HEVC - using CPU decode +- Consider T400 GPU for hardware acceleration if scaling to more cameras + +## Port Scan Results (W461ASC) + +- 80/tcp - HTTP (non-responsive, proprietary) +- 554/tcp - RTSP (working) +- 8086/tcp - Proprietary +- 35000/tcp - Proprietary diff --git a/src/system/modules/frigate/default.nix b/src/system/modules/frigate/default.nix index 94e345c..11c14d3 100644 --- a/src/system/modules/frigate/default.nix +++ b/src/system/modules/frigate/default.nix @@ -16,38 +16,46 @@ in services.frigate = { enable = true; hostname = "frigate.${domain}"; + # vaapiDriver = "i965"; # Haswell only supports H.264, not HEVC settings = { mqtt.enabled = false; + # ffmpeg.hwaccel_args = "preset-vaapi"; # Disabled - camera uses HEVC which Haswell can't decode + record.enabled = true; cameras = { doorbell = { + enabled = true; detect.enabled = false; ffmpeg.inputs = [{ - path = "rtsp://admin:ocu?u3Su@192.168.0.134/cam/realmonitor?channel=1&subtype=0"; + path = "rtsp://admin:ocu?u3Su@192.168.1.167/cam/realmonitor?channel=1&subtype=0"; roles = [ "record" ]; }]; }; living_room = { + enabled = false; detect.enabled = false; ffmpeg.inputs = [{ - path = "rtsp://admin:ocu?u3Su@192.168.0.181/cam/realmonitor?channel=1&subtype=0"; + path = "rtsp://admin:ocu?u3Su@192.168.1.147/cam/realmonitor?channel=1&subtype=0"; roles = [ "record" ]; }]; }; kitchen = { + enabled = false; detect.enabled = false; ffmpeg.inputs = [{ - path = "rtsp://admin:ocu?u3Su@192.168.0.181/cam/realmonitor?channel=2&subtype=0"; + path = "rtsp://admin:ocu?u3Su@192.168.1.147/cam/realmonitor?channel=2&subtype=0"; roles = [ "record" ]; }]; }; parking_lot = { + enabled = true; detect.enabled = false; ffmpeg.inputs = [{ - path = "rtsp://admin:ocu?u3Su@192.168.0.59/cam/realmonitor?channel=1&subtype=0"; + path = "rtsp://admin:ocu?u3Su@192.168.1.194/cam/realmonitor?channel=1&subtype=0"; roles = [ "record" ]; }]; }; porch = { + enabled = false; detect.enabled = false; ffmpeg.inputs = [{ path = "rtsp://admin:ocu?u3Su@192.168.0.43/cam/realmonitor?channel=1&subtype=0"; @@ -64,5 +72,27 @@ in forceSSL = true; }; + # Bind mount caches into the 3TB frigate LVM volume + systemd.tmpfiles.rules = [ + "d /var/lib/frigate/cache 0750 frigate frigate -" + "d /var/lib/frigate/nginx-cache 0750 nginx nginx -" + ]; + + fileSystems."/var/cache/frigate" = { + device = "/var/lib/frigate/cache"; + options = [ "bind" ]; + }; + + fileSystems."/var/cache/nginx/frigate" = { + device = "/var/lib/frigate/nginx-cache"; + options = [ "bind" ]; + }; + + # Backup recordings/database, exclude caches + modules.system.backup = { + paths = [ "/var/lib/frigate" ]; + exclude = [ "*/cache" "*/nginx-cache" ]; + }; + }; }