Compare commits

..

5 Commits

Author SHA1 Message Date
72c7287445 mpv: sane-sysvol: monitor pipewire for changes and relay that to uosc 2024-04-09 06:41:48 +00:00
b715fd346f mpv: don't need to force uosc volume state to 0 by default; nil is OK 2024-04-07 00:29:41 +00:00
1b9b0ac0f6 todo.md: add work around signal, mpv 2024-04-07 00:26:15 +00:00
79c4e2c405 mpv: sane-sysvol script: init
it's a one-way volume control, but that's a start
2024-04-07 00:26:15 +00:00
17a3f90825 mpv: rename plugin: sane -> sane-cast 2024-04-06 23:44:01 +00:00
1686 changed files with 56905 additions and 666223 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/build .working
/.working
result result
result-* result-*
/secrets/local.nix /secrets/local.nix

View File

@@ -1,12 +1,9 @@
keys: keys:
- &user_desko_colin age1tnl4jfgacwkargzeqnhzernw29xx8mkv73xh6ufdyde6q7859slsnzf24x - &user_desko_colin age1tnl4jfgacwkargzeqnhzernw29xx8mkv73xh6ufdyde6q7859slsnzf24x
- &user_flowy_colin age1nw3z25gn6l8gxneqw43tp8d2354c83d9sn3r0dqy5tapakdwhyvse0j2cc
- &user_lappy_colin age1j2pqnl8j0krdzk6npe93s4nnqrzwx978qrc0u570gzlamqpnje9sc8le2g - &user_lappy_colin age1j2pqnl8j0krdzk6npe93s4nnqrzwx978qrc0u570gzlamqpnje9sc8le2g
- &user_servo_colin age1z8fauff34cdecr6sjkre260luzxcca05kpcwvhx988d306tpcejsp63znu - &user_servo_colin age1z8fauff34cdecr6sjkre260luzxcca05kpcwvhx988d306tpcejsp63znu
- &user_moby_colin age1zsrsvd7j6l62fjxpfd2qnhqlk8wk4p8r0dtxpe4sdgnh2474095qdu7xj9 - &user_moby_colin age1zsrsvd7j6l62fjxpfd2qnhqlk8wk4p8r0dtxpe4sdgnh2474095qdu7xj9
- &host_crappy age1hl50ufuxnqy0jnk8fqeu4tclh4vte2xn2d59pxff0gun20vsmv5sp78chj
- &host_desko age1vnw7lnfpdpjn62l3u5nyv5xt2c965k96p98kc43mcnyzpetrts9q54mc9v - &host_desko age1vnw7lnfpdpjn62l3u5nyv5xt2c965k96p98kc43mcnyzpetrts9q54mc9v
- &host_flowy age1azm6carlm6tdjup37u5dr40585vjujajev70u4glwd9sv7swa99sk6mswx
- &host_lappy age1w7mectcjku6x3sd8plm8wkn2qfrhv9n6zhzlf329e2r2uycgke8qkf9dyn - &host_lappy age1w7mectcjku6x3sd8plm8wkn2qfrhv9n6zhzlf329e2r2uycgke8qkf9dyn
- &host_servo age1tzlyex2z6t88tg9h82943e39shxhmqeyr7ywhlwpdjmyqsndv3qq27x0rf - &host_servo age1tzlyex2z6t88tg9h82943e39shxhmqeyr7ywhlwpdjmyqsndv3qq27x0rf
- &host_moby age18vq5ktwgeaysucvw9t67drqmg5zd5c5k3le34yqxckkfj7wqdqgsd4ejmt - &host_moby age18vq5ktwgeaysucvw9t67drqmg5zd5c5k3le34yqxckkfj7wqdqgsd4ejmt
@@ -15,13 +12,10 @@ creation_rules:
key_groups: key_groups:
- age: - age:
- *user_desko_colin - *user_desko_colin
- *user_flowy_colin
- *user_lappy_colin - *user_lappy_colin
- *user_servo_colin - *user_servo_colin
- *user_moby_colin - *user_moby_colin
- *host_crappy
- *host_desko - *host_desko
- *host_flowy
- *host_lappy - *host_lappy
- *host_servo - *host_servo
- *host_moby - *host_moby
@@ -29,7 +23,6 @@ creation_rules:
key_groups: key_groups:
- age: - age:
- *user_desko_colin - *user_desko_colin
- *user_flowy_colin
- *user_lappy_colin - *user_lappy_colin
- *user_servo_colin - *user_servo_colin
- *host_servo - *host_servo
@@ -37,28 +30,18 @@ creation_rules:
key_groups: key_groups:
- age: - age:
- *user_desko_colin - *user_desko_colin
- *user_flowy_colin
- *user_lappy_colin - *user_lappy_colin
- *host_desko - *host_desko
- path_regex: secrets/flowy*
key_groups:
- age:
- *user_lappy_colin
- *user_flowy_colin
- *user_desko_colin
- *host_flowy
- path_regex: secrets/lappy* - path_regex: secrets/lappy*
key_groups: key_groups:
- age: - age:
- *user_lappy_colin - *user_lappy_colin
- *user_flowy_colin
- *user_desko_colin - *user_desko_colin
- *host_lappy - *host_lappy
- path_regex: secrets/moby* - path_regex: secrets/moby*
key_groups: key_groups:
- age: - age:
- *user_desko_colin - *user_desko_colin
- *user_flowy_colin
- *user_lappy_colin - *user_lappy_colin
- *user_moby_colin - *user_moby_colin
- *host_moby - *host_moby

View File

@@ -2,8 +2,6 @@
# .❄≡We|_c0m3 7o m`/ f14k≡❄. # .❄≡We|_c0m3 7o m`/ f14k≡❄.
(er, it's not a flake anymore. welcome to my nix files.)
## What's Here ## What's Here
this is the top-level repo from which i configure/deploy all my NixOS machines: this is the top-level repo from which i configure/deploy all my NixOS machines:
@@ -17,53 +15,60 @@ the only hard dependency for my exported pkgs/modules should be [nixpkgs][nixpkg
building [hosts/](./hosts/) will require [sops][sops]. building [hosts/](./hosts/) will require [sops][sops].
you might specifically be interested in these files (elaborated further in #key-points-of-interest): you might specifically be interested in these files (elaborated further in #key-points-of-interest):
- [my packages](./pkgs/by-name) - ~~[`sxmo-utils`](./pkgs/additional/sxmo-utils/default.nix)~~
- ~~[example SXMO deployment](./hosts/modules/gui/sxmo/default.nix)~~
- these files will remain until my config settles down, but i no longer use or maintain SXMO.
- [my implementation of impermanence](./modules/persist/default.nix) - [my implementation of impermanence](./modules/persist/default.nix)
- my way of deploying dotfiles/configuring programs per-user: - my way of deploying dotfiles/configuring programs per-user:
- [modules/fs/](./modules/fs/default.nix) - [modules/fs/](./modules/fs/default.nix)
- [modules/programs/](./modules/programs/default.nix) - [modules/programs/](./modules/programs/default.nix)
- [modules/users/](./modules/users/default.nix) - [modules/users.nix](./modules/users.nix)
if you find anything here genuinely useful, message me so that i can work to upstream it!
[nixpkgs]: https://github.com/NixOS/nixpkgs [nixpkgs]: https://github.com/NixOS/nixpkgs
[sops]: https://github.com/Mic92/sops-nix [sops]: https://github.com/Mic92/sops-nix
[uninsane-org]: https://uninsane.org [uninsane-org]: https://uninsane.org
## Using This Repo In Your Own Config ## Using This Repo In Your Own Config
follow the instructions [here][NUR] to access my packages through the Nix User Repositories. this should be a pretty "standard" flake. just reference it, and import either
- `nixosModules.sane` (for the modules)
- `overlays.pkgs` (for the packages)
or follow the instructions [here][NUR] to use it via the Nix User Repositories.
[NUR]: https://nur.nix-community.org/ [NUR]: https://nur.nix-community.org/
## Layout ## Layout
- `doc/` - `doc/`
- instructions for tasks i find myself doing semi-occasionally in this repo. - instructions for tasks i find myself doing semi-occasionally in this repo.
- `hosts/` - `hosts/`
- configs which aren't factored with external use in mind. - the bulk of config which isn't factored with external use in mind.
- that is, if you were to add this repo to a flake.nix for your own use, - that is, if you were to add this repo to a flake.nix for your own use,
you won't likely be depending on anything in this directory. you won't likely be depending on anything in this directory.
- `integrations/` - `integrations/`
- code intended for consumption by external tools (e.g. the Nix User Repos). - code intended for consumption by external tools (e.g. the Nix User Repos)
- `modules/` - `modules/`
- config which is gated behind `enable` flags, in similar style to nixpkgs' `nixos/` directory. - config which is gated behind `enable` flags, in similar style to nixpkgs'
- if you depend on this repo for anything besides packages, it's most likely for something in this directory. `nixos/` directory.
- if you depend on this repo, it's most likely for something in this directory.
- `nixpatches/`
- literally, diffs i apply atop upstream nixpkgs before performing further eval.
- `overlays/` - `overlays/`
- exposed via the `overlays` output in `flake.nix`.
- predominantly a list of `callPackage` directives. - predominantly a list of `callPackage` directives.
- `pkgs/` - `pkgs/`
- derivations for things not yet packaged in nixpkgs. - derivations for things not yet packaged in nixpkgs.
- derivations for things from nixpkgs which i need to `override` for some reason. - derivations for things from nixpkgs which i need to `override` for some reason.
- inline code for wholly custom packages (e.g. `pkgs/by-name/sane-scripts/` for CLI tools - inline code for wholly custom packages (e.g. `pkgs/additional/sane-scripts/` for CLI tools
that are highly specific to my setup). that are highly specific to my setup).
- `scripts/` - `scripts/`
- scripts which aren't reachable on a deployed system, but may aid manual deployments. - scripts which aren't reachable on a deployed system, but may aid manual deployments
- `secrets/` - `secrets/`
- encrypted keys, API tokens, anything which one or more of my machines needs - encrypted keys, API tokens, anything which one or more of my machines needs
read access to but shouldn't be world-readable. read access to but shouldn't be world-readable.
- not much to see here. - not much to see here
- `templates/` - `templates/`
- exposed via the `templates` output in `flake.nix`.
- used to instantiate short-lived environments. - used to instantiate short-lived environments.
- used to auto-fill the boiler-plate portions of new packages. - used to auto-fill the boiler-plate portions of new packages.
@@ -82,40 +87,43 @@ i.e. you might find value in using these in your own config:
- populated with some statically-defined data - populated with some statically-defined data
- populated according to some script - populated according to some script
- created as a dependency of some service (e.g. `nginx`) - created as a dependency of some service (e.g. `nginx`)
- values defined here are applied neither at evaluation time _nor_ at activation time.
- rather, they become systemd services.
- systemd manages dependencies
- e.g. link `/var/www -> /mnt/my-drive/www` only _after_ `/mnt/my-drive/www` appears)
- this is akin to using [Home Manager's][home-manager] file API -- the part which lets you - this is akin to using [Home Manager's][home-manager] file API -- the part which lets you
statically define `~/.config` files -- just with a different philosophy. statically define `~/.config` files -- just with a different philosophy.
namely, it avoids any custom activation scripts by leveraging `systemd-tmpfiles`.
- `modules/persist/` - `modules/persist/`
- my implementation of impermanence, built atop the above `fs` module, with a few notable features: - my alternative to the Impermanence module.
- no custom activation scripts or services (uses `systemd-tmpfiles` and `.mount` units) - this builds atop `modules/fs/` to achieve things stock impermanence can't:
- persist things to encrypted storage which is unlocked at login time (pam_mount).
- "persist" cache directories -- to free up RAM -- but auto-wipe them on mount - "persist" cache directories -- to free up RAM -- but auto-wipe them on mount
and encrypt them to ephemeral keys so they're unreadable post shutdown/unmount. and encrypt them to ephemeral keys so they're unreadable post shutdown/unmount.
- persist to encrypted storage which is unlocked at login time.
- `modules/programs/` - `modules/programs/`
- like nixpkgs' `programs` options, but allows both system-wide or per-user deployment. - like nixpkgs' `programs` options, but allows both system-wide or per-user deployment.
- allows `fs` and `persist` config values to be gated behind program deployment: - allows `fs` and `persist` config values to be gated behind program deployment:
- e.g. `/home/<user>/.mozilla/firefox` is persisted only for users who - e.g. `/home/<user>/.mozilla/firefox` is persisted only for users who
`sane.programs.firefox.enableFor.user."<user>" = true;` `sane.programs.firefox.enableFor.user."<user>" = true;`
- allows aggressive sandboxing any program: - allows aggressive sandboxing any program:
- `sane.programs.firefox.sandbox.enable = true; # wraps the program so that it isolates itself into a new namespace when invoked` - `sane.programs.firefox.sandbox.method = "bwrap"; # sandbox with bubblewrap`
- `sane.programs.firefox.sandbox.whitelistWayland = true; # allow it to render a wayland window` - `sane.programs.firefox.sandbox.whitelistWayland = true; # allow it to render a wayland window`
- `sane.programs.firefox.sandbox.extraHomePaths = [ "Downloads" ]; # allow it read/write access to ~/Downloads` - `sane.programs.firefox.sandbox.extraHomePaths = [ "Downloads" ]; # allow it read/write access to ~/Downloads`
- integrated with `fs` and `persist` modules so that programs' config files and persisted data stores are linked into the sandbox w/o any extra involvement. - integrated with `fs` and `persist` modules so that programs' config files and persisted data stores are linked into the sandbox w/o any extra involvement.
- `modules/users/` - `modules/users.nix`
- convenience layer atop the above modules so that you can just write - convenience layer atop the above modules so that you can just write
`fs.".config/git"` instead of `fs."/home/colin/.config/git"` `fs.".config/git"` instead of `fs."/home/colin/.config/git"`
- simplified `systemd.services` API
some things in here could easily find broader use. if you would find benefit in
them being factored out of my config, message me and we could work to make that happen.
[home-manager]: https://github.com/nix-community/home-manager [home-manager]: https://github.com/nix-community/home-manager
## Mirrors ## Mirrors
this repo exists in a few known locations: this repo exists in a few known locations:
- primary: <https://git.uninsane.org/colin/nix-files> - primary: <https://git.uninsane.org/colin/nix-files>
- mirror: <https://github.com/nix-community/nur-combined/tree/master/repos/colinsane> - mirror: <https://github.com/nix-community/nur-combined/tree/master/repos/colinsane>
## Contact ## Contact
if you want to contact me for questions, or collaborate to split something useful into a shared repo, etc, if you want to contact me for questions, or collaborate to split something useful into a shared repo, etc,

185
TODO.md
View File

@@ -1,129 +1,81 @@
## BUGS ## BUGS
- alacritty Ctrl+N frequently fails to `cd` to the previous directory - Signal restart loop drains battery
- bunpen dbus sandboxing can't be *nested* (likely a problem in xdg-dbus-proxy) - decrease s6 restart time?
- dissent has a memory leak (3G+ after 24hr) - mpv `player-mode=pseudo-gui` swallows all loggin
- set a max memory use in the systemd service, to force it to restart as it leaks? - ringer (i.e. dino incoming call) doesn't prevent moby from sleeping
- `rmDbusServices` may break sandboxing - sway mouse/kb hotplug doesn't work
- e.g. if the package ships a systemd unit which references $out, then make-sandboxed won't properly update that unit. - `nix` operations from lappy hang when `desko` is unreachable
- `rmDbusServicesInPlace` is not affected - could at least direct the cache to `http://desko-hn:5001`
- mpv: audiocast has mpv sending its output to the builtin speakers unless manually changed
- syshud (volume overlay): when casting with `blast`, syshud doesn't react to volume changes
- dissent: if i launch it without net connectivity, it gets stuck at the login, and never tries again
- newsflash on moby can't play videos
- "open in browser" works though -- in mpv
- gnome-maps can't use geoclue *and* openstreetmap at the same time
- get gnome-maps to speak xdg-desktop-portal, and this will be fixed
- epiphany can't save cookies
- see under "preferences", cookies are disabled
- prevents logging into websites (OpenStreetMap)
- works when sandbox is disabled
- rsync to ssh target fails because of restrictive sandboxing
- `/mnt/.servo_ftp` retries every 10s, endlessly, rather than doing a linear backoff
- repro by `systemctl stop sftpgo` on servo, then watching `mnt-.servo_ftp.{mount,timer}` on desko
- `ovpns` (and presumably `doof`) net namespaces aren't firewalled
- not great because things like `bitmagnet` expose unprotected admin APIs by default!
- moby: NetworkManager doesn't connect to network until _after_ `systemctl restart NetworkManager`
- probably a dependency ordering issue
- e.g. we try to bring up NetworkManager before bringing up `lo`
- could be a perms issue (over-restrictive sandboxing)
## REFACTORING: ## REFACTORING:
- fold hosts/modules/ into toplevel modules/ - REMOVE DEPRECATED `crypt` from sftpgo_auth_hook
- add import checks to my Python nix-shell scripts
- consolidate ~/dev and ~/ref - consolidate ~/dev and ~/ref
- ~/dev becomes a link to ~/ref/cat/mine - ~/dev becomes a link to ~/ref/cat/mine
- fold hosts/common/home/ssh.nix -> hosts/common/users/colin.nix - fold hosts/common/home/ssh.nix -> hosts/common/users/colin.nix
- don't hardcode IP addresses so much in servo
- modules/netns: migrate `sane.netns.$NS.services = [ FOO ]` option to be `systemd.services.$FOO.sane.netns = NS`
- then change the ExecStartPre check to not ping `ipinfo.net` or whatever.
either port all of `sane-ip-check` to use a self-hosted reflector,
or settle for something like `test -eq "$(ip route get ...)" "$expectedGateway"`
### sops/secrets ### sops/secrets
- rework secrets to leverage `sane.fs`
- remove sops activation script as it's covered by my systemd sane.fs impl
- user secrets could just use `gocryptfs`, like with ~/private? - user secrets could just use `gocryptfs`, like with ~/private?
- can gocryptfs support nested filesystems, each with different perms (for desko, moby, etc)? - can gocryptfs support nested filesystems, each with different perms (for desko, moby, etc)?
### roles
- allow any host to take the role of `uninsane.org`
- will make it easier to test new services?
### upstreaming ### upstreaming
- upstream blueprint-compiler cross fixes -> nixpkgs - add updateScripts to all my packages in nixpkgs
- upstream cargo cross fixes -> nixpkgs - REVIEW/integrate jellyfin dataDir config: <https://github.com/NixOS/nixpkgs/pull/233617>
- upstream `gps-share` package -> nixpkgs
#### upstreaming to non-nixpkgs repos #### upstreaming to non-nixpkgs repos
- gnome-calls: retry net connection when DNS is down
- gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844> - gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844>
- linux: upstream PinePhonePro device trees
- nwg-panel: configurable media controls
- nwg-panel / playerctl hang fix (i think nwg-panel is what should be patched here)
## IMPROVEMENTS: ## IMPROVEMENTS:
- servo: expand /boot to 2 GiB like all other hosts
- moby: port to systemd-boot
- sane-deadlines: show day of the week for upcoming items
- and only show on "first" terminal opened; not on Ctrl+N terminals
- curlftpfs: replace with something better
- safer (rust? actively maintained? sandboxable?)
- handles spaces/symbols in filenames
- has better multi-stream perf (e.g. `sane-sync-music` should be able to copy N items in parallel)
- firefox: open *all* links (http, https, ...) with system handler
- removes the need for open-in-mpv, firefox-xdg-open, etc.
- matrix room links *just work*.
- `network.protocol-handler.external.https = true` in about:config *seems* to do this,
but breaks some webpages (e.g. Pleroma)
- associate http(s)://*.pdf with my pdf handler
- can't do that because lots of applications don't handle URIs
- could workaround using a wrapper that downloads the file and then passes it to the program
- geary: replace with envelope
- likely requires updating envelope to a more recent version (for multi-accounting), and therefore updating libadwaita...
### security/resilience ### security/resilience
- validate duplicity backups!
- encrypt more ~ dirs (~/archives, ~/records, ..?)
- best to do this after i know for sure i have good backups
- /mnt/desko/home, etc, shouldn't include secrets (~/private) - /mnt/desko/home, etc, shouldn't include secrets (~/private)
- 95% of its use is for remote media access and stuff which isn't in VCS (~/records) - 95% of its use is for remote media access and stuff which isn't in VCS (~/records)
- harden systemd services:
- servo: `coturn.service`
- servo: `postgresql.service`
- servo: `postfix.service`
- servo: `prosody.service`
- servo: `slskd.service`
- desko: `usbmuxd.service`
- servo: `backup-torrents.service`
- servo: `dedupe-media.service`
- remove SGID /run/wrappers/bin/sendmail, and just add senders to `postdrop` group
- port all sane.programs to be sandboxed - port all sane.programs to be sandboxed
- sandbox `nix`
- enforce that all `environment.packages` has a sandbox profile (or explicitly opts out) - enforce that all `environment.packages` has a sandbox profile (or explicitly opts out)
- enforce granular dbus sandboxing (bunpen-dbus-*) - revisit "non-sandboxable" apps and check that i'm not actually just missing mountpoints
- make gnome-keyring-daemon less monolithic - LL_FS_RW=/ isn't enough -- need all mount points like `=/:/proc:/sys:...`.
- no reason every application with _a_ secret needs to see _all_ secrets - ensure non-bin package outputs are linked for sandboxed apps
- check out oo7-daemon? - i.e. `outputs.man`, `outputs.debug`, `outputs.doc`, ...
- also unix-pass based provider: <https://github.com/mdellweg/pass_secret_service> - lock down dbus calls within the sandbox
- otherwise anyone can `systemd-run --user ...` to potentially escape a sandbox
- <https://github.com/flatpak/xdg-dbus-proxy>
- remove `.ssh` access from Firefox!
- limit access to `~/knowledge/secrets` through an agent that requires GUI approval, so a firefox exploit can't steal all my logins
- port sane-sandboxed to a compiled language (hare?)
- it adds like 50-70ms launch time _on my laptop_. i'd hate to know how much that is on the pinephone.
- remove /run/wrappers from the sandbox path
- they're mostly useless when using no-new-privs, just an opportunity to forget to specify deps
- make dconf stuff less monolithic - make dconf stuff less monolithic
- i.e. per-app dconf profiles for those which need it. possible static config. - i.e. per-app dconf profiles for those which need it. possible static config.
- flatpak/spectrum has some stuff to proxy dconf per-app - canaries for important services
- rework `programs` API to be just an overlay which wraps each binary in an env with XDG_DATA_DIRS etc set & the config/state links placed in /nix/store instead of $HOME. - e.g. daily email checks; daily backup checks
- integrate `nix check` into Gitea actions?
### user experience ### user experience
- setup a real calendar system, for recurring events - xdg-desktop-portal shouldn't kill children on exit
- rofi: sort items case-insensitively - *maybe* a job for `setsid -f`?
- rofi: enable mouse mode?
- mpv: add media looping controls (e.g. loop song, loop playlist)
- mpv: add/implement an extension to search youtube
- apparently `yt-dlp` does searching!
- replace starship prompt with something more efficient - replace starship prompt with something more efficient
- watch `forkstat`: it does way too much - watch `forkstat`: it does way too much
- cleanup nwg-panel so that it's not invoking swaync every second - cleanup waybar so that it's not invoking playerctl every 2 seconds
- nwg-panel: doesn't know that virtual-desktop 10/TV exists
- install apps: - install apps:
- display QR codes for WiFi endpoints: <https://linuxphoneapps.org/apps/noappid.wisperwind.wifi2qr/> - display QR codes for WiFi endpoints: <https://linuxphoneapps.org/apps/noappid.wisperwind.wifi2qr/>
- shopping list (not in nixpkgs): <https://linuxphoneapps.org/apps/ro.hume.cosmin.shoppinglist/> - shopping list (not in nixpkgs): <https://linuxphoneapps.org/apps/ro.hume.cosmin.shoppinglist/>
- offline Wikipedia (or, add to `wike`) - offline Wikipedia (or, add to `wike`)
- offline docs viewer (gtk): <https://github.com/workbenchdev/Biblioteca>
- some type of games manager/launcher - some type of games manager/launcher
- Gnome Highscore (retro games)?: <https://gitlab.gnome.org/World/highscore> - Gnome Highscore (retro games)?: <https://gitlab.gnome.org/World/highscore>
- better maps for mobile (Osmin (QtQuick)? Pure Maps (Qt/Kirigami)? Gnome Maps is improved in 45)
- note-taking app: <https://linuxphoneapps.org/categories/note-taking/> - note-taking app: <https://linuxphoneapps.org/categories/note-taking/>
- Folio is nice, uses standard markdown, though it only supports flat repos
- OSK overlay specifically for mobile gaming - OSK overlay specifically for mobile gaming
- i.e. mock joysticks, for use with SuperTux and SuperTuxKart - i.e. mock joysticks, for use with SuperTux and SuperTuxKart
- game: Hedgewars
- install mobile-friendly games: - install mobile-friendly games:
- Shattered Pixel Dungeon (nixpkgs `shattered-pixel-dungeon`; doesn't cross-compile b/c openjdk/libIDL) <https://github.com/ebolalex/shattered-pixel-dungeon> - Shattered Pixel Dungeon (nixpkgs `shattered-pixel-dungeon`; doesn't cross-compile b/c openjdk/libIDL) <https://github.com/ebolalex/shattered-pixel-dungeon>
- UnCiv (Civ V clone; nixpkgs `unciv`; doesn't cross-compile): <https://github.com/yairm210/UnCiv> - UnCiv (Civ V clone; nixpkgs `unciv`; doesn't cross-compile): <https://github.com/yairm210/UnCiv>
@@ -132,51 +84,54 @@
- numberlink (generic name for Flow Free). not packaged in Nix - numberlink (generic name for Flow Free). not packaged in Nix
- Neverball (https://neverball.org/screenshots.php). nix: as `neverball` - Neverball (https://neverball.org/screenshots.php). nix: as `neverball`
- blurble (https://linuxphoneapps.org/games/app.drey.blurble/). nix: not as of 2024-02-05 - blurble (https://linuxphoneapps.org/games/app.drey.blurble/). nix: not as of 2024-02-05
- Trivia Quiz (https://linuxphoneapps.org/games/io.github.nokse22.trivia-quiz/)
- sane-sync-music: remove empty dirs
- soulseek: install a CLI app usable over ssh
- moby: replace `spot` with its replacement, `riff` (<https://github.com/Diegovsky/riff>)
#### moby #### moby
- moby: port battery support to something upstreamable
- moby: install transito/mobroute public transit app: <https://sr.ht/~mil/mobroute/> <https://git.sr.ht/~mil/transito>
- see: <https://github.com/NixOS/nixpkgs/pull/335613>
- moby: consider honeybee instead of gnome-calls for calling? <https://git.sr.ht/~anjan/honeybee>
- uses XMPP, so more NAT/WoWLAN-friendly
- fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html> - fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html>
- fix cpupower for better power/perf
- `journalctl -u cpupower --boot` (problem is present on lappy, at least)
- use dynamic DRAM clocking to reduce power by 0.5W: <https://xnux.eu/log/083.html>
- coreboot implements DRAM training for rk3399: <https://gitlab.com/vicencb/kevinboot/-/blob/master/cb/sdram.c>
- moby: tune keyboard layout - moby: tune keyboard layout
- SwayNC/nwg-panel: add option to change audio output - SwayNC:
- Newsflash: sync OPML on start, same way i do with gpodder - don't show MPRIS if no players detected
- better podcasting client? - this is a problem of playerctld, i guess
- hardware upgrade (OnePlus)? - add option to change audio output
- fix colors (red alert) to match overall theme
- moby: tune GPS
- run only geoclue, and not gpsd, to save power?
- tune QGPS setting in eg25-control, for less jitter?
- direct mepo to prefer gpsd, with fallback to geoclue, for better accuracy?
- configure geoclue to do some smoothing?
- manually do smoothing, as some layer between mepo and geoclue/gpsd?
- moby: show battery state on ssh login
- moby: improve gPodder launch time
- moby: theme GTK apps (i.e. non-adwaita styles)
- especially, make the menubar collapsible
- try Gradience tool specifically for theming adwaita? <https://linuxphoneapps.org/apps/com.github.gradienceteam.gradience/>
#### non-moby #### non-moby
- RSS: integrate a paywall bypass - RSS: integrate a paywall bypass
- e.g. self-hosted [ladder](https://github.com/everywall/ladder) (like 12ft.io) - e.g. self-hosted [ladder](https://github.com/everywall/ladder) (like 12ft.io)
- RSS: have podcasts get downloaded straight into ~/Videos/... - neovim: set up language server (lsp; rnix-lsp; nvim-lspconfig)
- and strip the ads out using Whisper transcription + asking a LLM where the ad breaks are - neovim: integrate LLMs
- neovim: integrate ollama - Helix: make copy-to-system clipboard be the default
- neovim: better docsets (e.g. c++, glib) - firefox/librewolf: persist history
- firefox: persist history
- just not cookies or tabs - just not cookies or tabs
- package Nix/NixOS docs for Zeal
- install [doc-browser](https://github.com/qwfy/doc-browser)
- this supports both dash (zeal) *and* the datasets from <https://devdocs.io> (which includes nix!)
- install [devhelp](https://wiki.gnome.org/Apps/Devhelp) (gnome)
- have xdg-open parse `<repo:...> URIs (or adjust them so that it _can_ parse) - have xdg-open parse `<repo:...> URIs (or adjust them so that it _can_ parse)
- sane-bt-search: show details like 5.1 vs stereo, h264 vs h265 - sane-bt-search: show details like 5.1 vs stereo, h264 vs h265
- maybe just color these "keywords" in all search results? - maybe just color these "keywords" in all search results?
- transmission: apply `sane-tag-media` path fix in `torrent-done` script
- many .mkv files do appear to be tagged: i'd just need to add support in my own tooling
- more aggressively cleanup non-media files after DL (ripper logos, info txts)
- uninsane.org: make URLs relative to allow local use (and as offline homepage) - uninsane.org: make URLs relative to allow local use (and as offline homepage)
- email: fix so that local mail doesn't go to junk - email: fix so that local mail doesn't go to junk
- git sendmail flow adds the DKIM signatures, but gets delivered locally w/o having the sig checked, so goes into Junk - git sendmail flow adds the DKIM signatures, but gets delivered locally w/o having the sig checked, so goes into Junk
- could change junk filter from "no DKIM success" to explicit "DKIM failed" - could change junk filter from "no DKIM success" to explicit "DKIM failed"
- add an auto-reply address (e.g. `reply-test@uninsane.org`) which reflects all incoming mail; use this (or a friend running this) for liveness checks
### perf
- debug nixos-rebuild times
- add `pkgs.impure-cached.<foo>` package set to build things with ccache enabled
- every package here can be auto-generated, and marked with some env var so that it doesn't pollute the pure package set
- would be super handy for package prototyping!
## NEW FEATURES: ## NEW FEATURES:
- migrate Kodi box to nix
- migrate MAME cabinet to nix - migrate MAME cabinet to nix
- boot it from PXE from servo? - boot it from PXE from servo?
- enable IPv6 - enable IPv6

View File

@@ -1,5 +1,9 @@
{ ... }@args: # limited, non-flake interface to this repo.
let # this file exposes the same view into `pkgs` which the flake would see when evaluated.
sane-nix-files = import ./pkgs/by-name/sane-nix-files/package.nix { }; #
in # the primary purpose of this file is so i can run `updateScript`s which expect
import "${sane-nix-files}/impure.nix" args # the root to be `default.nix`
{ pkgs ? import <nixpkgs> {} }:
pkgs.appendOverlays [
(import ./overlays/all.nix)
]

View File

@@ -1,33 +0,0 @@
to add a host:
- create the new nix targets
- hosts/by-name/HOST
- let the toplevel (impure.nix) know about HOST
- let the other hosts know about this host (hosts/common/hosts.nix)
- let sops know about the host's pubkey (.sops.yaml)
- re-encrypt all sops keys in secrets/common
- build and flash an image
- optionally expand the rootfs
- `cfdisk /dev/sda2` -> resize partition
- `mount /dev/sda2 boot`
- `btrfs filesystem resize max root`
- setup required persistent directories
- `mkdir -p root/persist/private`
- `gocryptfs -init root/persist/private`
- then boot the device, and for every dangling symlink in ~/.local/share, ~/.cache, do `mkdir -p` on it
- setup host ssh
- `mkdir -p root/persist/plaintext/etc/ssh/host_keys`
- boot the machine and let it create its own ssh keys
- add the pubkey to `hosts/common/hosts.nix`
- setup user ssh
- `ssh-keygen`. don't enter any password; it's stored in a password-encrypted fs.
- add the pubkey to `hosts/common/hosts.nix`
- allow the new host to view secrets
- instructions in hosts/common/secrets.nix
- run `ssh-to-age` on user/host pubkeys
- add age key to .sops.yaml
- update encrypted secrets: `find secrets -type f -exec sops updatekeys -y '{}' ';'`
- setup wireguard keys
- `pk=$(wg genkey)`
- `echo "$pk" | sops encrypt --filename-override secrets/$(hostname)/wg-home.priv.bin --output secrets/$(hostname)/wg-home.priv.bin`
- `pub=$(echo "$pk" | wg pubkey)`
- add pubkey to hosts/common/hosts.nix

View File

@@ -1,49 +0,0 @@
## migrating a host to a new drive
### 1. copy persistent data off of the host:
```sh
$ mkdir -p mnt old/persist
$ mount /dev/$old mnt
$ rsync -arv mnt/persist/ old/persist/
```
### 2. flash the new drive
```
$ nix-build -A hosts.moby.img
$ dd if=$(readlink ./result) of=/dev/$new bs=4M oflag=direct conv=sync status=progress
```
### 3.1. expand the partition
```sh
$ cfdisk /dev/$new
# scroll to the last partition
> Resize
leave at default (max)
> Write
type "yes"
> Quit
```
### 3.2. expand the filesystem
```
$ mkdir -p /mnt/$new
$ mount /dev/$new /mnt/$new
$ btrfs filesystem resize max /mnt/$new
```
### 4. copy data onto the new host
```
$ mkdir /mnt/$new
$ mount /dev/$new /mnt/$new
# if you want to use btrfs snapshots (e.g. snapper), then create the data directory as a subvolume:
$ btrfs subvolume create /mnt/$new/persist
# restore the data
$ rsync -arv old/persist/ /mnt/$new/persist/
```
### 5. ensure/fix ownership
```
$ chmod -R a+rX /mnt/$new/nix
# or, let the nix daemon do it:
$ nix copy --no-check-sigs --to /mnt/$new $(nix-build -A hosts.moby)
```
### 6. insert the disk into the system, and boot!

View File

@@ -1,12 +0,0 @@
## deploying to SD card
- build a toplevel config: `nix build '.#hosts.moby.img'`
- mount a system:
- `mkdir -p root/{nix,boot}`
- `mount /dev/sdX1 root/boot`
- `mount /dev/sdX2 root/nix`
- copy the config:
- `sudo nix copy --no-check-sigs --to root/ $(readlink result)`
- nix will copy stuff to `root/nix/store`
- install the boot files:
- `sudo /nix/store/sbwpwngjlgw4f736ay9hgi69pj3fdwk5-extlinux-conf-builder.sh -d ./root/boot -t 5 -c $(readlink ./result)`
- extlinux-conf-builder can be found in `/run/current-system/bin/switch-to-configuration`

330
flake.lock generated Normal file
View File

@@ -0,0 +1,330 @@
{
"nodes": {
"flake-compat": {
"locked": {
"lastModified": 1688025799,
"narHash": "sha256-ktpB4dRtnksm9F5WawoIkEneh1nrEvuxb5lJFt1iOyw=",
"owner": "nix-community",
"repo": "flake-compat",
"rev": "8bf105319d44f6b9f0d764efa4fdef9f1cc9ba1c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs-wayland",
"nix-eval-jobs",
"nixpkgs"
]
},
"locked": {
"lastModified": 1701473968,
"narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"lib-aggregate": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1711886936,
"narHash": "sha256-D2WENp9GuaCostvNcQ7vElekk0V5cuMdnFZ7NfRhVrQ=",
"owner": "nix-community",
"repo": "lib-aggregate",
"rev": "9c06929b83e57c18d125f1105ba6a423f24083d2",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "lib-aggregate",
"type": "github"
}
},
"mobile-nixos": {
"flake": false,
"locked": {
"lastModified": 1694749521,
"narHash": "sha256-MiVokKlpcJmfoGuWAMeW1En7gZ5hk0rCQArYm6P9XCc=",
"owner": "nixos",
"repo": "mobile-nixos",
"rev": "d25d3b87e7f300d8066e31d792337d9cd7ecd23b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "d25d3b87e7f300d8066e31d792337d9cd7ecd23b",
"repo": "mobile-nixos",
"type": "github"
}
},
"nix-eval-jobs": {
"inputs": {
"flake-parts": "flake-parts",
"nix-github-actions": "nix-github-actions",
"nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1705242886,
"narHash": "sha256-TLj334vRwFtSym3m+NnKcNCnKKPNoTC/TDZL40vmOso=",
"owner": "nix-community",
"repo": "nix-eval-jobs",
"rev": "6b03a93296faf174b97546fd573c8b379f523a8d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-eval-jobs",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"nixpkgs-wayland",
"nix-eval-jobs",
"nixpkgs"
]
},
"locked": {
"lastModified": 1701208414,
"narHash": "sha256-xrQ0FyhwTZK6BwKhahIkUVZhMNk21IEI1nUcWSONtpo=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "93e39cc1a087d65bcf7a132e75a650c44dd2b734",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1703134684,
"narHash": "sha256-SQmng1EnBFLzS7WSRyPM9HgmZP2kLJcPAz+Ug/nug6o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d6863cbcbbb80e71cecfc03356db1cda38919523",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1711846064,
"narHash": "sha256-cqfX0QJNEnge3a77VnytM0Q6QZZ0DziFXt6tSCV8ZSc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "90b1a963ff84dc532db92f678296ff2499a60a87",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs-next-unpatched": {
"locked": {
"lastModified": 1712383280,
"narHash": "sha256-YL8miM11o/jMqOwt5DsdyhPgh/JgCl1kOIzvX7ukniY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "7c74352f2f7eca1925729f5c9c80cb89df8e74a2",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "staging-next",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1711819797,
"narHash": "sha256-tNeB6emxj74Y6ctwmsjtMlzUMn458sBmwnD35U5KIM4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2b4e3ca0091049c6fbb4908c66b05b77eaef9f0c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unpatched": {
"locked": {
"lastModified": 1712398506,
"narHash": "sha256-oopwPeBKBXQEw2BlyK2jEs2farZ5uMjAZU7H4FpGuGE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c58702222e0a29fd01cc42d70737d699995f6389",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-wayland": {
"inputs": {
"flake-compat": "flake-compat",
"lib-aggregate": "lib-aggregate",
"nix-eval-jobs": "nix-eval-jobs",
"nixpkgs": [
"nixpkgs-unpatched"
]
},
"locked": {
"lastModified": 1712237761,
"narHash": "sha256-NoMBBCADTms3yx5BL+sbc7vfDivNiYULO6t9GBAsPt0=",
"owner": "nix-community",
"repo": "nixpkgs-wayland",
"rev": "9b77653338f52da4b498abdf4835efb6ff6e453e",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs-wayland",
"type": "github"
}
},
"root": {
"inputs": {
"mobile-nixos": "mobile-nixos",
"nixpkgs-next-unpatched": "nixpkgs-next-unpatched",
"nixpkgs-unpatched": "nixpkgs-unpatched",
"nixpkgs-wayland": "nixpkgs-wayland",
"sops-nix": "sops-nix",
"uninsane-dot-org": "uninsane-dot-org"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs-unpatched"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1711855048,
"narHash": "sha256-HxegAPnQJSC4cbEbF4Iq3YTlFHZKLiNTk8147EbLdGg=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "99b1e37f9fc0960d064a7862eb7adfb92e64fa10",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs-wayland",
"nix-eval-jobs",
"nixpkgs"
]
},
"locked": {
"lastModified": 1702979157,
"narHash": "sha256-RnFBbLbpqtn4AoJGXKevQMCGhra4h6G2MPcuTSZZQ+g=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "2961375283668d867e64129c22af532de8e77734",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"uninsane-dot-org": {
"inputs": {
"nixpkgs": [
"nixpkgs-unpatched"
]
},
"locked": {
"lastModified": 1711371733,
"narHash": "sha256-+brjlMyLVnVADY31sN82Ap0IsPE2WZEwHUd94sY6BXI=",
"ref": "refs/heads/master",
"rev": "b9502e6f190752d327f8cee7fa4b139094bd7c16",
"revCount": 237,
"type": "git",
"url": "https://git.uninsane.org/colin/uninsane"
},
"original": {
"type": "git",
"url": "https://git.uninsane.org/colin/uninsane"
}
}
},
"root": "root",
"version": 7
}

644
flake.nix Normal file
View File

@@ -0,0 +1,644 @@
# FLAKE FEEDBACK:
# - if flake inputs are meant to be human-readable, a human should be able to easily track them down given the URL.
# - this is not the case with registry URLs, like `nixpkgs/nixos-22.11`.
# - this is marginally the case with schemes like `github:nixos/nixpkgs`.
# - given the *existing* `git+https://` scheme, i propose expressing github URLs similarly:
# - `github+https://github.com/nixos/nixpkgs/tree/nixos-22.11`
# - this would allow for the same optimizations as today's `github:nixos/nixpkgs`, but without obscuring the source.
# a code reader could view the source being referenced simply by clicking the https:// portion of that URI.
# - need some way to apply local patches to inputs.
#
#
# DEVELOPMENT DOCS:
# - Flake docs: <https://nixos.wiki/wiki/Flakes>
# - Flake RFC: <https://github.com/tweag/rfcs/blob/flakes/rfcs/0049-flakes.md>
# - Discussion: <https://github.com/NixOS/rfcs/pull/49>
# - <https://serokell.io/blog/practical-nix-flakes>
#
#
# COMMON OPERATIONS:
# - update a specific flake input:
# - `nix flake lock --update-input nixpkgs`
{
# XXX: use the `github:` scheme instead of the more readable git+https: because it's *way* more efficient
# preferably, i would rewrite the human-readable https URLs to nix-specific github: URLs with a helper,
# but `inputs` is required to be a strict attrset: not an expression.
inputs = {
# branch workflow:
# - daily:
# - nixos-unstable cut from master after enough packages have been built in caches.
# - every 6 hours:
# - master auto-merged into staging and staging-next
# - staging-next auto-merged into staging.
# - manually, approximately once per month:
# - staging-next is cut from staging.
# - staging-next merged into master.
#
# which branch to source from?
# - nixos-unstable: for everyday development; it provides good caching
# - master: temporarily if i'm otherwise cherry-picking lots of already-applied patches
# - staging-next: if testing stuff that's been PR'd into staging, i.e. base library updates.
# - staging: maybe if no staging-next -> master PR has been cut yet?
#
# <https://github.com/nixos/nixpkgs/tree/nixos-unstable>
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-unstable";
nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=master";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-staging";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-staging-next";
nixpkgs-next-unpatched.url = "github:nixos/nixpkgs?ref=staging-next";
nixpkgs-wayland = {
url = "github:nix-community/nixpkgs-wayland";
inputs.nixpkgs.follows = "nixpkgs-unpatched";
};
mobile-nixos = {
# <https://github.com/nixos/mobile-nixos>
# only used for building disk images, not relevant after deployment
# TODO: replace with something else. commit `0f3ac0bef1aea70254a3bae35e3cc2561623f4c1`
# replaces the imageBuilder with a "new implementation from celun" and wildly breaks my use.
# pinning to d25d3b... is equivalent to holding at 2023-09-15
url = "github:nixos/mobile-nixos?ref=d25d3b87e7f300d8066e31d792337d9cd7ecd23b";
flake = false;
};
sops-nix = {
# <https://github.com/Mic92/sops-nix>
# used to distribute secrets to my hosts
url = "github:Mic92/sops-nix";
# inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "nixpkgs-unpatched";
};
uninsane-dot-org = {
# provides the package to deploy <https://uninsane.org>, used only when building the servo host
url = "git+https://git.uninsane.org/colin/uninsane";
# inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "nixpkgs-unpatched";
};
};
outputs = {
self,
nixpkgs-unpatched,
nixpkgs-next-unpatched ? nixpkgs-unpatched,
nixpkgs-wayland,
mobile-nixos,
sops-nix,
uninsane-dot-org,
...
}@inputs:
let
inherit (builtins) attrNames elem listToAttrs map mapAttrs;
# redefine some nixpkgs `lib` functions to avoid the infinite recursion
# of if we tried to use patched `nixpkgs.lib` as part of the patching process.
mapAttrs' = f: set:
listToAttrs (map (attr: f attr set.${attr}) (attrNames set));
optionalAttrs = cond: attrs: if cond then attrs else {};
# mapAttrs but without the `name` argument
mapAttrValues = f: mapAttrs (_: f);
# rather than apply our nixpkgs patches as a flake input, do that here instead.
# this (temporarily?) resolves the bad UX wherein a subflake residing in the same git
# repo as the main flake causes the main flake to have an unstable hash.
patchNixpkgs = variant: nixpkgs: (import ./nixpatches/flake.nix).outputs {
inherit variant nixpkgs;
self = patchNixpkgs variant nixpkgs;
};
nixpkgs' = patchNixpkgs "master" nixpkgs-unpatched;
nixpkgsCompiledBy = system: nixpkgs'.legacyPackages."${system}";
evalHost = { name, local, target, light ? false, nixpkgs ? nixpkgs' }: nixpkgs.lib.nixosSystem {
system = target;
modules = [
{
nixpkgs.buildPlatform.system = local;
# nixpkgs.config.replaceStdenv = { pkgs }: pkgs.ccacheStdenv;
}
(optionalAttrs (local != target) {
# XXX(2023/12/11): cache.nixos.org uses `system = ...` instead of `hostPlatform.system`, and that choice impacts the closure of every package.
# so avoid specifying hostPlatform.system on non-cross builds, so i can use upstream caches.
nixpkgs.hostPlatform.system = target;
})
(optionalAttrs light {
sane.enableSlowPrograms = false;
})
(import ./hosts/instantiate.nix { hostName = name; })
self.nixosModules.default
self.nixosModules.passthru
{
nixpkgs.overlays = [
self.overlays.passthru
self.overlays.sane-all
];
}
];
};
in {
nixosConfigurations = let
hosts = {
servo = { name = "servo"; local = "x86_64-linux"; target = "x86_64-linux"; };
desko = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; };
desko-light = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; light = true; };
lappy = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; };
lappy-light = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; light = true; };
moby = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; };
moby-light = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; light = true; };
rescue = { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; };
};
hostsNext = mapAttrs' (h: v: {
name = "${h}-next";
value = v // { nixpkgs = patchNixpkgs "staging-next" nixpkgs-next-unpatched; };
}) hosts;
in mapAttrValues evalHost (
hosts // hostsNext
);
# unofficial output
# this produces a EFI-bootable .img file (GPT with a /boot partition and a system (/ or /nix) partition).
# after building this:
# - flash it to a bootable medium (SD card, flash drive, HDD)
# - resize the root partition (use cfdisk)
# - mount the part
# - chown root:nixbld <part>/nix/store
# - chown root:root -R <part>/nix/store/*
# - chown root:root -R <part>/persist # if using impermanence
# - populate any important things (persist/, home/colin/.ssh, etc)
# - boot
# - if fs wasn't resized automatically, then `sudo btrfs filesystem resize max /`
# - checkout this flake into /etc/nixos AND UPDATE THE FS UUIDS.
# - `nixos-rebuild --flake './#<host>' switch`
imgs = mapAttrValues (host: host.config.system.build.img) self.nixosConfigurations;
# unofficial output
hostConfigs = mapAttrValues (host: host.config) self.nixosConfigurations;
hostSystems = mapAttrValues (host: host.config.system.build.toplevel) self.nixosConfigurations;
hostPkgs = mapAttrValues (host: host.config.system.build.pkgs) self.nixosConfigurations;
hostPrograms = mapAttrValues (host: mapAttrValues (p: p.package) host.config.sane.programs) self.nixosConfigurations;
patched.nixpkgs = nixpkgs';
overlays = {
# N.B.: `nix flake check` requires every overlay to take `final: prev:` at defn site,
# hence the weird redundancy.
default = final: prev: self.overlays.pkgs final prev;
sane-all = final: prev: import ./overlays/all.nix final prev;
pkgs = final: prev: import ./overlays/pkgs.nix final prev;
pins = final: prev: import ./overlays/pins.nix final prev;
preferences = final: prev: import ./overlays/preferences.nix final prev;
passthru = final: prev:
let
mobile = (import "${mobile-nixos}/overlay/overlay.nix");
uninsane = uninsane-dot-org.overlays.default;
wayland = final: prev: {
# default is to dump the packages into `waylandPkgs` *and* the toplevel.
# but i just want the `waylandPkgs` set
inherit (nixpkgs-wayland.overlays.default final prev)
waylandPkgs
new-wayland-protocols #< 2024/03/10: nixpkgs-wayland assumes this will be in the toplevel
;
};
in
(mobile final prev)
// (uninsane final prev)
// (wayland final prev)
;
};
nixosModules = rec {
default = sane;
sane = import ./modules;
passthru = { ... }: {
imports = [
sops-nix.nixosModules.sops
];
};
};
# this includes both our native packages and all the nixpkgs packages.
legacyPackages =
let
allPkgsFor = sys: (nixpkgsCompiledBy sys).appendOverlays [
self.overlays.passthru self.overlays.pkgs
];
in {
x86_64-linux = allPkgsFor "x86_64-linux";
aarch64-linux = allPkgsFor "aarch64-linux";
};
# extract only our own packages from the full set.
# because of `nix flake check`, we flatten the package set and only surface x86_64-linux packages.
packages = mapAttrs
(system: passthruPkgs: passthruPkgs.lib.filterAttrs
(name: pkg:
# keep only packages which will pass `nix flake check`, i.e. keep only:
# - derivations (not package sets)
# - packages that build for the given platform
(! elem name [ "feeds" "pythonPackagesExtensions" ])
&& (passthruPkgs.lib.meta.availableOn passthruPkgs.stdenv.hostPlatform pkg)
)
(
# expose sane packages and chosen inputs (uninsane.org)
(import ./pkgs { pkgs = passthruPkgs; }) // {
inherit (passthruPkgs) uninsane-dot-org;
}
)
)
# self.legacyPackages;
{
x86_64-linux = (nixpkgsCompiledBy "x86_64-linux").appendOverlays [
self.overlays.passthru
];
}
;
apps."x86_64-linux" =
let
pkgs = self.legacyPackages."x86_64-linux";
sanePkgs = import ./pkgs { inherit pkgs; };
deployScript = host: addr: action: pkgs.writeShellScript "deploy-${host}" ''
set -e
host="${host}"
addr="${addr}"
action="${if action != null then action else ""}"
runOnTarget() {
# run the command ($@) on the machine we're deploying to.
# if that's a remote machine, then do it via ssh, else local shell.
if [ -n "$addr" ]; then
ssh "$addr" "$@"
else
"$@"
fi
}
nix build ".#nixosConfigurations.$host.config.system.build.toplevel" --out-link "./build/result-$host" "$@"
storePath="$(readlink ./build/result-$host)"
# mimic `nixos-rebuild --target-host`, in effect:
# - nix-copy-closure ...
# - nix-env --set ...
# - switch-to-configuration <boot|dry-activate|switch|test|>
# avoid the actual `nixos-rebuild` for a few reasons:
# - fewer nix evals
# - more introspectability and debuggability
# - sandbox friendliness (especially: `git` doesn't have to be run as root)
if [ -n "$addr" ]; then
sudo nix store sign -r -k /run/secrets/nix_serve_privkey "$storePath"
# add more `-v` for more verbosity (up to 5).
# builders-use-substitutes false: optimizes so that the remote machine doesn't try to get paths from its substituters.
# we already have all paths here, and the remote substitution is slow to check and SERIOUSLY flaky on moby in particular.
nix copy -vv --option builders-use-substitutes false --to "ssh-ng://$addr" "$storePath"
fi
if [ -n "$action" ]; then
runOnTarget sudo nix-env -p /nix/var/nix/profiles/system --set "$storePath"
runOnTarget sudo "$storePath/bin/switch-to-configuration" "$action"
fi
'';
deployApp = host: addr: action: {
type = "app";
program = ''${deployScript host addr action}'';
};
# pkg updating.
# a cleaner alternative lives here: <https://discourse.nixos.org/t/how-can-i-run-the-updatescript-of-personal-packages/25274/2>
# mkUpdater :: [ String ] -> { type = "app"; program = path; }
mkUpdater = attrPath: {
type = "app";
program = let
pkg = pkgs.lib.getAttrFromPath attrPath sanePkgs;
strAttrPath = pkgs.lib.concatStringsSep "." attrPath;
commandArgv = pkg.updateScript.command or pkg.updateScript;
command = pkgs.lib.escapeShellArgs commandArgv;
in builtins.toString (pkgs.writeShellScript "update-${strAttrPath}" ''
export UPDATE_NIX_NAME=${pkg.name}
export UPDATE_NIX_PNAME=${pkg.pname}
export UPDATE_NIX_OLD_VERSION=${pkg.version}
export UPDATE_NIX_ATTR_PATH=${strAttrPath}
${command}
'');
};
mkUpdatersNoAliases = opts: basePath: pkgs.lib.concatMapAttrs
(name: pkg:
if pkg.recurseForDerivations or false then {
"${name}" = mkUpdaters opts (basePath ++ [ name ]);
} else if pkg.updateScript or null != null then {
"${name}" = mkUpdater (basePath ++ [ name ]);
} else {}
)
(pkgs.lib.getAttrFromPath basePath sanePkgs);
mkUpdaters = { ignore ? [], flakePrefix ? [] }@opts: basePath:
let
updaters = mkUpdatersNoAliases opts basePath;
invokeUpdater = name: pkg:
let
fullPath = basePath ++ [ name ];
doUpdateByDefault = !builtins.elem fullPath ignore;
# in case `name` has a `.` in it, we have to quote it
escapedPath = builtins.map (p: ''"${p}"'') fullPath;
updatePath = builtins.concatStringsSep "." (flakePrefix ++ escapedPath);
in pkgs.lib.optionalString doUpdateByDefault (
pkgs.lib.escapeShellArgs [
"nix" "run" ".#${updatePath}"
]
);
in {
type = "app";
# top-level app just invokes the updater of everything one layer below it
program = builtins.toString (pkgs.writeShellScript
(builtins.concatStringsSep "-" (flakePrefix ++ basePath))
(builtins.concatStringsSep
"\n"
(pkgs.lib.mapAttrsToList invokeUpdater updaters)
)
);
} // updaters;
in {
help = {
type = "app";
program = let
helpMsg = builtins.toFile "nixos-config-help-message" ''
commands:
- `nix run '.#help'`
- show this message
- `nix run '.#update.pkgs'`
- updates every package
- `nix run '.#update.feeds'`
- updates metadata for all feeds
- `nix run '.#init-feed' <url>`
- `nix run '.#deploy.{desko,lappy,moby,servo}[-light|-test]' [nix args ...]`
- build and deploy the host
- `nix run '.#preDeploy.{desko,lappy,moby,servo}[-light]' [nix args ...]`
- copy closures to a host, but don't activate it
- or `nix run '.#preDeploy'` to target all hosts
- `nix run '.#check'`
- make sure all systems build; NUR evaluates
- `nix run '.#bench'`
- benchmark the eval time of common targets this flake provides
specific build targets of interest:
- `nix build '.#imgs.rescue'`
'';
in builtins.toString (pkgs.writeShellScript "nixos-config-help" ''
cat ${helpMsg}
echo ""
echo "complete flake structure:"
nix flake show --option allow-import-from-derivation true
'');
};
# wrangle some names to get package updaters which refer back into the flake, but also conditionally ignore certain paths (e.g. sane.feeds).
# TODO: better design
update = rec {
_impl.pkgs.sane = mkUpdaters { flakePrefix = [ "update" "_impl" "pkgs" ]; ignore = [ [ "sane" "feeds" ] ]; } [ "sane" ];
pkgs = _impl.pkgs.sane;
_impl.feeds.sane.feeds = mkUpdaters { flakePrefix = [ "update" "_impl" "feeds" ]; } [ "sane" "feeds" ];
feeds = _impl.feeds.sane.feeds;
};
init-feed = {
type = "app";
program = "${pkgs.feeds.init-feed}";
};
deploy = {
desko = deployApp "desko" "desko" "switch";
desko-light = deployApp "desko-light" "desko" "switch";
lappy = deployApp "lappy" "lappy" "switch";
lappy-light = deployApp "lappy-light" "lappy" "switch";
moby = deployApp "moby" "moby" "switch";
moby-light = deployApp "moby-light" "moby" "switch";
moby-test = deployApp "moby" "moby" "test";
servo = deployApp "servo" "servo" "switch";
# like `nixos-rebuild --flake . switch`
self = deployApp "$(hostname)" "" "switch";
self-light = deployApp "$(hostname)-light" "" "switch";
type = "app";
program = builtins.toString (pkgs.writeShellScript "deploy-all" ''
nix run '.#deploy.lappy'
nix run '.#deploy.moby'
nix run '.#deploy.desko'
nix run '.#deploy.servo'
'');
};
preDeploy = {
# build the host and copy the runtime closure to that host, but don't activate it.
desko = deployApp "desko" "desko" null;
desko-light = deployApp "desko-light" "desko" null;
lappy = deployApp "lappy" "lappy" null;
lappy-light = deployApp "lappy-light" "lappy" null;
moby = deployApp "moby" "moby" null;
moby-light = deployApp "moby-light" "moby" null;
servo = deployApp "servo" "servo" null;
type = "app";
program = builtins.toString (pkgs.writeShellScript "predeploy-all" ''
# copy the -light variants first; this might be run while waiting on a full build. or the full build failed.
nix run '.#preDeploy.moby-light' -- "$@"
nix run '.#preDeploy.lappy-light' -- "$@"
nix run '.#preDeploy.desko-light' -- "$@"
nix run '.#preDeploy.lappy' -- "$@"
nix run '.#preDeploy.servo' -- "$@"
nix run '.#preDeploy.moby' -- "$@"
nix run '.#preDeploy.desko' -- "$@"
'');
};
sync = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-all" ''
RC_lappy=$(nix run '.#sync.lappy' -- "$@")
RC_moby=$(nix run '.#sync.moby' -- "$@")
RC_desko=$(nix run '.#sync.desko' -- "$@")
echo "lappy: $RC_lappy"
echo "moby: $RC_moby"
echo "desko: $RC_desko"
'');
};
sync.desko = {
# copy music from servo to desko
# can run this from any device that has ssh access to desko and servo
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-desko" ''
sudo mount /mnt/desko/home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compat /mnt/servo/media/Music /mnt/desko/home/Music "$@"
'');
};
sync.lappy = {
# copy music from servo to lappy
# can run this from any device that has ssh access to lappy and servo
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-lappy" ''
sudo mount /mnt/lappy/home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat /mnt/servo/media/Music /mnt/lappy/home/Music "$@"
'');
};
sync.moby = {
# copy music from servo to moby
# can run this from any device that has ssh access to moby and servo
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-moby" ''
sudo mount /mnt/moby/home
sudo mount /mnt/desko/home
${pkgs.rsync}/bin/rsync -arv --exclude servo-macros /mnt/moby/home/Pictures/ /mnt/desko/home/Pictures/moby/
# N.B.: limited by network/disk -> reduce job count to improve pause/resume behavior
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat --jobs 4 /mnt/servo/media/Music /mnt/moby/home/Music "$@"
'');
};
check = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "check-all" ''
nix run '.#check.nur'
RC0=$?
nix run '.#check.hostConfigs'
RC1=$?
nix run '.#check.rescue'
RC2=$?
echo "nur: $RC0"
echo "hostConfigs: $RC1"
echo "rescue: $RC2"
exit $(($RC0 | $RC1 | $RC2))
'');
};
check.nur = {
# `nix run '.#check-nur'`
# validates that my repo can be included in the Nix User Repository
type = "app";
program = builtins.toString (pkgs.writeShellScript "check-nur" ''
cd ${./.}/integrations/nur
NIX_PATH= NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix-env -f . -qa \* --meta --xml \
--allowed-uris https://static.rust-lang.org \
--option restrict-eval true \
--option allow-import-from-derivation true \
--drv-path --show-trace \
-I nixpkgs=${nixpkgs-unpatched} \
-I nixpkgs-overlays=${./.}/hosts/common/nix/overlay \
-I ../../ \
| tee # tee to prevent interactive mode
'');
};
check.hostConfigs = {
type = "app";
program = let
checkHost = host: let
shellHost = pkgs.lib.replaceStrings [ "-" ] [ "_" ] host;
in ''
nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./build/result-${host} -j2 "$@"
RC_${shellHost}=$?
'';
in builtins.toString (pkgs.writeShellScript
"check-host-configs"
''
# build minimally-usable hosts first, then their full image.
# this gives me a minimal image i can deploy or copy over, early.
${checkHost "desko-light"}
${checkHost "moby-light"}
${checkHost "lappy-light"}
${checkHost "desko"}
${checkHost "lappy"}
${checkHost "servo"}
${checkHost "moby"}
${checkHost "rescue"}
# still want to build the -light variants first so as to avoid multiple simultaneous webkitgtk builds
${checkHost "desko-light-next"}
${checkHost "moby-light-next"}
${checkHost "desko-next"}
${checkHost "lappy-next"}
${checkHost "servo-next"}
${checkHost "moby-next"}
${checkHost "rescue-next"}
echo "desko: $RC_desko"
echo "lappy: $RC_lappy"
echo "servo: $RC_servo"
echo "moby: $RC_moby"
echo "rescue: $RC_rescue"
echo "desko-next: $RC_desko_next"
echo "lappy-next: $RC_lappy_next"
echo "servo-next: $RC_servo_next"
echo "moby-next: $RC_moby_next"
echo "rescue-next: $RC_rescue_next"
# i don't really care if the -next hosts fail. i build them mostly to keep the cache fresh/ready
exit $(($RC_desko | $RC_lappy | $RC_servo | $RC_moby | $RC_rescue))
''
);
};
check.rescue = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "check-rescue" ''
nix build -v '.#imgs.rescue' --out-link ./build/result-rescue-img -j2
'');
};
bench = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "bench" ''
doBench() {
attrPath="$1"
shift
echo -n "benchmarking eval of '$attrPath'... "
/run/current-system/sw/bin/time -f "%e sec" -o /dev/stdout \
nix eval --no-eval-cache --quiet --raw ".#$attrPath" --apply 'result: if result != null then "" else "unexpected null"' $@ 2> /dev/null
}
if [ -n "$1" ]; then
doBench "$@"
else
doBench hostConfigs
doBench hostConfigs.lappy
doBench hostConfigs.lappy.sane.programs
doBench hostConfigs.lappy.sane.users.colin
doBench hostConfigs.lappy.sane.fs
doBench hostConfigs.lappy.environment.systemPackages
fi
'');
};
};
templates = {
env.python-data = {
# initialize with:
# - `nix flake init -t '/home/colin/dev/nixos/#env.python-data'`
# then enter with:
# - `nix develop`
path = ./templates/env/python-data;
description = "python environment for data processing";
};
pkgs.rust-inline = {
# initialize with:
# - `nix flake init -t '/home/colin/dev/nixos/#pkgs.rust-inline'`
path = ./templates/pkgs/rust-inline;
description = "rust package and development environment (inline rust sources)";
};
pkgs.rust = {
# initialize with:
# - `nix flake init -t '/home/colin/dev/nixos/#pkgs.rust'`
path = ./templates/pkgs/rust;
description = "rust package fit to ship in nixpkgs";
};
pkgs.make = {
# initialize with:
# - `nix flake init -t '/home/colin/dev/nixos/#pkgs.make'`
path = ./templates/pkgs/make;
description = "default Makefile-based derivation";
};
};
};
}

View File

@@ -1,19 +0,0 @@
# MAME arcade cabinet
# Raspberry Pi 400:
# - quad-core Cortex-A72 @ 1.8 GHz (ARMv8-A 64; BCM2711)
# - 4GiB RAM
{ ... }:
{
imports = [
./fs.nix
];
sane.hal.rpi-400.enable = true;
sane.roles.client = true; # for WiFi creds
# TODO: port to `sane.programs` interface
services.xserver.desktopManager.kodi.enable = true;
# /boot space is at a premium, especially with uncompressed kernels. default was 20.
# boot.loader.generic-extlinux-compatible.configurationLimit = 10;
}

View File

@@ -1,17 +0,0 @@
{ ... }:
{
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/cccccccc-aaaa-dddd-eeee-000020250621";
fsType = "btrfs";
options = [
"compress=zstd"
"defaults"
];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/2025-0621";
fsType = "vfat";
};
}

View File

@@ -1,33 +0,0 @@
# Samsung chromebook XE303C12
# - <https://wiki.postmarketos.org/wiki/Samsung_Chromebook_(google-snow)>
{ ... }:
{
imports = [
./fs.nix
];
sane.hal.samsung.enable = true;
sane.roles.client = true;
# sane.roles.pc = true;
users.users.colin.initialPassword = "147147";
sane.programs.sway.enableFor.user.colin = true;
sane.programs.calls.enableFor.user.colin = false;
sane.programs.consoleMediaUtils.enableFor.user.colin = true;
sane.programs.epiphany.enableFor.user.colin = true;
sane.programs.geary.enableFor.user.colin = false;
# sane.programs.firefox.enableFor.user.colin = true;
sane.programs.portfolio-filemanager.enableFor.user.colin = true;
sane.programs.signal-desktop.enableFor.user.colin = false;
sane.programs.wike.enableFor.user.colin = true;
sane.programs.dino.config.autostart = false;
sane.programs.dissent.config.autostart = false;
sane.programs.fractal.config.autostart = false;
sane.programs.sway.config.mod = "Mod1"; #< alt key instead of Super
# sane.programs.guiApps.enableFor.user.colin = false;
# sane.programs.pcGuiApps.enableFor.user.colin = false; #< errors!
}

View File

@@ -1,16 +0,0 @@
{ ... }:
{
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/55555555-0303-0c12-86df-eda9e9311526";
fsType = "btrfs";
options = [
"compress=zstd"
"defaults"
];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/303C-5A37";
fsType = "vfat";
};
}

View File

@@ -1,57 +1,64 @@
{ config, lib, ... }: { config, pkgs, ... }:
{ {
imports = [ imports = [
./fs.nix ./fs.nix
]; ];
# firewall has to be open to allow clients to use services hosted on this device,
# like `ollama`
sane.ports.openFirewall = true;
# sane.programs.devPkgs.enableFor.user.colin = true;
# sane.guest.enable = true; # sane.guest.enable = true;
# don't enable wifi by default: it messes with connectivity. # services.distccd.enable = true;
# systemd.services.iwd.enable = false; # sane.programs.distcc.enableFor.user.guest = true;
# networking.wireless.enable = false;
# systemd.services.wpa_supplicant.enable = false; # TODO: remove emulation, but need to fix nixos-rebuild to moby for that.
# sane.programs.wpa_supplicant.enableFor.user.colin = lib.mkForce false; # sane.roles.build-machine.emulation = true;
# sane.programs.wpa_supplicant.enableFor.system = lib.mkForce false;
# don't auto-connect to wifi networks
# see: <https://networkmanager.dev/docs/api/latest/NetworkManager.conf.html#device-spec>
networking.networkmanager.unmanaged = [ "type:wifi" ];
sops.secrets.colin-passwd.neededForUsers = true; sops.secrets.colin-passwd.neededForUsers = true;
sane.ports.openFirewall = true; # for e.g. nix-serve
sane.roles.build-machine.enable = true; sane.roles.build-machine.enable = true;
sane.roles.client = true; sane.roles.client = true;
sane.roles.dev-machine = true;
sane.roles.pc = true; sane.roles.pc = true;
sane.roles.work = true;
sane.services.ollama.enable = lib.mkIf (config.sane.maxBuildCost >= 3) true;
sane.services.wg-home.enable = true; sane.services.wg-home.enable = true;
sane.ovpn.addrV4 = "172.26.55.21"; sane.services.wg-home.ip = config.sane.hosts.by-name."desko".wg-home.ip;
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:20c1:a73c"; sane.services.duplicity.enable = true;
sane.services.rsync-net.enable = true; sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path;
sane.nixcache.substituters.desko = false;
sane.nixcache.remote-builders.desko = false; sane.nixcache.remote-builders.desko = false;
sane.programs.firefox.config.formFactor = "desktop";
sane.programs.sane-private-unlock-remote.enableFor.user.colin = true;
sane.programs.sane-private-unlock-remote.config.hosts = [ "servo" ];
sane.programs.sway.enableFor.user.colin = true; sane.programs.sway.enableFor.user.colin = true;
sane.programs.iphoneUtils.enableFor.user.colin = true;
sane.programs.steam.enableFor.user.colin = true; sane.programs.steam.enableFor.user.colin = true;
sane.programs.nwg-panel.config = { # sane.programs.devPkgs.enableFor.user.colin = true;
battery = false;
brightness = false;
};
sane.programs.mpv.config.defaultProfile = "high-quality"; sane.programs."gnome.geary".config.autostart = true;
sane.programs.signal-desktop.config.autostart = true;
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
# needed to use libimobiledevice/ifuse, for iphone sync # needed to use libimobiledevice/ifuse, for iphone sync
services.usbmuxd.enable = true; services.usbmuxd.enable = true;
hardware.amdgpu.opencl.enable = true; # desktop (AMD's opencl implementation AKA "ROCM"); probably required for ollama # don't enable wifi by default: it messes with connectivity.
systemd.services.iwd.enable = false;
systemd.services.wpa_supplicant.enable = false;
# default config: https://man.archlinux.org/man/snapper-configs.5
# defaults to something like:
# - hourly snapshots
# - auto cleanup; keep the last 10 hourlies, last 10 daylies, last 10 monthlys.
services.snapper.configs.nix = {
# TODO: for the impermanent setup, we'd prefer to just do /nix/persist,
# but that also requires setting up the persist dir as a subvol
SUBVOLUME = "/nix";
# TODO: ALLOW_USERS doesn't seem to work. still need `sudo snapper -c nix list`
ALLOW_USERS = [ "colin" ];
};
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
system.stateVersion = "21.05";
} }

View File

@@ -3,10 +3,10 @@
{ {
# increase /tmp space (defaults to 50% of RAM) for building large nix things. # increase /tmp space (defaults to 50% of RAM) for building large nix things.
# a cross-compiled kernel, particularly, will easily use 30+GB of tmp # a cross-compiled kernel, particularly, will easily use 30+GB of tmp
fileSystems."/tmp".options = [ "size=128G" ]; fileSystems."/tmp".options = [ "size=64G" ];
fileSystems."/nix" = { fileSystems."/nix" = {
device = "/dev/disk/by-uuid/dddddddd-eeee-5555-cccc-000020250527"; device = "/dev/disk/by-uuid/845d85bf-761d-431b-a406-e6f20909154f";
fsType = "btrfs"; fsType = "btrfs";
options = [ options = [
"compress=zstd" "compress=zstd"
@@ -15,7 +15,7 @@
}; };
fileSystems."/boot" = { fileSystems."/boot" = {
device = "/dev/disk/by-uuid/2025-0527"; device = "/dev/disk/by-uuid/5049-9AFD";
fsType = "vfat"; fsType = "vfat";
}; };
} }

View File

@@ -1,58 +0,0 @@
{ lib, pkgs, ... }:
{
imports = [
./fs.nix
];
sane.roles.client = true;
sane.roles.pc = true;
sane.roles.work = true;
sane.services.wg-home.enable = true;
# sane.ovpn.addrV4 = "172.23.119.72";
# sane.guest.enable = true;
sane.programs.sane-private-unlock-remote.enableFor.user.colin = true;
sane.programs.sane-private-unlock-remote.config.hosts = [ "servo" ];
sane.programs.firefox.config.formFactor = "laptop";
sane.programs.itgmania.enableFor.user.colin = true;
sane.programs.sway.enableFor.user.colin = true;
sops.secrets.colin-passwd.neededForUsers = true;
sane.services.rsync-net.enable = true;
# add an entry to boot into Windows, as if it had been launched directly from the BIOS.
boot.loader.systemd-boot.rebootForBitlocker = true;
boot.loader.systemd-boot.windows.primary.efiDeviceHandle = "HD0b";
system.activationScripts.makeDefaultBootEntry = {
text = let
makeDefaultBootEntry = pkgs.writeShellApplication {
name = "makeDefaultBootEntry";
runtimeInputs = with pkgs; [
efibootmgr
gnugrep
];
text = ''
# configure the EFI firmware to boot into NixOS by default.
# do this by querying the active boot entry, and just making that be the default.
# this is needed on flowy because enabling secure boot / booting into Windows
# resets the default boot order; manually reconfiguring that is tiresome.
efi=$(efibootmgr)
bootCurrent=$(echo "$efi" | grep '^BootCurrent: ')
bootCurrent=''${bootCurrent/BootCurrent: /}
bootOrder=$(echo "$efi" | grep '^BootOrder: ')
bootOrder=''${bootOrder/BootOrder: /}
if ! [[ "$bootOrder" =~ ^"$bootCurrent", ]]; then
# booted entry was not the default,
# so prepend it to the boot order:
newBootOrder="$bootCurrent,$bootOrder"
(set -x; efibootmgr -o "$newBootOrder")
fi
'';
};
in lib.getExe makeDefaultBootEntry;
};
}

View File

@@ -1,17 +0,0 @@
{ ... }:
{
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/ffffffff-1111-0000-eeee-000020250531";
fsType = "btrfs";
options = [
"compress=zstd"
"defaults"
];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/2025-0531";
fsType = "vfat";
};
}

View File

@@ -1,37 +1,37 @@
{ lib, ... }: { config, pkgs, ... }:
{ {
imports = [ imports = [
./fs.nix ./fs.nix
]; ];
sane.roles.client = true; sane.roles.client = true;
sane.roles.dev-machine = true;
sane.roles.pc = true; sane.roles.pc = true;
sane.services.wg-home.enable = true; sane.services.wg-home.enable = true;
sane.ovpn.addrV4 = "172.23.119.72"; sane.services.wg-home.ip = config.sane.hosts.by-name."lappy".wg-home.ip;
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:0332:aa96/128";
# sane.guest.enable = true; # sane.guest.enable = true;
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
sane.programs.sane-private-unlock-remote.enableFor.user.colin = true;
sane.programs.sane-private-unlock-remote.config.hosts = [ "servo" ];
sane.programs.firefox.config.formFactor = "laptop";
sane.programs.itgmania.enableFor.user.colin = true;
# sane.programs.stepmania.enableFor.user.colin = true; #< TODO: fix build
sane.programs.sway.enableFor.user.colin = true; sane.programs.sway.enableFor.user.colin = true;
sane.programs."gnome.geary".config.autostart = true;
sane.programs.signal-desktop.config.autostart = true;
sane.programs.stepmania.enableFor.user.colin = true;
sops.secrets.colin-passwd.neededForUsers = true; sops.secrets.colin-passwd.neededForUsers = true;
sane.services.rsync-net.enable = true; # default config: https://man.archlinux.org/man/snapper-configs.5
# defaults to something like:
# - hourly snapshots
# - auto cleanup; keep the last 10 hourlies, last 10 daylies, last 10 monthlys.
services.snapper.configs.nix = {
# TODO: for the impermanent setup, we'd prefer to just do /nix/persist,
# but that also requires setting up the persist dir as a subvol
SUBVOLUME = "/nix";
ALLOW_USERS = [ "colin" ];
};
# starting 2024/09, under default settings (apparently 256 quantum), audio would crackle under load. # docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
# 1024 solves *most* crackles, but still noticable under heavier loads. system.stateVersion = "21.05";
sane.programs.pipewire.config.min-quantum = 2048;
# limit how many snapshots we keep, due to extremely limited disk space (TODO: remove this override after upgrading lappy hard drive)
services.snapper.configs.root.TIMELINE_LIMIT_HOURLY = lib.mkForce 2;
services.snapper.configs.root.TIMELINE_LIMIT_DAILY = lib.mkForce 2;
services.snapper.configs.root.TIMELINE_LIMIT_WEEKLY = lib.mkForce 0;
services.snapper.configs.root.TIMELINE_LIMIT_MONTHLY = lib.mkForce 0;
services.snapper.configs.root.TIMELINE_LIMIT_YEARLY = lib.mkForce 0;
} }

View File

@@ -0,0 +1,7 @@
xkb_keymap {
xkb_keycodes { include "evdev+aliases(qwerty)" };
xkb_types { include "complete" };
xkb_compat { include "complete" };
xkb_symbols { include "pc+us+inet(evdev)" };
xkb_geometry { include "pc(pc105)" };
};

View File

@@ -0,0 +1,22 @@
# tow-boot: <https://tow-boot.org>
# docs (pinephone specific): <https://github.com/Tow-Boot/Tow-Boot/tree/development/boards/pine64-pinephoneA64>
# LED and button behavior is defined here: <https://github.com/Tow-Boot/Tow-Boot/blob/development/modules/tow-boot/phone-ux.nix>
# - hold VOLDOWN: enter recovery mode
# - LED will turn aqua instead of yellow
# - recovery mode would ordinarily allow a selection of entries, but for pinephone i guess it doesn't do anything?
# - hold VOLUP: force it to load the OS from eMMC?
# - LED will turn blue instead of yellow
# boot LEDs:
# - yellow = entered tow-boot
# - 10 red flashes => poweroff means tow-boot couldn't boot into the next stage (i.e. distroboot)
# - distroboot: <https://source.denx.de/u-boot/u-boot/-/blob/v2022.04/doc/develop/distro.rst>)
{ config, pkgs, ... }:
{
# we need space in the GPT header to place tow-boot.
# only actually need 1 MB, but better to over-allocate than under-allocate
sane.image.extraGPTPadding = 16 * 1024 * 1024;
sane.image.firstPartGap = 0;
sane.image.installBootloader = ''
dd if=${pkgs.tow-boot-pinephone}/Tow-Boot.noenv.bin of=$out/nixos.img bs=1024 seek=8 conv=notrunc
'';
}

View File

@@ -1,4 +1,7 @@
# Pinephone # Pinephone
# other setups to reference:
# - <https://hamblingreen.gitlab.io/2022/03/02/my-pinephone-setup.html>
# - sxmo Arch user. lots of app recommendations
# #
# wikis, resources, ...: # wikis, resources, ...:
# - Linux Phone Apps: <https://linuxphoneapps.org/> # - Linux Phone Apps: <https://linuxphoneapps.org/>
@@ -6,42 +9,71 @@
# - Mobian wiki: <https://wiki.mobian-project.org/doku.php?id=start> # - Mobian wiki: <https://wiki.mobian-project.org/doku.php?id=start>
# - recommended apps, chatrooms # - recommended apps, chatrooms
{ ... }: { config, pkgs, lib, ... }:
{ {
imports = [ imports = [
./bootloader.nix
./fs.nix ./fs.nix
./gps.nix
./kernel.nix
./polyfill.nix
]; ];
sane.hal.pine64-pinephone-pro.enable = true;
sane.roles.client = true; sane.roles.client = true;
sane.roles.handheld = true; sane.roles.handheld = true;
sane.zsh.showDeadlines = false; # unlikely to act on them when in shell
sane.services.wg-home.enable = true; sane.services.wg-home.enable = true;
sane.ovpn.addrV4 = "172.24.87.255"; sane.services.wg-home.ip = config.sane.hosts.by-name."moby".wg-home.ip;
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:18cd:a72b";
# for some reason desko -> moby deploys are super flaky when desko is also a nixcache (not true of desko -> lappy deploys, though!)
# > unable to download 'http://desko:5001/<hash>.narinfo': Server returned nothing (no headers, no data) (52)
sane.nixcache.substituters.desko = false;
# XXX colin: phosh doesn't work well with passwordless login, # XXX colin: phosh doesn't work well with passwordless login,
# so set this more reliable default password should anything go wrong # so set this more reliable default password should anything go wrong
users.users.colin.initialPassword = "147147"; users.users.colin.initialPassword = "147147";
# services.getty.autologinUser = "root"; # allows for emergency maintenance?
sops.secrets.colin-passwd.neededForUsers = true; sops.secrets.colin-passwd.neededForUsers = true;
sane.services.rsync-net.enable = true; # sane.gui.sxmo.enable = true;
sane.programs.sway.enableFor.user.colin = true; sane.programs.sway.enableFor.user.colin = true;
sane.programs.sway.config.mod = "Mod1"; #< alt key instead of Super sane.programs.swaylock.enableFor.user.colin = false; #< not usable on touch
sane.programs.schlock.enableFor.user.colin = true;
sane.programs.swayidle.config.actions.screenoff.delay = 300;
sane.programs.swayidle.config.actions.screenoff.enable = true;
sane.programs.sane-input-handler.enableFor.user.colin = true;
sane.programs.blueberry.enableFor.user.colin = false; # bluetooth manager: doesn't cross compile!
sane.programs.fcitx5.enableFor.user.colin = false; # does not cross compile
sane.programs.mercurial.enableFor.user.colin = false; # does not cross compile
sane.programs.nvme-cli.enableFor.system = false; # does not cross compile (libhugetlbfs)
# enabled for easier debugging # enabled for easier debugging
sane.programs.eg25-control.enableFor.user.colin = true; sane.programs.eg25-control.enableFor.user.colin = true;
# sane.programs.rtl8723cs-wowlan.enableFor.user.colin = true; sane.programs.rtl8723cs-wowlan.enableFor.user.colin = true;
# sane.programs.eg25-manager.enableFor.user.colin = true;
# sane.programs.ntfy-sh.config.autostart = true; # sane.programs.ntfy-sh.config.autostart = true;
sane.programs.dino.config.autostart = true; sane.programs.dino.config.autostart = true;
sane.programs.signal-desktop.config.autostart = false; # sane.programs.signal-desktop.config.autostart = true; # TODO: enable once electron stops derping.
sane.programs.geary.config.autostart = false; # sane.programs."gnome.geary".config.autostart = true;
# sane.programs.calls.config.autostart = true;
sane.programs.pipewire.config = { sane.programs.firefox.mime.priority = 300; # prefer other browsers when possible
# tune so Dino doesn't drop audio # HACK/TODO: make `programs.P.env.VAR` behave according to `mime.priority`
sane.programs.firefox.env = lib.mkForce {};
sane.programs.epiphany.env.BROWSER = "epiphany";
# note the .conf.d approach: using ~/.config/pipewire/pipewire.conf directly breaks all audio,
# presumably because that deletes the defaults entirely whereas the .conf.d approach selectively overrides defaults
sane.user.fs.".config/pipewire/pipewire.conf.d/10-fix-dino-mic-cutout.conf".symlink.text = ''
# config docs: <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire#properties>
# useful to run `pw-top` to see that these settings are actually having effect,
# and `pw-metadata` to see if any settings conflict (e.g. max-quantum < min-quantum)
#
# restart pipewire after editing these files:
# - `systemctl --user restart pipewire`
# - pipewire users will likely stop outputting audio until they are also restarted
#
# there's seemingly two buffers for the mic (see: <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained>) # there's seemingly two buffers for the mic (see: <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained>)
# 1. Pipewire buffering out of the driver and into its own member. # 1. Pipewire buffering out of the driver and into its own member.
# 2. Pipewire buffering into Dino. # 2. Pipewire buffering into Dino.
@@ -52,16 +84,67 @@
# `pw-metadata -n settings 0 clock.force-quantum 1024` reduces to about 1 error per second. # `pw-metadata -n settings 0 clock.force-quantum 1024` reduces to about 1 error per second.
# `pw-metadata -n settings 0 clock.force-quantum 2048` reduces to 1 error every < 10s. # `pw-metadata -n settings 0 clock.force-quantum 2048` reduces to 1 error every < 10s.
# pipewire default config includes `clock.power-of-two-quantum = true` # pipewire default config includes `clock.power-of-two-quantum = true`
min-quantum = 2048; context.properties = {
max-quantum = 8192; default.clock.min-quantum = 2048
default.clock.max-quantum = 8192
}
'';
boot.loader.efi.canTouchEfiVariables = false;
# /boot space is at a premium. default was 20.
# even 10 can be too much
boot.loader.generic-extlinux-compatible.configurationLimit = 8;
# mobile.bootloader.enable = false;
# mobile.boot.stage-1.enable = false;
# boot.initrd.systemd.enable = false;
# boot.initrd.services.swraid.enable = false; # attempt to fix dm_mod stuff
# hardware.firmware makes the referenced files visible to the kernel, for whenever a driver explicitly asks for them.
# these files are visible from userspace by following `/sys/module/firmware_class/parameters/path`
#
# mobile-nixos' /lib/firmware includes:
# rtl_bt (bluetooth)
# anx7688-fw.bin (USB-C chip: power negotiation, HDMI/dock)
# ov5640_af.bin (camera module)
# hardware.firmware = [ config.mobile.device.firmware ];
# hardware.firmware = [ pkgs.rtl8723cs-firmware ];
hardware.firmware = [
(pkgs.linux-firmware-megous.override {
# rtl_bt = false probably means no bluetooth connectivity.
# N.B.: DON'T RE-ENABLE without first confirming that wake-on-lan works during suspend (rtcwake).
# it seems the rtl_bt stuff ("bluetooth coexist") might make wake-on-LAN radically more flaky.
rtl_bt = false;
})
];
system.stateVersion = "21.11";
# defined: https://www.freedesktop.org/software/systemd/man/machine-info.html
# XXX colin: not sure which, if any, software makes use of this
environment.etc."machine-info".text = ''
CHASSIS="handset"
'';
# enable rotation sensor
hardware.sensor.iio.enable = true;
# TODO: move elsewhere...
systemd.services.ModemManager.serviceConfig = {
# N.B.: the extra "" in ExecStart serves to force upstream ExecStart to be ignored
ExecStart = [ "" "${pkgs.modemmanager}/bin/ModemManager --debug" ];
# --debug sets DEBUG level logging: so reset
ExecStartPost = [ "${pkgs.modemmanager}/bin/mmcli --set-logging=INFO" ];
}; };
sane.programs.mpv.config.defaultProfile = "fast"; services.udev.extraRules = let
chmod = "${pkgs.coreutils}/bin/chmod";
chown = "${pkgs.coreutils}/bin/chown";
in ''
# make Pinephone flashlight writable by user.
# taken from postmarketOS: <repo:postmarketOS/pmaports:device/main/device-pine64-pinephone/60-flashlight.rules>
SUBSYSTEM=="leds", DEVPATH=="*/*:flash", RUN+="${chmod} g+w /sys%p/brightness /sys%p/flash_strobe", RUN+="${chown} :video /sys%p/brightness /sys%p/flash_strobe"
# /boot space is at a premium, especially with uncompressed kernels. default was 20. # make Pinephone front LEDs writable by user.
# boot.loader.generic-extlinux-compatible.configurationLimit = 10; SUBSYSTEM=="leds", DEVPATH=="*/*:indicator", RUN+="${chmod} g+w /sys%p/brightness", RUN+="${chown} :video /sys%p/brightness"
'';
# TODO: switch to systemd-boot
boot.loader.generic-extlinux-compatible.enable = true;
boot.loader.systemd-boot.enable = false;
} }

View File

@@ -0,0 +1,69 @@
# pinephone GPS happens in EG25 modem
# serial control interface to modem is /dev/ttyUSB2
# after enabling GPS, readout is /dev/ttyUSB1
#
# minimal process to enable modem and GPS:
# - `echo 1 > /sys/class/modem-power/modem-power/device/powered`
# - `screen /dev/ttyUSB2 115200`
# - `AT+QGPSCFG="nmeasrc",1`
# - `AT+QGPS=1`
# this process is automated by my `eg25-control` program and services (`eg25-control-powered`, `eg25-control-gps`)
# - see the `modules/` directory further up this repository.
#
# now, something like `gpsd` can directly read from /dev/ttyUSB1,
# or geoclue can query the GPS directly through modem-manager
#
# initial GPS fix can take 15+ minutes.
# meanwhile, services like eg25-manager or eg25-control-freshen-agps can speed this up by uploading assisted GPS data to the modem.
#
# support/help:
# - geoclue, gnome-maps
# - irc: #gnome-maps on irc.gimp.org
# - Matrix: #gnome-maps:gnome.org (unclear if bridged to IRC)
#
# programs to pair this with:
# - `satellite-gtk`: <https://codeberg.org/tpikonen/satellite>
# - shows/tracks which satellites the GPS is connected to; useful to understand fix characteristics
# - `gnome-maps`: uses geoclue, has route planning
# - `mepo`: uses gpsd, minimalist, flaky, and buttons are kinda hard to activate on mobile
# - puremaps?
# - osmin?
#
# known/outstanding bugs:
# - `systemctl start eg25-control-gps` can the hang the whole system (2023/10/06)
# - i think it's actually `eg25-control-powered` which does this (started by the gps)
# - best guess is modem draws so much power at launch that other parts of the system see undervoltage
# - workaround is to hard power-cycle the system. the modem may not bring up after reboot: leave unpowered for 60s and boot again.
#
# future work:
# - integrate with [wigle](https://www.wigle.net/) for offline equivalent to Mozilla Location Services
{ config, lib, ... }:
{
# test gpsd with `gpspipe -w -n 10 2> /dev/null | grep -m 1 TPV | jq '.lat, .lon' | tr '\n' ' '`
# ^ should return <lat> <long>
services.gpsd.enable = true;
services.gpsd.devices = [ "/dev/ttyUSB1" ];
# test geoclue2 by building `geoclue2-with-demo-agent`
# and running "${geoclue2-with-demo-agent}/libexec/geoclue-2.0/demos/where-am-i"
# note that geoclue is dbus-activated, and auto-stops after 60s with no caller
services.geoclue2.enable = true;
services.geoclue2.appConfig.where-am-i = {
# this is the default "agent", shipped by geoclue package: allow it to use location
isAllowed = true;
isSystem = false;
# XXX: setting users != [] might be causing `where-am-i` to time out
users = [
# restrict to only one set of users. empty array (default) means "allow any user to access geolocation".
(builtins.toString config.users.users.colin.uid)
];
};
systemd.services.geoclue.after = lib.mkForce []; #< defaults to network-online, but not all my sources require network
users.users.geoclue.extraGroups = [
"dialout" # TODO: figure out if dialout is required. that's for /dev/ttyUSB1, but geoclue probably doesn't read that?
];
sane.services.eg25-control.enable = true;
sane.programs.where-am-i.enableFor.user.colin = true;
}

View File

@@ -0,0 +1,91 @@
{ pkgs, ... }:
let
dmesg = "${pkgs.util-linux}/bin/dmesg";
grep = "${pkgs.gnugrep}/bin/grep";
modprobe = "${pkgs.kmod}/bin/modprobe";
ensureHWReady = ''
# common boot failure:
# blank screen (no backlight even), with the following log:
# ```syslog
# sun8i-dw-hdmi 1ee0000.hdmi: Couldn't get the HDMI PHY
# ...
# sun4i-drm display-engine: Couldn't bind all pipelines components
# ...
# sun8i-dw-hdmi: probe of 1ee0000.hdmi failed with error -17
# ```
#
# in particular, that `probe ... failed` occurs *only* on failed boots
# (the other messages might sometimes occur even on successful runs?)
#
# reloading the sun8i hdmi driver usually gets the screen on, showing boot text.
# then restarting display-manager.service gets us to the login.
#
# NB: the above log is default level. though less specific, there's a `err` level message that also signals this:
# sun4i-drm display-engine: failed to bind 1ee0000.hdmi (ops sun8i_dw_hdmi_ops [sun8i_drm_hdmi]): -17
# NB: this is the most common, but not the only, failure mode for `display-manager`.
# another error seems characterized by these dmesg logs, in which reprobing sun8i_drm_hdmi does not fix:
# ```syslog
# sun6i-mipi-dsi 1ca0000.dsi: Couldn't get the MIPI D-PHY
# sun4i-drm display-engine: Couldn't bind all pipelines components
# sun6i-mipi-dsi 1ca0000.dsi: Couldn't register our component
# ```
if (${dmesg} --kernel --level err --color=never --notime | ${grep} -q 'sun4i-drm display-engine: failed to bind 1ee0000.hdmi')
then
echo "reprobing sun8i_drm_hdmi"
# if a command here fails it errors the whole service, so prefer to log instead
${modprobe} -r sun8i_drm_hdmi || echo "failed to unload sun8i_drm_hdmi"
${modprobe} sun8i_drm_hdmi || echo "failed to load sub8i_drm_hdmi"
fi
'';
in
{
boot.kernelPackages = pkgs.linuxPackagesFor pkgs.linux-megous;
# boot.kernelPackages = pkgs.linuxPackagesFor pkgs.linux-manjaro;
# boot.kernelPackages = pkgs.linuxPackagesFor pkgs.linux_latest;
# alternatively, apply patches directly to stock nixos kernel:
# boot.kernelPatches = manjaroPatches ++ [
# (patchDefconfig kernelConfig)
# ];
# configure nixos to build a compressed kernel image, since it doesn't usually do that for aarch64 target.
# without this i run out of /boot space in < 10 generations
nixpkgs.hostPlatform.linux-kernel = {
# defaults:
name = "aarch64-multiplatform";
baseConfig = "defconfig";
DTB = true;
autoModules = true;
preferBuiltin = true;
# extraConfig = ...
# ^-- raspberry pi stuff: we don't need it.
# target = "Image"; # <-- default
target = "Image.gz"; # <-- compress the kernel image
# target = "zImage"; # <-- confuses other parts of nixos :-(
};
# disable proximity sensor.
# the filtering/calibration is bad that it causes the screen to go fully dark at times.
boot.blacklistedKernelModules = [ "stk3310" ];
boot.kernelParams = [
# without this some GUI apps fail: `DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory`
# this is because they can't allocate enough video ram.
# see related nixpkgs issue: <https://github.com/NixOS/nixpkgs/issues/260222>
# TODO(2023/12/03): remove once mesa 23.3.1 lands: <https://github.com/NixOS/nixpkgs/pull/265740>
#
# the default CMA seems to be 32M.
# i was running fine with 256MB from 2022/07-ish through 2022/12-ish, but then the phone quit reliably coming back from sleep (phosh): maybe a memory leak?
# `cat /proc/meminfo` to see CmaTotal/CmaFree if interested in tuning this.
"cma=512M"
# 2023/10/20: potential fix for the lima (GPU) timeout bugs:
# - <https://gitlab.com/postmarketOS/pmaports/-/issues/805#note_890467824>
"lima.sched_timeout_ms=2000"
];
# services.xserver.displayManager.job.preStart = ensureHWReady;
# systemd.services.greetd.preStart = ensureHWReady;
systemd.services.unl0kr.preStart = ensureHWReady;
}

View File

@@ -0,0 +1,45 @@
# this file configures preferences per program, without actually enabling any programs.
# the goal is to separate the place where we decide *what* to use (i.e. `sane.programs.firefox.enable = true` -- at the toplevel)
# from where we specific how that thing should behave *if* it's in use.
#
# NixOS backgrounds:
# - <https://github.com/NixOS/nixos-artwork>
# - <https://github.com/NixOS/nixos-artwork/issues/50> (colorful; unmerged)
# - <https://github.com/NixOS/nixos-artwork/pull/60/files> (desktop-oriented; clean; unmerged)
# - <https://itsfoss.com/content/images/2023/04/nixos-tutorials.png>
{ lib, pkgs, sane-lib, ... }:
{
sane.programs.firefox.config = {
# compromise impermanence for the sake of usability
persistCache = "private";
persistData = "private";
# i don't do crypto stuff on moby
addons.ether-metamask.enable = false;
# sidebery UX doesn't make sense on small screen
addons.sidebery.enable = false;
};
sane.programs.swaynotificationcenter.config = {
backlight = "backlight"; # /sys/class/backlight/*backlight*/brightness
};
sane.programs.alacritty.config.fontSize = 9;
sane.programs.sway.config = {
font = "pango:monospace 10";
mod = "Mod1"; # prefer Alt
workspace_layout = "tabbed";
};
sane.programs.waybar.config = {
fontSize = 14;
height = 26;
persistWorkspaces = [ "1" "2" "3" "4" "5" ];
modules.media = false;
modules.network = false;
modules.perf = false;
modules.windowTitle = false;
# TODO: show modem state
};
}

View File

@@ -1,13 +1,18 @@
{ ... }: { pkgs, ... }:
{ {
imports = [ imports = [
./fs.nix ./fs.nix
]; ];
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
sane.persist.enable = false; # what we mean here is that the image is immutable; `/` is still tmpfs. sane.persist.enable = false; # what we mean here is that the image is immutable; `/` is still tmpfs.
sane.nixcache.enable = false; # don't want to be calling out to dead machines that we're *trying* to rescue sane.nixcache.enable = false; # don't want to be calling out to dead machines that we're *trying* to rescue
# auto-login at shell # auto-login at shell
services.getty.autologinUser = "colin"; services.getty.autologinUser = "colin";
# users.users.colin.initialPassword = "colin"; # users.users.colin.initialPassword = "colin";
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
system.stateVersion = "21.05";
} }

View File

@@ -1,40 +1,57 @@
{ ... }: { config, pkgs, ... }:
{ {
imports = [ imports = [
./fs.nix ./fs.nix
./net ./net.nix
./services ./services
./users
]; ];
# for administering services sane.programs = {
sane.programs.clightning-sane.enableFor.user.colin = true; # for administering services
# sane.programs.freshrss.enableFor.user.colin = true; freshrss.enableFor.user.colin = true;
# sane.programs.signaldctl.enableFor.user.colin = true; matrix-synapse.enableFor.user.colin = true;
# sane.programs.matrix-synapse.enableFor.user.colin = true; signaldctl.enableFor.user.colin = true;
};
sane.roles.build-machine.enable = true; sane.roles.build-machine.enable = true;
sane.programs.sane-deadlines.config.showOnLogin = false; # ~/knowledge doesn't always exist sane.zsh.showDeadlines = false; # ~/knowledge doesn't always exist
sane.programs.consoleUtils.suggestedPrograms = [ sane.programs.consoleUtils.suggestedPrograms = [
"consoleMediaUtils" # notably, for go2tv / casting "consoleMediaUtils" # notably, for go2tv / casting
"pcConsoleUtils" "pcConsoleUtils"
"sane-scripts.stop-all-servo" "sane-scripts.stop-all-servo"
]; ];
sane.services.dyn-dns.enable = true; sane.services.dyn-dns.enable = true;
sane.services.wg-home.enable = true;
sane.services.wg-home.visibleToWan = true;
sane.services.wg-home.forwardToWan = true;
sane.services.wg-home.routeThroughServo = false;
sane.services.wg-home.ip = config.sane.hosts.by-name."servo".wg-home.ip;
sane.nixcache.substituters.servo = false;
sane.nixcache.substituters.desko = false;
sane.nixcache.remote-builders.desko = false; sane.nixcache.remote-builders.desko = false;
sane.nixcache.remote-builders.servo = false; sane.nixcache.remote-builders.servo = false;
sane.services.rsync-net.enable = true; # sane.services.duplicity.enable = true; # TODO: re-enable after HW upgrade
# automatically log in at the virtual consoles. # automatically log in at the virtual consoles.
# using root here makes sure we always have an escape hatch. # using root here makes sure we always have an escape hatch
# XXX(2024-07-27): this is incompatible if using s6, which needs to auto-login as `colin` to start its user services.
services.getty.autologinUser = "root"; services.getty.autologinUser = "root";
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
# both transmission and ipfs try to set different net defaults. # both transmission and ipfs try to set different net defaults.
# we just use the most aggressive of the two here: # we just use the most aggressive of the two here:
boot.kernel.sysctl = { boot.kernel.sysctl = {
"net.core.rmem_max" = 4194304; # 4MB "net.core.rmem_max" = 4194304; # 4MB
}; };
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "21.11";
} }

View File

@@ -1,9 +1,60 @@
{ lib, pkgs, ... }: # zfs docs:
# - <https://nixos.wiki/wiki/ZFS>
# - <repo:nixos/nixpkgs:nixos/modules/tasks/filesystems/zfs.nix>
#
# zfs check health: `zpool status`
#
# zfs pool creation (requires `boot.supportedFilesystems = [ "zfs" ];`
# - 1. identify disk IDs: `ls -l /dev/disk/by-id`
# - 2. pool these disks: `zpool create -f -m legacy pool raidz ata-ST4000VN008-2DR166_WDH0VB45 ata-ST4000VN008-2DR166_WDH17616 ata-ST4000VN008-2DR166_WDH0VC8Q ata-ST4000VN008-2DR166_WDH17680`
# - legacy documented: <https://superuser.com/questions/790036/what-is-a-zfs-legacy-mount-point>
# - 3. enable acl support: `zfs set acltype=posixacl pool`
#
# import pools: `zpool import pool`
# show zfs datasets: `zfs list` (will be empty if haven't imported)
# show zfs properties (e.g. compression): `zfs get all pool`
# set zfs properties: `zfs set compression=on pool`
{ ... }:
{ {
# hostId: not used for anything except zfs guardrail? # hostId: not used for anything except zfs guardrail?
# [hex(ord(x)) for x in 'serv'] # [hex(ord(x)) for x in 'serv']
# networking.hostId = "73657276"; networking.hostId = "73657276";
boot.supportedFilesystems = [ "zfs" ];
# boot.zfs.enabled = true;
boot.zfs.forceImportRoot = false;
# scrub all zfs pools weekly:
services.zfs.autoScrub.enable = true;
boot.extraModprobeConfig = ''
### zfs_arc_max tunable:
# ZFS likes to use half the ram for its own cache and let the kernel push everything else to swap.
# so, reduce its cache size
# see: <https://askubuntu.com/a/1290387>
# see: <https://serverfault.com/a/1119083>
# see: <https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max>
# for all tunables, see: `man 4 zfs`
# to update these parameters without rebooting:
# - `echo '4294967296' | sane-sudo-redirect /sys/module/zfs/parameters/zfs_arc_max`
### zfs_bclone_enabled tunable
# this allows `cp --reflink=always FOO BAR` to work. i.e. shallow copies.
# it's unstable as of 2.2.3. led to *actual* corruption in 2.2.1, but hopefully better by now.
# - <https://github.com/openzfs/zfs/issues/405>
# note that `du -h` won't *always* show the reduced size for reflink'd files (?).
# `zpool get all | grep clone` seems to be the way to *actually* see how much data is being deduped
options zfs zfs_arc_max=4294967296 zfs_bclone_enabled=1
'';
# to be able to mount the pool like this, make sure to tell zfs to NOT manage it itself.
# otherwise local-fs.target will FAIL and you will be dropped into a rescue shell.
# - `zfs set mountpoint=legacy pool`
# if done correctly, the pool can be mounted before this `fileSystems` entry is created:
# - `sudo mount -t zfs pool /mnt/persist/pool`
fileSystems."/mnt/pool" = {
device = "pool";
fsType = "zfs";
options = [ "acl" ]; #< not sure if this `acl` flag is actually necessary. it mounts without it.
};
# services.zfs.zed = ... # TODO: zfs can send me emails when disks fail
sane.programs.sysadminUtils.suggestedPrograms = [ "zfs" ];
sane.persist.stores."ext" = { sane.persist.stores."ext" = {
origin = "/mnt/pool/persist"; origin = "/mnt/pool/persist";
@@ -16,7 +67,7 @@
fileSystems."/tmp".options = [ "size=32G" ]; fileSystems."/tmp".options = [ "size=32G" ];
fileSystems."/nix" = { fileSystems."/nix" = {
device = "/dev/disk/by-uuid/55555555-eeee-ffff-bbbb-000020250820"; device = "/dev/disk/by-uuid/cc81cca0-3cc7-4d82-a00c-6243af3e7776";
fsType = "btrfs"; fsType = "btrfs";
options = [ options = [
"compress=zstd" "compress=zstd"
@@ -25,39 +76,23 @@
}; };
fileSystems."/boot" = { fileSystems."/boot" = {
device = "/dev/disk/by-uuid/2025-0820"; device = "/dev/disk/by-uuid/6EE3-4171";
fsType = "vfat"; fsType = "vfat";
}; };
fileSystems."/mnt/pool" = { # slow, external storage (for archiving, etc)
# all btrfs devices of the same RAID volume use the same UUID. fileSystems."/mnt/usb-hdd" = {
device = "UUID=40fc6e1d-ba41-44de-bbf3-1aa02c3441df"; device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
fsType = "btrfs"; fsType = "btrfs";
options = [ options = [
# "compress=zstd" #< not much point in compressing... mostly videos and music; media. "compress=zstd"
"defaults" "defaults"
# `device=...` only needed if `btrfs scan` hasn't yet been run
# see: <https://askubuntu.com/a/484374>
# i don't know what guarantees NixOS/systemd make about that, so specifying all devices for now
# "device=/dev/disk/by-partuuid/14a7d00a-be53-2b4e-96f9-7e2c964674ec" #< removed 2024-11-24 (for capacity upgrade)
"device=/dev/disk/by-partuuid/409a147e-2282-49eb-87a7-c968032ede88" #< added 2024-11-24
# "device=/dev/disk/by-partuuid/6b86cc10-c3cc-ec4d-b20d-b6688f0959a6" #< removed 2025-06-04 (early drive failure; capacity upgrade)
# "device=/dev/disk/by-partuuid/7fd85cac-b6f3-8248-af4e-68e703d11020" #< removed 2024-11-13 (early drive failure)
"device=/dev/disk/by-partuuid/92ebbbfb-022f-427d-84d5-39349d4bc02a" #< added 2025-05-14
"device=/dev/disk/by-partuuid/9e6c06b0-4a39-4d69-813f-1f5992f62ed7" #< added 2025-06-05
"device=/dev/disk/by-partuuid/d9ad5ebc-0fc4-4d89-9fd0-619ce5210f1b" #< added 2024-11-13
# "device=/dev/disk/by-partuuid/ef0e5c7b-fccf-f444-bac4-534424326159" #< removed 2025-05-14 (early drive failure)
"nofail"
# "x-systemd.before=local-fs.target"
"x-systemd.device-bound=false" #< don't unmount when `device` disappears (i thought this was necessary, for drive replacement, but it might not be)
"x-systemd.device-timeout=60s"
"x-systemd.mount-timeout=60s"
]; ];
}; };
sane.fs."/mnt/usb-hdd".mount = {};
# TODO: move this elsewhere and automate the ACLs!
# FIRST TIME SETUP FOR MEDIA DIRECTORY: # FIRST TIME SETUP FOR MEDIA DIRECTORY:
# - set the group sticky bit: `sudo find /var/media -type d -exec chmod g+s {} +` # - set the group stick bit: `sudo find /var/media -type d -exec chmod g+s {} +`
# - this ensures new files/dirs inherit the group of their parent dir (instead of the user who creates them) # - this ensures new files/dirs inherit the group of their parent dir (instead of the user who creates them)
# - ensure everything under /var/media is mounted with `-o acl`, to support acls # - ensure everything under /var/media is mounted with `-o acl`, to support acls
# - ensure all files are rwx by group: `setfacl --recursive --modify d:g::rwx /var/media` # - ensure all files are rwx by group: `setfacl --recursive --modify d:g::rwx /var/media`
@@ -69,9 +104,8 @@
mode = "0775"; mode = "0775";
}]; }];
sane.fs."/var/media/archive".dir = {}; sane.fs."/var/media/archive".dir = {};
sane.fs."/var/media/archive/temp".dir = {};
# this is file.text instead of symlink.text so that it may be read over a remote mount (where consumers might not have any /nix/store/.../README.md path) # this is file.text instead of symlink.text so that it may be read over a remote mount (where consumers might not have any /nix/store/.../README.md path)
sane.fs."/var/media/archive/temp/README.md".file.text = '' sane.fs."/var/media/archive/README.md".file.text = ''
this directory is for media i wish to remove from my library, this directory is for media i wish to remove from my library,
but keep for a short time in case i reverse my decision. but keep for a short time in case i reverse my decision.
treat it like a system trash can. treat it like a system trash can.
@@ -81,6 +115,7 @@
sane.fs."/var/media/Books/Books".dir = {}; sane.fs."/var/media/Books/Books".dir = {};
sane.fs."/var/media/Books/Visual".dir = {}; sane.fs."/var/media/Books/Visual".dir = {};
sane.fs."/var/media/collections".dir = {}; sane.fs."/var/media/collections".dir = {};
# sane.fs."/var/media/datasets".dir = {};
sane.fs."/var/media/freeleech".dir = {}; sane.fs."/var/media/freeleech".dir = {};
sane.fs."/var/media/Music".dir = {}; sane.fs."/var/media/Music".dir = {};
sane.fs."/var/media/Pictures".dir = {}; sane.fs."/var/media/Pictures".dir = {};
@@ -89,18 +124,34 @@
sane.fs."/var/media/Videos/Shows".dir = {}; sane.fs."/var/media/Videos/Shows".dir = {};
sane.fs."/var/media/Videos/Talks".dir = {}; sane.fs."/var/media/Videos/Talks".dir = {};
systemd.services.dedupe-media = { # this is file.text instead of symlink.text so that it may be read over a remote mount (where consumers might not have any /nix/store/.../README.md path)
description = "transparently de-duplicate /var/media entries by using block-level hardlinks"; sane.fs."/var/lib/uninsane/datasets/README.md".file.text = ''
script = '' this directory may seem redundant with ../media/datasets. it isn't.
${lib.getExe' pkgs.util-linux "hardlink"} /var/media --reflink=always --ignore-time --verbose this directory exists on SSD, allowing for speedy access to specific datasets when necessary.
''; the contents should be a subset of what's in ../media/datasets.
}; '';
systemd.timers.dedupe-media = {
wantedBy = [ "multi-user.target" ]; # btrfs doesn't easily support swapfiles
timerConfig = { # swapDevices = [
OnStartupSec = "23min"; # { device = "/nix/persist/swapfile"; size = 4096; }
OnUnitActiveSec = "720min"; # ];
};
}; # this can be a partition. create with:
# fdisk <dev>
# n
# <default partno>
# <start>
# <end>
# t
# <partno>
# 19 # set part type to Linux swap
# w # write changes
# mkswap -L swap <part>
# swapDevices = [
# {
# label = "swap";
# # TODO: randomEncryption.enable = true;
# }
# ];
} }

184
hosts/by-name/servo/net.nix Normal file
View File

@@ -0,0 +1,184 @@
{ config, lib, pkgs, ... }:
let
portOpts = with lib; types.submodule {
options = {
visibleTo.ovpn = mkOption {
type = types.bool;
default = false;
};
};
};
in
{
options = with lib; {
sane.ports.ports = mkOption {
# add the `visibleTo.ovpn` option
type = types.attrsOf portOpts;
};
};
config = {
networking.domain = "uninsane.org";
sane.ports.openFirewall = true;
sane.ports.openUpnp = true;
# unless we add interface-specific settings for each VPN, we have to define nameservers globally.
# networking.nameservers = [
# "1.1.1.1"
# "9.9.9.9"
# ];
# services.resolved.extraConfig = ''
# # docs: `man resolved.conf`
# # DNS servers to use via the `wg-ovpns` interface.
# # i hope that from the root ns, these aren't visible.
# DNS=46.227.67.134%wg-ovpns 192.165.9.158%wg-ovpns
# FallbackDNS=1.1.1.1 9.9.9.9
# '';
# OVPN CONFIG (https://www.ovpn.com):
# DOCS: https://nixos.wiki/wiki/WireGuard
# if you `systemctl restart wireguard-wg-ovpns`, make sure to also restart any other services in `NetworkNamespacePath = .../ovpns`.
# TODO: why not create the namespace as a seperate operation (nix config for that?)
networking.wireguard.enable = true;
networking.wireguard.interfaces.wg-ovpns = let
ip = "${pkgs.iproute2}/bin/ip";
in-ns = "${ip} netns exec ovpns";
iptables = "${pkgs.iptables}/bin/iptables";
veth-host-ip = "10.0.1.5";
veth-local-ip = "10.0.1.6";
vpn-ip = "185.157.162.178";
# DNS = 46.227.67.134, 192.165.9.158, 2a07:a880:4601:10f0:cd45::1, 2001:67c:750:1:cafe:cd45::1
vpn-dns = "46.227.67.134";
bridgePort = port: proto: ''
${in-ns} ${iptables} -A PREROUTING -t nat -p ${proto} --dport ${port} -m iprange --dst-range ${vpn-ip} \
-j DNAT --to-destination ${veth-host-ip}
'';
bridgeStatements = lib.foldlAttrs
(acc: port: portCfg: acc ++ (builtins.map (bridgePort port) portCfg.protocol))
[]
config.sane.ports.ports;
in {
privateKeyFile = config.sops.secrets.wg_ovpns_privkey.path;
# wg is active only in this namespace.
# run e.g. ip netns exec ovpns <some command like ping/curl/etc, it'll go through wg>
# sudo ip netns exec ovpns ping www.google.com
interfaceNamespace = "ovpns";
ips = [
"185.157.162.178/32"
];
peers = [
{
publicKey = "SkkEZDCBde22KTs/Hc7FWvDBfdOCQA4YtBEuC3n5KGs=";
endpoint = "185.157.162.10:9930";
# alternatively: use hostname, but that presents bootstrapping issues (e.g. if host net flakes)
# endpoint = "vpn36.prd.amsterdam.ovpn.com:9930";
allowedIPs = [ "0.0.0.0/0" ];
# nixOS says this is important for keeping NATs active
persistentKeepalive = 25;
# re-executes wg this often. docs hint that this might help wg notice DNS/hostname changes.
# so, maybe that helps if we specify endpoint as a domain name
# dynamicEndpointRefreshSeconds = 30;
# when refresh fails, try it again after this period instead.
# TODO: not avail until nixpkgs upgrade
# dynamicEndpointRefreshRestartSeconds = 5;
}
];
preSetup = ''
${ip} netns add ovpns || (test -e /run/netns/ovpns && echo "ovpns already exists")
'';
postShutdown = ''
${in-ns} ip link del ovpns-veth-b || echo "couldn't delete ovpns-veth-b"
${ip} link del ovpns-veth-a || echo "couldn't delete ovpns-veth-a"
${ip} netns delete ovpns || echo "couldn't delete ovpns"
# restore rules/routes
${ip} rule del from ${veth-host-ip} lookup ovpns pref 50 || echo "couldn't delete init -> ovpns rule"
${ip} route del default via ${veth-local-ip} dev ovpns-veth-a proto kernel src ${veth-host-ip} metric 1002 table ovpns || echo "couldn't delete init -> ovpns route"
${ip} rule add from all lookup local pref 0
${ip} rule del from all lookup local pref 100
'';
postSetup = ''
# DOCS:
# - some of this approach is described here: <https://josephmuia.ca/2018-05-16-net-namespaces-veth-nat/>
# - iptables primer: <https://danielmiessler.com/study/iptables/>
# create veth pair
${ip} link add ovpns-veth-a type veth peer name ovpns-veth-b
${ip} addr add ${veth-host-ip}/24 dev ovpns-veth-a
${ip} link set ovpns-veth-a up
# mv veth-b into the ovpns namespace
${ip} link set ovpns-veth-b netns ovpns
${in-ns} ip addr add ${veth-local-ip}/24 dev ovpns-veth-b
${in-ns} ip link set ovpns-veth-b up
# make it so traffic originating from the host side of the veth
# is sent over the veth no matter its destination.
${ip} rule add from ${veth-host-ip} lookup ovpns pref 50
# for traffic originating at the host veth to the WAN, use the veth as our gateway
# not sure if the metric 1002 matters.
${ip} route add default via ${veth-local-ip} dev ovpns-veth-a proto kernel src ${veth-host-ip} metric 1002 table ovpns
# give the default route lower priority
${ip} rule add from all lookup local pref 100
${ip} rule del from all lookup local pref 0
# in order to access DNS in this netns, we need to route it to the VPN's nameservers
# - alternatively, we could fix DNS servers like 1.1.1.1.
${in-ns} ${iptables} -A OUTPUT -t nat -p udp --dport 53 -m iprange --dst-range 127.0.0.53 \
-j DNAT --to-destination ${vpn-dns}:53
'' + (lib.concatStringsSep "\n" bridgeStatements);
};
# create a new routing table that we can use to proxy traffic out of the root namespace
# through the ovpns namespace, and to the WAN via VPN.
networking.iproute2.rttablesExtraConfig = ''
5 ovpns
'';
networking.iproute2.enable = true;
# HURRICANE ELECTRIC CONFIG:
# networking.sits = {
# hurricane = {
# remote = "216.218.226.238";
# local = "192.168.0.5";
# # local = "10.0.0.5";
# # remote = "10.0.0.1";
# # local = "10.0.0.22";
# dev = "eth0";
# ttl = 255;
# };
# };
# networking.interfaces."hurricane".ipv6 = {
# addresses = [
# # mx.uninsane.org (publically routed /64)
# {
# address = "2001:470:b:465::1";
# prefixLength = 128;
# }
# # client addr
# # {
# # address = "2001:470:a:466::2";
# # prefixLength = 64;
# # }
# ];
# routes = [
# {
# address = "::";
# prefixLength = 0;
# # via = "2001:470:a:466::1";
# }
# ];
# };
# # after configuration, we want the hurricane device to look like this:
# # hurricane: flags=209<UP,POINTOPOINT,RUNNING,NOARP> mtu 1480
# # inet6 2001:470:a:450::2 prefixlen 64 scopeid 0x0<global>
# # inet6 fe80::c0a8:16 prefixlen 64 scopeid 0x20<link>
# # sit txqueuelen 1000 (IPv6-in-IPv4)
# # test with:
# # curl --interface hurricane http://[2607:f8b0:400a:80b::2004]
# # ping 2607:f8b0:400a:80b::2004
};
}

View File

@@ -1,60 +0,0 @@
# debugging:
# - enable logs (shows handshake attempts)
# - `echo module wireguard +p | sane-sudo-redirect /sys/kernel/debug/dynamic_debug/control`
# - `sudo dmesg --follow`
# patterns: "Sending keepalive packet to peer NN (N.N.N.N:NNNNN)"
# patterns: "Sending handshake initiation to peer NN (N.N.N.N:NNNNN)"
# - when wg-doof and wg-ovpns stop routing traffic, restart with:
# - `systemctl restart netns-doof-wg`
# - handshaking:
# - `wg show` should *always* show "latest handshake: N", with N < 2 minutes ago.
{ lib, ... }:
let
portOpts = with lib; types.submodule {
options = {
visibleTo.ovpns = mkOption {
type = types.bool;
default = false;
description = ''
whether to forward inbound traffic on the OVPN vpn port to the corresponding localhost port.
'';
};
visibleTo.doof = mkOption {
type = types.bool;
default = false;
description = ''
whether to forward inbound traffic on the doofnet vpn port to the corresponding localhost port.
'';
};
};
};
in
{
options = with lib; {
sane.ports.ports = mkOption {
# add the `visibleTo.{doof,ovpns}` options
type = types.attrsOf portOpts;
};
};
imports = [
./doof.nix
./ovpn.nix
./wg-home.nix
];
config = {
networking.domain = "uninsane.org";
systemd.network.networks."50-eth0" = {
matchConfig.Name = "eth0";
networkConfig.Address = [
"205.201.63.12/32"
"10.78.79.51/22"
];
networkConfig.DNS = [ "10.78.79.1" ];
};
sane.ports.openFirewall = true;
sane.ports.openUpnp = true;
};
}

View File

@@ -1,27 +0,0 @@
{ config, ... }:
{
# tun-sea config
sane.dns.zones."uninsane.org".inet.A."doof.tunnel" = "205.201.63.12";
# sane.dns.zones."uninsane.org".inet.AAAA."doof.tunnel" = "2602:fce8:106::51"; #< TODO: enable IPv6 (i have /128)
# if the tunnel breaks, restart it manually:
# - `systemctl restart netns-doof.service`
sane.netns.doof = {
veth.initns.ipv4 = "10.0.2.5";
veth.netns.ipv4 = "10.0.2.6";
routeTable = 12;
# wg.port = 51821;
wg.privateKeyFile = config.sops.secrets.wg_doof_privkey.path;
wg.address.ipv4 = "205.201.63.12";
wg.peer.publicKey = "nuESyYEJ3YU0hTZZgAd7iHBz1ytWBVM5PjEL1VEoTkU=";
wg.peer.endpoint = "tun-sea.doof.net:53263";
# wg.peer.endpoint = "205.201.63.44:53263";
};
# inside doof, forward DNS requests back to the root machine
# this is fine: nothing inside the ns performs DNS except for wireguard,
# and we're not forwarding external DNS requests here
# XXX: ACTUALLY, CAN'T EASILY DO THAT BECAUSE HICKORY-DNS IS ALREADY USING PORT 53
# but that's ok, we don't really need DNS *inside* this namespace.
# sane.netns.doof.dns.ipv4 = config.sane.netns.doof.veth.netns.ipv4;
}

View File

@@ -1,20 +0,0 @@
{ config, ... }:
{
sane.ovpn.addrV4 = "172.23.174.114"; #< this applies to the dynamic VPNs -- NOT the static VPN
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:8df3:14b0";
# OVPN CONFIG (https://www.ovpn.com):
# DOCS: https://nixos.wiki/wiki/WireGuard
sane.netns.ovpns = {
veth.initns.ipv4 = "10.0.1.5";
veth.netns.ipv4 = "10.0.1.6";
routeTable = 11;
dns.ipv4 = "46.227.67.134"; #< DNS requests inside the namespace are forwarded here
# wg.port = 51822;
wg.privateKeyFile = config.sops.secrets.wg_ovpns_privkey.path;
wg.address.ipv4 = "146.70.100.165"; #< IP address for my end of the VPN tunnel. for OVPN public IPv4, this is also the public IP address.
wg.peer.publicKey = "xc9p/lf2uLg6IGDh54E0Pbc6WI/J9caaByhwD4Uiu0Q="; #< pubkey by which i can authenticate OVPN, varies per OVPN endpoint
wg.peer.endpoint = "vpn31.prd.losangeles.ovpn.com:9930";
# wg.peer.endpoint = "45.83.89.131:9930";
};
}

View File

@@ -1,14 +0,0 @@
{ config, ... }:
{
sane.services.wg-home.enable = true;
sane.services.wg-home.visibleToWan = true;
sane.services.wg-home.forwardToWan = true;
sane.services.wg-home.routeThroughServo = false;
services.unbound.settings.server.interface = [
# provide DNS to my wireguard clients
config.sane.hosts.by-name."servo".wg-home.ip
];
services.unbound.settings.server.access-control = [
"${config.sane.hosts.by-name."servo".wg-home.ip}/24 allow"
];
}

View File

@@ -1,70 +0,0 @@
# bitmagnet is a DHT crawler. it discovers publicly reachable torrents and indexes:
# - torrent's magnet URI
# - torrent's name
# - torrent's file list (the first 100 files, per torrent), including size and "type" (e.g. video)
# - seeder/leecher counts
# - torrent's size
# it provides a web UI to query these, especially a search form.
# data is stored in postgresql as `bitmagnet` db (`sudo -u bitmagnet psql`)
# after 30 days of operation:
# - 12m torrents discovered
# - 77GB database size => 6500B per torrent
{ config, ... }:
{
services.bitmagnet.enable = true;
sane.netns.ovpns.services = [ "bitmagnet" ];
sane.ports.ports."3334" = {
protocol = [ "tcp" "udp" ];
# visibleTo.ovpns = true; #< not needed: it runs in the ovpns namespace
description = "colin-bitmagnet";
};
services.bitmagnet.settings = {
# dht_crawler.scaling_factor: how rapidly to crawl the DHT.
# influences number of worker threads, buffer sizes, etc.
# default: 10.
# docs claim "diminishing returns" above 10, but seems weakly confident about that.
dht_crawler.scaling_factor = 64;
# http_server.local_address: `$addr:$port` to `listen` to.
# default is `:3333`, which listens on _all_ interfaces.
# the http server exposes unprotected admin endpoints though, so restrict to private interfaces:
http_server.local_address = "${config.sane.netns.ovpns.veth.netns.ipv4}:3333";
# tmdb.enabled: whether to query The Movie DataBase to resolve filename -> movie title.
# default: true.
# docs claim 1 query per second rate limit, unless you supply your own API key.
tmdb.enabled = false;
};
# bitmagnet web client
# protected by passwd because it exposes some mutation operations:
# - queuing "jobs"
# - deleting torrent infos (in bulk)
# it uses graphql for _everything_, so no easy way to disable just the mutations (and remove the password) AFAICT.
services.nginx.virtualHosts."bitmagnet.uninsane.org" = {
# basicAuth is cleartext user/pw, so FORCE this to happen over SSL
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://${config.sane.netns.ovpns.veth.netns.ipv4}:3333";
recommendedProxySettings = true;
};
basicAuthFile = config.sops.secrets.bitmagnet_passwd.path;
};
sops.secrets."bitmagnet_passwd" = {
owner = config.users.users.nginx.name;
mode = "0400";
};
sane.dns.zones."uninsane.org".inet.CNAME."bitmagnet" = "native";
systemd.services.bitmagnet = {
# hardening (systemd-analyze security bitmagnet). base nixos service is already partially hardened.
serviceConfig.CapabilityBoundingSet = "";
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.PrivateDevices = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProcSubset = "pid";
serviceConfig.SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
};
}

View File

@@ -0,0 +1,34 @@
{ config, lib, ... }:
let
cweb-cfg = config.services.calibre-web;
inherit (cweb-cfg) user group;
inherit (cweb-cfg.listen) ip port;
svc-dir = "/var/lib/${cweb-cfg.dataDir}";
in
# XXX: disabled because of runtime errors like:
# > File "/nix/store/c7jqvx980nlg9xhxi065cba61r2ain9y-calibre-web-0.6.19/lib/python3.10/site-packages/calibreweb/cps/db.py", line 926, in speaking_language
# > languages = self.session.query(Languages) \
# > AttributeError: 'NoneType' object has no attribute 'query'
lib.mkIf false
{
sane.persist.sys.byStore.plaintext = [
{ inherit user group; mode = "0700"; path = svc-dir; method = "bind"; }
];
services.calibre-web.enable = true;
services.calibre-web.listen.ip = "127.0.0.1";
# XXX: externally populate `${svc-dir}/metadata.db` (once) from
# <https://github.com/janeczku/calibre-web/blob/master/library/metadata.db>
# i don't know why you have to do this??
# services.calibre-web.options.calibreLibrary = svc-dir;
services.nginx.virtualHosts."calibre.uninsane.org" = {
addSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://${ip}:${builtins.toString port}";
};
};
sane.dns.zones."uninsane.org".inet.CNAME."calibre" = "native";
}

View File

@@ -24,65 +24,50 @@
# that is NOT the case when the STUN server and client A are on the same LAN # that is NOT the case when the STUN server and client A are on the same LAN
# even if client A contacts the STUN server via its WAN address with port reflection enabled. # even if client A contacts the STUN server via its WAN address with port reflection enabled.
# hence, there's no obvious way to put the STUN server on the same LAN as either client and expect the rest to work. # hence, there's no obvious way to put the STUN server on the same LAN as either client and expect the rest to work.
# - there an old version which *half worked*, which is: { lib, ... }:
# - run the turn server in the root namespace.
# - bind the turn server to the veth connecting it to the VPN namespace (so it sends outgoing traffic to the right place).
# - NAT the turn port range from VPN into root namespace (so it receives incomming traffic).
# - this approach would fail the prosody conversations.im check, but i didn't notice *obvious* call routing errors.
#
# debugging:
# - log messages like 'usage: realm=<turn.uninsane.org>, username=<1715915193>, rp=14, rb=1516, sp=8, sb=684'
# - rp = received packets
# - rb = received bytes
# - sp = sent packets
# - sb = sent bytes
{ config, lib, ... }:
let let
# TURN port range (inclusive). # TODO: this range could be larger, but right now that's costly because each element is its own UPnP forward
# default coturn behavior is to use the upper quarter of all ports. i.e. 49152 - 65535. # TURN port range (inclusive)
# i believe TURN allocations expire after either 5 or 10 minutes of inactivity. turnPortLow = 49152;
turnPortLow = 49152; # 49152 = 0xc000 turnPortHigh = 49167;
turnPortHigh = turnPortLow + 256;
turnPortRange = lib.range turnPortLow turnPortHigh; turnPortRange = lib.range turnPortLow turnPortHigh;
in in
{ {
# the port definitions are only needed if running in the root net namespace sane.ports.ports = lib.mkMerge ([
# sane.ports.ports = lib.mkMerge ([ {
# { "3478" = {
# "3478" = { # this is the "control" port.
# # this is the "control" port. # i.e. no client data is forwarded through it, but it's where clients request tunnels.
# # i.e. no client data is forwarded through it, but it's where clients request tunnels. protocol = [ "tcp" "udp" ];
# protocol = [ "tcp" "udp" ]; # visibleTo.lan = true;
# # visibleTo.lan = true; # visibleTo.wan = true;
# # visibleTo.wan = true; visibleTo.ovpn = true;
# visibleTo.ovpns = true; # forward traffic from the VPN to the root NS description = "colin-stun-turn";
# description = "colin-stun-turn"; };
# }; "5349" = {
# "5349" = { # the other port 3478 also supports TLS/DTLS, but presumably clients wanting TLS will default 5349
# # the other port 3478 also supports TLS/DTLS, but presumably clients wanting TLS will default 5349 protocol = [ "tcp" ];
# protocol = [ "tcp" ]; # visibleTo.lan = true;
# # visibleTo.lan = true; # visibleTo.wan = true;
# # visibleTo.wan = true; visibleTo.ovpn = true;
# visibleTo.ovpns = true; description = "colin-stun-turn-over-tls";
# description = "colin-stun-turn-over-tls"; };
# }; }
# } ] ++ (builtins.map
# ] ++ (builtins.map (port: {
# (port: { "${builtins.toString port}" = let
# "${builtins.toString port}" = let count = port - turnPortLow + 1;
# count = port - turnPortLow + 1; numPorts = turnPortHigh - turnPortLow + 1;
# numPorts = turnPortHigh - turnPortLow + 1; in {
# in { protocol = [ "tcp" "udp" ];
# protocol = [ "tcp" "udp" ]; # visibleTo.lan = true;
# # visibleTo.lan = true; # visibleTo.wan = true;
# # visibleTo.wan = true; visibleTo.ovpn = true;
# visibleTo.ovpns = true; description = "colin-turn-${builtins.toString count}-of-${builtins.toString numPorts}";
# description = "colin-turn-${builtins.toString count}-of-${builtins.toString numPorts}"; };
# }; })
# }) turnPortRange
# turnPortRange ));
# ));
services.nginx.virtualHosts."turn.uninsane.org" = { services.nginx.virtualHosts."turn.uninsane.org" = {
# allow ACME to procure a cert via nginx for this domain # allow ACME to procure a cert via nginx for this domain
@@ -104,6 +89,13 @@ in
SRV."_turns._tcp" = "5 50 5349 turn"; SRV."_turns._tcp" = "5 50 5349 turn";
}; };
sane.derived-secrets."/var/lib/coturn/shared_secret.bin" = {
encoding = "base64";
# TODO: make this not globally readable
acl.mode = "0644";
};
sane.fs."/var/lib/coturn/shared_secret.bin".wantedBeforeBy = [ "coturn.service" ];
# provide access to certs # provide access to certs
users.users.turnserver.extraGroups = [ "nginx" ]; users.users.turnserver.extraGroups = [ "nginx" ];
@@ -111,33 +103,22 @@ in
services.coturn.realm = "turn.uninsane.org"; services.coturn.realm = "turn.uninsane.org";
services.coturn.cert = "/var/lib/acme/turn.uninsane.org/fullchain.pem"; services.coturn.cert = "/var/lib/acme/turn.uninsane.org/fullchain.pem";
services.coturn.pkey = "/var/lib/acme/turn.uninsane.org/key.pem"; services.coturn.pkey = "/var/lib/acme/turn.uninsane.org/key.pem";
# N.B.: prosody needs to read this shared secret
sops.secrets."coturn_shared_secret".owner = "turnserver";
sops.secrets."coturn_shared_secret".group = "turnserver";
sops.secrets."coturn_shared_secret".mode = "0440";
#v disable to allow unauthenticated access (or set `services.coturn.no-auth = true`)
services.coturn.use-auth-secret = true; services.coturn.use-auth-secret = true;
services.coturn.static-auth-secret-file = "/run/secrets/coturn_shared_secret"; services.coturn.static-auth-secret-file = "/var/lib/coturn/shared_secret.bin";
services.coturn.lt-cred-mech = true; #< XXX: use-auth-secret overrides lt-cred-mech services.coturn.lt-cred-mech = true;
services.coturn.min-port = turnPortLow; services.coturn.min-port = turnPortLow;
services.coturn.max-port = turnPortHigh; services.coturn.max-port = turnPortHigh;
# services.coturn.secure-stun = true; # services.coturn.secure-stun = true;
services.coturn.extraConfig = lib.concatStringsSep "\n" [ services.coturn.extraConfig = lib.concatStringsSep "\n" [
"verbose" "verbose"
# "Verbose" #< even MORE verbosity than "verbose" (it's TOO MUCH verbosity really) # "Verbose" #< even MORE verbosity than "verbose"
"no-multicast-peers" # disables sending to IPv4 broadcast addresses (e.g. 224.0.0.0/3) # "no-multicast-peers" # disables sending to IPv4 broadcast addresses (e.g. 224.0.0.0/3)
# "listening-ip=${config.sane.netns.ovpns.veth.initns.ipv4}" "external-ip=${config.sane.netns.ovpns.wg.address.ipv4}" #< 2024/04/25: works, if running in root namespace "listening-ip=10.0.1.5"
"listening-ip=${config.sane.netns.ovpns.wg.address.ipv4}" "external-ip=${config.sane.netns.ovpns.wg.address.ipv4}" # "external-ip=185.157.162.178/10.0.1.5"
"external-ip=185.157.162.178"
# old attempts:
# "external-ip=${config.sane.netns.ovpns.wg.address.ipv4}/${config.sane.netns.ovpns.veth.initns.ipv4}"
# "listening-ip=10.78.79.51" # can be specified multiple times; omit for * # "listening-ip=10.78.79.51" # can be specified multiple times; omit for *
# "external-ip=97.113.128.229/10.78.79.51" # "external-ip=97.113.128.229/10.78.79.51"
# "external-ip=97.113.128.229" # "external-ip=97.113.128.229"
# "mobility" # "mobility with ICE (MICE) specs support" (?) # "mobility" # "mobility with ICE (MICE) specs support" (?)
]; ];
systemd.services.coturn.serviceConfig.NetworkNamespacePath = "/run/netns/ovpns";
} }

View File

@@ -1,5 +1,4 @@
# as of 2023/12/02: complete blockchain is 530 GiB (on-disk size may be larger) # as of 2023/12/02: complete blockchain is 530 GiB (on-disk size may be larger)
# as of 2025/08/06: on-disk blockchain as reported by `du` is 732 GiB
# #
# ports: # ports:
# - 8333: for node-to-node communications # - 8333: for node-to-node communications
@@ -17,17 +16,14 @@
# - validate with `bitcoin-cli -netinfo` # - validate with `bitcoin-cli -netinfo`
{ config, lib, pkgs, sane-lib, ... }: { config, lib, pkgs, sane-lib, ... }:
let let
# bitcoind = config.sane.programs.bitcoind.packageUnwrapped;
bitcoind = pkgs.bitcoind;
# wrapper to run bitcoind with the tor onion address as externalip (computed at runtime) # wrapper to run bitcoind with the tor onion address as externalip (computed at runtime)
_bitcoindWithExternalIp = pkgs.writeShellScriptBin "bitcoind" '' _bitcoindWithExternalIp = with pkgs; writeShellScriptBin "bitcoind" ''
set -xeu
externalip="$(cat /var/lib/tor/onion/bitcoind/hostname)" externalip="$(cat /var/lib/tor/onion/bitcoind/hostname)"
exec ${lib.getExe' bitcoind "bitcoind"} "-externalip=$externalip" "$@" exec ${bitcoind}/bin/bitcoind "-externalip=$externalip" "$@"
''; '';
# the package i provide to services.bitcoind ends up on system PATH, and used by other tools like clightning. # the package i provide to services.bitcoind ends up on system PATH, and used by other tools like clightning.
# therefore, even though services.bitcoind only needs `bitcoind` binary, provide all the other bitcoin-related binaries (notably `bitcoin-cli`) as well: # therefore, even though services.bitcoind only needs `bitcoind` binary, provide all the other bitcoin-related binaries (notably `bitcoin-cli`) as well:
bitcoindWithExternalIp = pkgs.symlinkJoin { bitcoindWithExternalIp = with pkgs; symlinkJoin {
name = "bitcoind-with-external-ip"; name = "bitcoind-with-external-ip";
paths = [ _bitcoindWithExternalIp bitcoind ]; paths = [ _bitcoindWithExternalIp bitcoind ];
}; };
@@ -65,67 +61,23 @@ in
passwordHMAC = "30002c05d82daa210550e17a182db3f3$6071444151281e1aa8a2729f75e3e2d224e9d7cac3974810dab60e7c28ffaae4"; passwordHMAC = "30002c05d82daa210550e17a182db3f3$6071444151281e1aa8a2729f75e3e2d224e9d7cac3974810dab60e7c28ffaae4";
}; };
extraConfig = '' extraConfig = ''
# checkblocks: default 6: how many blocks to verify on start
checkblocks=3
# don't load the wallet, and disable wallet RPC calls # don't load the wallet, and disable wallet RPC calls
disablewallet=1 disablewallet=1
# proxy all outbound traffic through Tor # proxy all outbound traffic through Tor
proxy=127.0.0.1:9050 proxy=127.0.0.1:9050
''; '';
extraCmdlineOptions = [
# `man bitcoind` for options
# "-assumevalid=0" # to perform script validation on all blocks, instead of just the latest checkpoint published by bitcoin-core
# "-debug"
# "-debug=estimatefee"
# "-debug=leveldb"
# "-debug=http"
# "-debug=net"
"-debug=proxy"
"-debug=rpc"
# "-debug=validation"
# "-reindex" # wipe chainstate, block index, other indices; rebuild from blk*.dat (takes 2.5hrs)
# "-reindex-chainstate" # wipe chainstate; rebuild from blk*.dat
];
}; };
users.users.bitcoind-mainnet.extraGroups = [ "tor" ]; users.users.bitcoind-mainnet.extraGroups = [ "tor" ];
systemd.services.bitcoind-mainnet = { systemd.services.bitcoind-mainnet.serviceConfig.RestartSec = "30s"; #< default is 0
after = [ "tor.service" ];
requires = [ "tor.service" ];
serviceConfig.RestartSec = "30s"; #< default is 0
# hardening (systemd-analyze security bitcoind-mainnet)
serviceConfig.StateDirectory = "bitcoind-mainnet";
serviceConfig.LockPersonality = true;
serviceConfig.MemoryDenyWriteExecute = "true";
serviceConfig.NoNewPrivileges = "true";
serviceConfig.PrivateDevices = "true";
serviceConfig.PrivateMounts = true;
serviceConfig.PrivateTmp = "true";
serviceConfig.PrivateUsers = true;
serviceConfig.ProcSubset = "pid";
serviceConfig.ProtectControlGroups = true;
serviceConfig.ProtectHome = true;
serviceConfig.ProtectHostname = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectKernelModules = true;
serviceConfig.ProtectKernelTunables = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProtectSystem = lib.mkForce "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
serviceConfig.RestrictNamespaces = true;
serviceConfig.RestrictSUIDSGID = true;
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.SystemCallFilter = [ "@system-service" ];
};
sane.users.colin.fs.".bitcoin/bitcoin.conf" = sane-lib.fs.wantedSymlinkTo config.sops.secrets."bitcoin.conf".path;
sops.secrets."bitcoin.conf" = { sops.secrets."bitcoin.conf" = {
mode = "0600"; mode = "0600";
owner = "colin"; owner = "colin";
group = "users"; group = "users";
}; };
sane.programs.bitcoin-cli.enableFor.user.colin = true; # for debugging/administration: `bitcoin-cli` sane.programs.bitcoind.enableFor.user.colin = true; # for debugging/administration: `bitcoin-cli`
} }

View File

@@ -1,21 +1,5 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#!nix-shell -i python3 -p pyln-client -p python3 #!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.pyln-client ])"
"""
clightning-sane: helper to perform common Lightning node admin operations:
- view channel balances
- rebalance channels
COMMON OPERATIONS:
- view channel balances: `clightning-sane status`
- rebalance channels to improve routability (without paying any fees): `clightning-sane autobalance`
FULL OPERATION:
- `clightning-sane status --full`
- `P$`: represents how many msats i've captured in fees from this channel.
- `COST`: rough measure of how much it's "costing" me to let my channel partner hold funds on his side of the channel.
this is based on the notion that i only capture fees from outbound transactions, and so the channel partner holding all liquidity means i can't capture fees on that liquidity.
"""
# pyln-client docs: <https://github.com/ElementsProject/lightning/tree/master/contrib/pyln-client> # pyln-client docs: <https://github.com/ElementsProject/lightning/tree/master/contrib/pyln-client>
# terminology: # terminology:
@@ -130,17 +114,6 @@ class TxBounds:
return TxBounds(min_msat=self.min_msat, max_msat=new_max) return TxBounds(min_msat=self.min_msat, max_msat=new_max)
class Node:
def __init__(self, fields: dict):
self.fields = fields
@property
def alias(self) -> str:
alias = self.fields.get("alias", None)
if alias is None:
logger.debug(f"node lacks alias field: {self.fields}")
alias = self.fields.get("nodeid", None)
return alias if alias is not None else "Unknown"
class LocalChannel: class LocalChannel:
def __init__(self, channels: list, rpc: "RpcHelper"): def __init__(self, channels: list, rpc: "RpcHelper"):
@@ -212,7 +185,7 @@ class LocalChannel:
@property @property
def remote_alias(self) -> str: def remote_alias(self) -> str:
return self.remote_node.alias return self.remote_node["alias"]
@property @property
def scid(self) -> str: def scid(self) -> str:
@@ -323,10 +296,10 @@ class RpcHelper:
assert listchan and listchan.get("channels", []) != [], f"bad listchannels for {scid}: {listchan}" assert listchan and listchan.get("channels", []) != [], f"bad listchannels for {scid}: {listchan}"
return LocalChannel(listchan["channels"], self) return LocalChannel(listchan["channels"], self)
def node(self, id: str) -> Node: def node(self, id: str) -> dict:
nodes = self.rpc.listnodes(id)["nodes"] nodes = self.rpc.listnodes(id)["nodes"]
assert len(nodes) == 1, f"unexpected: multiple nodes for {id}: {nodes}" assert len(nodes) == 1, f"unexpected: multiple nodes for {id}: {nodes}"
return Node(nodes[0]) return nodes[0]
def peerchannel(self, scid: str, peer_id: str) -> dict: def peerchannel(self, scid: str, peer_id: str) -> dict:
peerchannels = self.rpc.listpeerchannels(peer_id)["channels"] peerchannels = self.rpc.listpeerchannels(peer_id)["channels"]
@@ -506,14 +479,6 @@ class LoopJobDone(Enum):
ABORTED = "ABORTED" ABORTED = "ABORTED"
class AbstractLoopRunner: class AbstractLoopRunner:
"""
base class.
implementors implement `pop_job` and `finished_job`.
this super class coordinates how to call those in a loop:
- spawn N threads, each one calls `pop_job` then `finished_job`, in a loop
- coordinates completion:
- `run_to_completion` waits until a sufficient number (usually just one) of `LoopJobDone` jobs have been received before returning.
"""
def __init__(self, looper: LoopRouter, bounds: TxBounds, parallelism: int): def __init__(self, looper: LoopRouter, bounds: TxBounds, parallelism: int):
self.looper = looper self.looper = looper
self.bounds = bounds self.bounds = bounds
@@ -605,12 +570,6 @@ class LoopPairState:
self.failed_tx_throttler = 0 # increase by one every time we fail, decreases more gradually, when we succeed self.failed_tx_throttler = 0 # increase by one every time we fail, decreases more gradually, when we succeed
class LoopBalancer(AbstractLoopRunner): class LoopBalancer(AbstractLoopRunner):
"""
keeps context for looping from a specific `out` scid to a specific `in` scid.
typically just produces `LoopJob`s indicating how much more we want to loop,
but in the case of repeat errors, it may interleave `LoopJobIdle`,
and if the channel is very problematic is returns `LoopJobDone.ABORTED`
"""
def __init__(self, out: str, in_: str, amount: int, looper: LoopRouter, bounds: TxBounds, parallelism: int=1): def __init__(self, out: str, in_: str, amount: int, looper: LoopRouter, bounds: TxBounds, parallelism: int=1):
super().__init__(looper, bounds, parallelism) super().__init__(looper, bounds, parallelism)
self.state = LoopPairState(out, in_, amount) self.state = LoopPairState(out, in_, amount)
@@ -746,11 +705,6 @@ def autobalance_once(rpc: RpcHelper, metrics: Metrics, bounds: TxBounds, paralle
def autobalance(rpc: RpcHelper, min_msat: int, max_msat: int, parallelism: int): def autobalance(rpc: RpcHelper, min_msat: int, max_msat: int, parallelism: int):
"""
balance all channels, repeatedly, until either
1. no channels want more inbound liquidity
2. or no channels want more outbound liquidity
"""
bounds = TxBounds(min_msat=min_msat, max_msat=max_msat) bounds = TxBounds(min_msat=min_msat, max_msat=max_msat)
metrics = Metrics() metrics = Metrics()
while not autobalance_once(rpc, metrics, bounds, parallelism): while not autobalance_once(rpc, metrics, bounds, parallelism):
@@ -764,12 +718,7 @@ def show_status(rpc: RpcHelper, full: bool=False):
try: try:
ch = rpc.localchannel(peerch["short_channel_id"]) ch = rpc.localchannel(peerch["short_channel_id"])
except: except:
try: print(f"{peerch['peer_id']} scid:{peerch['short_channel_id']} state:{peerch['state']} NO CHANNELS")
node = rpc.node(peerch['peer_id'])
except:
print(f"node_pubkey({peerch['peer_id']}) scid:{peerch.get('short_channel_id')} state:{peerch['state']} NO CHANNELS")
else:
print(f"node({node.alias}) scid:{peerch.get('short_channel_id')} state:{peerch['state']} NO CHANNELS")
else: else:
print(ch.to_str(with_scid=True, with_bal_ratio=True, with_payments=True, with_cost=full, with_ppm_theirs=True, with_ppm_mine=True, with_peer_id=full)) print(ch.to_str(with_scid=True, with_bal_ratio=True, with_payments=True, with_cost=full, with_ppm_theirs=True, with_ppm_mine=True, with_peer_id=full))
@@ -777,7 +726,7 @@ def main():
logging.basicConfig() logging.basicConfig()
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
parser = argparse.ArgumentParser(usage=__doc__) parser = argparse.ArgumentParser(description="rebalance lightning channel balances")
parser.add_argument("--verbose", action="store_true", help="more logging") parser.add_argument("--verbose", action="store_true", help="more logging")
parser.add_argument("--min-msat", default="999", help="min transaction size") parser.add_argument("--min-msat", default="999", help="min transaction size")
parser.add_argument("--max-msat", default="1000000", help="max transaction size") parser.add_argument("--max-msat", default="1000000", help="max transaction size")
@@ -804,18 +753,13 @@ def main():
rpc = RpcHelper(LightningRpc(RPC_FILE)) rpc = RpcHelper(LightningRpc(RPC_FILE))
action = getattr(args, "action", None) if args.action == "status":
if action is None:
show_status(rpc)
if action == "status":
show_status(rpc, full=args.full) show_status(rpc, full=args.full)
if action == "loop": if args.action == "loop":
balance_loop(rpc, out=args.out, in_=args.in_, amount_msat=int(args.amount), min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs)) balance_loop(rpc, out=args.out, in_=args.in_, amount_msat=int(args.amount), min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs))
if action == "autobalance": if args.action == "autobalance":
autobalance(rpc, min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs)) autobalance(rpc, min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs))
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -72,11 +72,13 @@
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.ext = [
# clightning takes up only a few MB. but then several hundred MB of crash logs that i should probably GC.
{ user = "clightning"; group = "clightning"; mode = "0710"; path = "/var/lib/clightning"; method = "bind"; } { user = "clightning"; group = "clightning"; mode = "0710"; path = "/var/lib/clightning"; method = "bind"; }
]; ];
# `lightning-cli` finds its RPC file via `~/.lightning/bitcoin/lightning-rpc`, to message the daemon
sane.user.fs.".lightning".symlink.target = "/var/lib/clightning";
# see bitcoin.nix for how to generate this # see bitcoin.nix for how to generate this
services.bitcoind.mainnet.rpc.users.clightning.passwordHMAC = services.bitcoind.mainnet.rpc.users.clightning.passwordHMAC =
"befcb82d9821049164db5217beb85439$2c31ac7db3124612e43893ae13b9527dbe464ab2d992e814602e7cb07dc28985"; "befcb82d9821049164db5217beb85439$2c31ac7db3124612e43893ae13b9527dbe464ab2d992e814602e7cb07dc28985";
@@ -103,7 +105,6 @@
users.users.clightning.extraGroups = [ "tor" ]; users.users.clightning.extraGroups = [ "tor" ];
systemd.services.clightning.after = [ "tor.service" ]; systemd.services.clightning.after = [ "tor.service" ];
systemd.services.clightning.requires = [ "tor.service" ];
# lightning-config contains fields from here: # lightning-config contains fields from here:
# - <https://docs.corelightning.org/docs/configuration> # - <https://docs.corelightning.org/docs/configuration>
@@ -115,24 +116,11 @@
# - fee-per-satoshi=<ppm> # - fee-per-satoshi=<ppm>
# - feature configs (i.e. experimental-xyz options) # - feature configs (i.e. experimental-xyz options)
sane.services.clightning.extraConfig = '' sane.services.clightning.extraConfig = ''
# log levels: "io", "trace", "debug", "info", "unusual", "broken" log-level=debug:lightningd
# log-level=info
# log-level=info:lightningd
# log-level=debug:lightningd
log-level=debug
# log-level=io
disable-plugin=cln-xpay
# let me use `lightning-cli dev-*` subcommands, fucktards.
developer
# `developer` enables `dev-*` but *disables* the older commands. asshats.
allow-deprecated-apis=true
# peerswap: # peerswap:
# - config example: <https://github.com/fort-nix/nix-bitcoin/pull/462/files#diff-b357d832705b8ce8df1f41934d613f79adb77c4cd5cd9e9eb12a163fca3e16c6> # - config example: <https://github.com/fort-nix/nix-bitcoin/pull/462/files#diff-b357d832705b8ce8df1f41934d613f79adb77c4cd5cd9e9eb12a163fca3e16c6>
# XXX: peerswap crashes clightning on launch. stacktrace is useless. # XXX: peerswap crashes clightning on launch. stacktrace is useless.
# plugin={lib.getExe' pkgs.peerswap "peerswap"} # plugin=${pkgs.peerswap}/bin/peerswap
# peerswap-db-path=/var/lib/clightning/peerswap/swaps # peerswap-db-path=/var/lib/clightning/peerswap/swaps
# peerswap-policy-path=... # peerswap-policy-path=...
''; '';
@@ -143,5 +131,5 @@
group = "clightning"; group = "clightning";
}; };
sane.programs.lightning-cli.enableFor.user.colin = true; # for debugging/admin: sane.programs.clightning.enableFor.user.colin = true; # for debugging/admin: `lightning-cli`
} }

View File

@@ -1,5 +1,4 @@
{ lib, ... }: { ... }:
lib.mkIf false #< 2024/07/27: i don't use it, too much surface-area for me to run it pro-bono (`systemd-analyze security monero`)
{ {
services.i2p.enable = true; services.i2p.enable = true;
} }

View File

@@ -1,6 +1,5 @@
# as of 2023/11/26: complete downloaded blockchain should be 200GiB on disk, give or take. # as of 2023/11/26: complete downloaded blockchain should be 200GiB on disk, give or take.
{ lib, ... }: { ... }:
lib.mkIf false #< 2024/07/27: i don't use it, too much surface-area for me to run it pro-bono (`systemd-analyze security monero`)
{ {
sane.persist.sys.byStore.ext = [ sane.persist.sys.byStore.ext = [
# /var/lib/monero/lmdb is what consumes most of the space # /var/lib/monero/lmdb is what consumes most of the space

View File

@@ -1,10 +1,10 @@
# tor settings: <https://2019.www.torproject.org/docs/tor-manual.html.en> # tor settings: <https://2019.www.torproject.org/docs/tor-manual.html.en>
{ lib, ... }: { lib, ... }:
{ {
sane.persist.sys.byStore.ephemeral = [ # tor hidden service hostnames aren't deterministic, so persist.
# N.B.: tor hidden service hostnames aren't deterministic, so if you need them # might be able to get away with just persisting /var/lib/tor/onion, not sure.
# to be preserved across reboots then persist /var/lib/tor/onion in "private" store. sane.persist.sys.byStore.plaintext = [
{ user = "tor"; group = "tor"; mode = "0710"; path = "/var/lib/tor"; method = "bind"; } { user = "tor"; group = "tor"; mode = "0710"; path = "/var/lib/tor"; method = "bind"; }
]; ];
# tor: `tor.enable` doesn't start a relay, exit node, proxy, etc. it's minimal. # tor: `tor.enable` doesn't start a relay, exit node, proxy, etc. it's minimal.

View File

@@ -1,36 +1,34 @@
{ ... }: { ... }:
{ {
imports = [ imports = [
./bitmagnet.nix ./calibre.nix
./coturn.nix ./coturn.nix
./cryptocurrencies ./cryptocurrencies
./email ./email
./ejabberd.nix ./ejabberd.nix
./freshrss.nix ./freshrss.nix
./export ./export
./hickory-dns.nix
./gerbera.nix
./gitea.nix ./gitea.nix
./goaccess.nix ./goaccess.nix
./ipfs.nix ./ipfs.nix
./jackett ./jackett.nix
./jellyfin ./jellyfin.nix
./kiwix-serve.nix ./kiwix-serve.nix
./komga.nix ./komga.nix
./lemmy.nix ./lemmy.nix
./matrix ./matrix
./minidlna.nix
./mumble.nix
./navidrome.nix ./navidrome.nix
./nginx ./nginx.nix
./nixos-prebuild.nix ./nixos-prebuild.nix
./nixserve.nix
./ntfy ./ntfy
./pict-rs.nix ./pict-rs.nix
./pleroma.nix ./pleroma.nix
./postgresql ./postgres.nix
./prosody ./prosody
./slskd.nix ./slskd.nix
./transmission ./transmission.nix
./trust-dns.nix
./wikipedia.nix ./wikipedia.nix
]; ];
} }

View File

@@ -44,61 +44,61 @@ in
# everything configured below was fine: used ejabberd for several months. # everything configured below was fine: used ejabberd for several months.
lib.mkIf false lib.mkIf false
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
{ user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; method = "bind"; } { user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; method = "bind"; }
]; ];
sane.ports.ports = lib.mkMerge ([ sane.ports.ports = lib.mkMerge ([
{ {
"3478" = { "3478" = {
protocol = [ "tcp" "udp" ]; protocol = [ "tcp" "udp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-stun-turn"; description = "colin-xmpp-stun-turn";
}; };
"5222" = { "5222" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-client-to-server"; description = "colin-xmpp-client-to-server";
}; };
"5223" = { "5223" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpps-client-to-server"; # XMPP over TLS description = "colin-xmpps-client-to-server"; # XMPP over TLS
}; };
"5269" = { "5269" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true; visibleTo.wan = true;
description = "colin-xmpp-server-to-server"; description = "colin-xmpp-server-to-server";
}; };
"5270" = { "5270" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true; visibleTo.wan = true;
description = "colin-xmpps-server-to-server"; # XMPP over TLS description = "colin-xmpps-server-to-server"; # XMPP over TLS
}; };
"5280" = { "5280" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-bosh"; description = "colin-xmpp-bosh";
}; };
"5281" = { "5281" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-bosh-https"; description = "colin-xmpp-bosh-https";
}; };
"5349" = { "5349" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-stun-turn-over-tls"; description = "colin-xmpp-stun-turn-over-tls";
}; };
"5443" = { "5443" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-web-services"; # file uploads, websockets, admin description = "colin-xmpp-web-services"; # file uploads, websockets, admin
}; };
} }
@@ -109,8 +109,8 @@ lib.mkIf false
numPorts = turnPortHigh - turnPortLow + 1; numPorts = turnPortHigh - turnPortLow + 1;
in { in {
protocol = [ "tcp" "udp" ]; protocol = [ "tcp" "udp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-turn-${builtins.toString count}-of-${builtins.toString numPorts}"; description = "colin-xmpp-turn-${builtins.toString count}-of-${builtins.toString numPorts}";
}; };
}) })
@@ -457,12 +457,13 @@ lib.mkIf false
mod_version = {}; mod_version = {};
}; };
}); });
sed = "${pkgs.gnused}/bin/sed";
in '' in ''
ip=$(cat '${config.sane.services.dyn-dns.ipPath}') ip=$(cat '${config.sane.services.dyn-dns.ipPath}')
# config is 444 (not 644), so we want to write out-of-place and then atomically move # config is 444 (not 644), so we want to write out-of-place and then atomically move
# TODO: factor this out into `sane-woop` helper? # TODO: factor this out into `sane-woop` helper?
rm -f /var/lib/ejabberd/ejabberd.yaml.new rm -f /var/lib/ejabberd/ejabberd.yaml.new
${lib.getExe pkgs.gnused} "s/%ANATIVE%/$ip/g" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new ${sed} "s/%ANATIVE%/$ip/g" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new
mv /var/lib/ejabberd/ejabberd.yaml{.new,} mv /var/lib/ejabberd/ejabberd.yaml{.new,}
''; '';

View File

@@ -25,10 +25,10 @@
# #
# debugging: general connectivity issues # debugging: general connectivity issues
# - test that inbound port 25 is unblocked: # - test that inbound port 25 is unblocked:
# - `curl https://canyouseeme.org/ --data 'port=25&IP=$MX_IP' | grep 'see your service'` # - `curl https://canyouseeme.org/ --data 'port=25&IP=185.157.162.178' | grep 'see your service'`
# - and retry with port 465, 587 # - and retry with port 465, 587
# - i think this API requires the queried IP match the source IP # - i think this API requires the queried IP match the source IP
# - if necessary, `systemctl stop postfix` and `sudo nc -l $MX_IP 25`, then try https://canyouseeme.org # - if necessary, `systemctl stop postfix` and `sudo nc -l 185.157.162.178 25`, then try https://canyouseeme.org
{ ... }: { ... }:
{ {

View File

@@ -8,14 +8,14 @@
{ {
sane.ports.ports."143" = { sane.ports.ports."143" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-imap-imap.uninsane.org"; description = "colin-imap-imap.uninsane.org";
}; };
sane.ports.ports."993" = { sane.ports.ports."993" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-imaps-imap.uninsane.org"; description = "colin-imaps-imap.uninsane.org";
}; };
@@ -83,8 +83,8 @@
# sieve_plugins = sieve_imapsieve # sieve_plugins = sieve_imapsieve
# } # }
# mail_debug = yes mail_debug = yes
# auth_debug = yes auth_debug = yes
# verbose_ssl = yes # verbose_ssl = yes
''; '';
@@ -124,9 +124,7 @@
# ]; # ];
}; };
}; };
environment.systemPackages = [ services.dovecot2.modules = [
# XXX(2025-03-16): dovecot loads modules from /run/current-system/sw/lib/dovecot/modules
# see: <https://github.com/NixOS/nixpkgs/pull/387642>
pkgs.dovecot_pigeonhole # enables sieve execution (?) pkgs.dovecot_pigeonhole # enables sieve execution (?)
]; ];
services.dovecot2.sieve = { services.dovecot2.sieve = {
@@ -143,5 +141,5 @@
''; '';
}; };
systemd.services.dovecot.serviceConfig.RestartSec = lib.mkForce "15s"; # nixos defaults this to 1s systemd.services.dovecot2.serviceConfig.RestartSec = lib.mkForce "15s"; # nixos defaults this to 1s
} }

View File

@@ -1,13 +1,6 @@
# postfix config options: <https://www.postfix.org/postconf.5.html> # postfix config options: <https://www.postfix.org/postconf.5.html>
# config files:
# - /etc/postfix/main.cf
# - /etc/postfix/master.cf
#
# logs:
# - postfix logs directly to *syslog*,
# so check e.g. ~/.local/share/rsyslog
{ config, lib, pkgs, ... }: { lib, pkgs, ... }:
let let
submissionOptions = { submissionOptions = {
@@ -25,14 +18,14 @@ let
}; };
in in
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
# TODO: mode? could be more granular # TODO: mode? could be more granular
{ user = "opendkim"; group = "opendkim"; path = "/var/lib/opendkim"; method = "bind"; } #< TODO: migrate to secrets { user = "opendkim"; group = "opendkim"; path = "/var/lib/opendkim"; method = "bind"; }
{ user = "root"; group = "root"; path = "/var/lib/postfix"; method = "bind"; }
{ user = "root"; group = "root"; path = "/var/spool/mail"; method = "bind"; } { user = "root"; group = "root"; path = "/var/spool/mail"; method = "bind"; }
# *probably* don't need these dirs: # *probably* don't need these dirs:
# "/var/lib/dhparams" # https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/dhparams.nix # "/var/lib/dhparams" # https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/dhparams.nix
# "/var/lib/dovecot" # "/var/lib/dovecot"
# "/var/lib/postfix"
]; ];
# XXX(2023/10/20): opening these ports in the firewall has the OPPOSITE effect as intended. # XXX(2023/10/20): opening these ports in the firewall has the OPPOSITE effect as intended.
@@ -63,7 +56,8 @@ in
sane.dns.zones."uninsane.org".inet = { sane.dns.zones."uninsane.org".inet = {
MX."@" = "10 mx.uninsane.org."; MX."@" = "10 mx.uninsane.org.";
A."mx" = "%AOVPNS%"; #< XXX: RFC's specify that the MX record CANNOT BE A CNAME. TODO: use "%AOVPNS%? # XXX: RFC's specify that the MX record CANNOT BE A CNAME
A."mx" = "185.157.162.178";
# Sender Policy Framework: # Sender Policy Framework:
# +mx => mail passes if it originated from the MX # +mx => mail passes if it originated from the MX
@@ -99,12 +93,9 @@ in
services.postfix.hostname = "mx.uninsane.org"; services.postfix.hostname = "mx.uninsane.org";
services.postfix.origin = "uninsane.org"; services.postfix.origin = "uninsane.org";
services.postfix.destination = [ "localhost" "uninsane.org" ]; services.postfix.destination = [ "localhost" "uninsane.org" ];
services.postfix.config.smtpd_tls_chain_files = [ services.postfix.sslCert = "/var/lib/acme/mx.uninsane.org/fullchain.pem";
"/var/lib/acme/mx.uninsane.org/key.pem" services.postfix.sslKey = "/var/lib/acme/mx.uninsane.org/key.pem";
"/var/lib/acme/mx.uninsane.org/fullchain.pem"
];
# see: `man 5 virtual`
services.postfix.virtual = '' services.postfix.virtual = ''
notify.matrix@uninsane.org matrix-synapse notify.matrix@uninsane.org matrix-synapse
@uninsane.org colin @uninsane.org colin
@@ -114,7 +105,7 @@ in
# smtpd_milters = local:/run/opendkim/opendkim.sock # smtpd_milters = local:/run/opendkim/opendkim.sock
# milter docs: http://www.postfix.org/MILTER_README.html # milter docs: http://www.postfix.org/MILTER_README.html
# mail filters for receiving email and from authorized SMTP clients (i.e. via submission) # mail filters for receiving email and from authorized SMTP clients (i.e. via submission)
# smtpd_milters = inet:$IP:8891 # smtpd_milters = inet:185.157.162.190:8891
# opendkim.sock will add a Authentication-Results header, with `dkim=pass|fail|...` value to received messages # opendkim.sock will add a Authentication-Results header, with `dkim=pass|fail|...` value to received messages
smtpd_milters = "unix:/run/opendkim/opendkim.sock"; smtpd_milters = "unix:/run/opendkim/opendkim.sock";
# mail filters for sendmail # mail filters for sendmail
@@ -145,32 +136,17 @@ in
# smtpd_sender_restrictions = reject_unknown_sender_domain # smtpd_sender_restrictions = reject_unknown_sender_domain
}; };
# debugging options:
# services.postfix.masterConfig = {
# "proxymap".args = [ "-v" ];
# "proxywrite".args = [ "-v" ];
# "relay".args = [ "-v" ];
# "smtp".args = [ "-v" ];
# "smtp_inet".args = [ "-v" ];
# "submission".args = [ "-v" ];
# "submissions".args = [ "-v" ];
# "submissions".chroot = false;
# "submissions".private = false;
# "submissions".privileged = true;
# };
services.postfix.enableSubmission = true; services.postfix.enableSubmission = true;
services.postfix.submissionOptions = submissionOptions; services.postfix.submissionOptions = submissionOptions;
services.postfix.enableSubmissions = true; services.postfix.enableSubmissions = true;
services.postfix.submissionsOptions = submissionOptions; services.postfix.submissionsOptions = submissionOptions;
systemd.services.postfix.unitConfig.RequiresMountsFor = [ systemd.services.postfix.after = [ "wireguard-wg-ovpns.service" ];
"/var/spool/mail" # spooky errors when postfix is run w/o this: `warning: connect #1 to subsystem private/proxymap: Connection refused` systemd.services.postfix.partOf = [ "wireguard-wg-ovpns.service" ];
"/var/lib/opendkim" systemd.services.postfix.serviceConfig = {
]; # run this behind the OVPN static VPN
NetworkNamespacePath = "/run/netns/ovpns";
# run these behind the OVPN static VPN };
sane.netns.ovpns.services = [ "opendkim" "postfix" ];
#### OPENDKIM #### OPENDKIM
@@ -189,37 +165,34 @@ in
# keeping this the same as the hostname seems simplest # keeping this the same as the hostname seems simplest
services.opendkim.selector = "mx"; services.opendkim.selector = "mx";
systemd.services.opendkim.after = [ "wireguard-wg-ovpns.service" ];
systemd.services.opendkim.partOf = [ "wireguard-wg-ovpns.service" ];
systemd.services.opendkim.serviceConfig = { systemd.services.opendkim.serviceConfig = {
# run this behind the OVPN static VPN
NetworkNamespacePath = "/run/netns/ovpns";
# /run/opendkim/opendkim.sock needs to be rw by postfix # /run/opendkim/opendkim.sock needs to be rw by postfix
UMask = lib.mkForce "0011"; UMask = lib.mkForce "0011";
}; };
#### OUTGOING MESSAGE REWRITING: #### OUTGOING MESSAGE REWRITING:
# - `man 5 header_checks` services.postfix.enableHeaderChecks = true;
# - <https://www.postfix.org/header_checks.5.html> services.postfix.headerChecks = [
# - populates `/var/lib/postfix/conf/header_checks` # intercept gitea registration confirmations and manually screen them
# XXX(2024-08-06): registration gating via email matches is AWFUL: {
# 1. bypassed if the service offers localization. # headerChecks are somehow ignorant of alias rules: have to redirect to a real user
# 2. if i try to forward the registration request, it may match the filter again and get sent back to my inbox. action = "REDIRECT colin@uninsane.org";
# 3. header checks are possibly under-used in the ecosystem, and may break postfix config. pattern = "/^Subject: Please activate your account/";
# services.postfix.enableHeaderChecks = true; }
# services.postfix.headerChecks = [ # intercept Matrix registration confirmations
# # intercept gitea registration confirmations and manually screen them {
# { action = "REDIRECT colin@uninsane.org";
# # headerChecks are somehow ignorant of alias rules: have to redirect to a real user pattern = "/^Subject:.*Validate your email/";
# action = "REDIRECT colin@uninsane.org"; }
# pattern = "/^Subject: Please activate your account/"; # XXX postfix only supports performing ONE action per header.
# } # {
# # intercept Matrix registration confirmations # action = "REPLACE Subject: git application: Please activate your account";
# { # pattern = "/^Subject:.*activate your account/";
# action = "REDIRECT colin@uninsane.org"; # }
# pattern = "/^Subject:.*Validate your email/"; ];
# }
# # XXX postfix only supports performing ONE action per header.
# # {
# # action = "REPLACE Subject: git application: Please activate your account";
# # pattern = "/^Subject:.*activate your account/";
# # }
# ];
} }

View File

@@ -2,7 +2,7 @@
{ {
imports = [ imports = [
./nfs.nix ./nfs.nix
./sftpgo ./sftpgo.nix
]; ];
users.groups.export = {}; users.groups.export = {};
@@ -10,7 +10,7 @@
fileSystems."/var/export/media" = { fileSystems."/var/export/media" = {
# everything in here could be considered publicly readable (based on the viewer's legal jurisdiction) # everything in here could be considered publicly readable (based on the viewer's legal jurisdiction)
device = "/var/media"; device = "/var/media";
options = [ "rbind" "nofail" ]; options = [ "rbind" ];
}; };
# fileSystems."/var/export/playground" = { # fileSystems."/var/export/playground" = {
# device = config.fileSystems."/mnt/persist/ext".device; # device = config.fileSystems."/mnt/persist/ext".device;
@@ -34,25 +34,20 @@
]; ];
sane.fs."/var/export/README.md" = { sane.fs."/var/export/README.md" = {
wantedBy = [ "nfs.service" "sftpgo.service" ];
file.text = '' file.text = ''
- media/ read-only: Videos, Music, Books, etc - media/ read-only: Videos, Music, Books, etc
- playground/ read-write*: use it to share files with other users of this server, inaccessible from the www - playground/ read-write: use it to share files with other users of this server
*if you can't write to it, make sure you're connected to the WiFi and not mobile.
''; '';
}; };
sane.fs."/var/export/playground/README.md" = { sane.fs."/var/export/playground/README.md" = {
wantedBy = [ "nfs.service" "sftpgo.service" ];
file.text = '' file.text = ''
this directory is intentionally read+write by anyone with access. this directory is intentionally read+write by anyone with access (i.e. on the LAN).
- share files - share files
- write poetry - write poetry
- be a friendly troll - be a friendly troll
''; '';
}; };
sane.fs."/var/export/.public_for_test/test" = {
file.text = ''
automated tests read this file to probe connectivity
'';
};
} }

View File

@@ -15,7 +15,6 @@
# - could maybe be done with some mount option? # - could maybe be done with some mount option?
{ config, lib, ... }: { config, lib, ... }:
lib.mkIf false #< TODO: remove nfs altogether! it's not exactly the most secure
{ {
services.nfs.server.enable = true; services.nfs.server.enable = true;

View File

@@ -0,0 +1,111 @@
# docs:
# - <https://github.com/drakkan/sftpgo>
# - config options: <https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md>
# - config defaults: <https://github.com/drakkan/sftpgo/blob/main/sftpgo.json>
# - nixos options: <repo:nixos/nixpkgs:nixos/modules/services/web-apps/sftpgo.nix>
# - nixos example: <repo:nixos/nixpkgs:nixos/tests/sftpgo.nix>
#
# sftpgo is a FTP server that also supports WebDAV, SFTP, and web clients.
{ config, lib, pkgs, sane-lib, ... }:
let
sftpgo_external_auth_hook = pkgs.static-nix-shell.mkPython3Bin {
pname = "sftpgo_external_auth_hook";
srcRoot = ./.;
};
in
{
# Client initiates a FTP "control connection" on port 21.
# - this handles the client -> server commands, and the server -> client status, but not the actual data
# - file data, directory listings, etc need to be transferred on an ephemeral "data port".
# - 50000-50100 is a common port range for this.
# 50000 is used by soulseek.
sane.ports.ports = {
"21" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-FTP server";
};
} // (sane-lib.mapToAttrs
(port: {
name = builtins.toString port;
value = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-FTP server data port range";
};
})
(lib.range 50050 50100)
);
services.sftpgo = {
enable = true;
group = "export";
settings = {
ftpd = {
bindings = [
{
# binding this means any wireguard client can connect
address = "10.0.10.5";
port = 21;
debug = true;
}
{
# binding this means any LAN client can connect (also WAN traffic forwarded from the gateway)
address = "10.78.79.51";
port = 21;
debug = true;
}
];
# active mode is susceptible to "bounce attacks", without much benefit over passive mode
disable_active_mode = true;
hash_support = true;
passive_port_range = {
start = 50050;
end = 50100;
};
banner = ''
Welcome, friends, to Colin's FTP server! Also available via NFS on the same host, but LAN-only.
Read-only access (LAN-restricted):
Username: "anonymous"
Password: "anonymous"
CONFIGURE YOUR CLIENT FOR "PASSIVE" mode, e.g. `ftp --passive uninsane.org`.
Please let me know if anything's broken or not as it should be. Otherwise, browse and transfer freely :)
'';
};
data_provider = {
driver = "memory";
external_auth_hook = "${sftpgo_external_auth_hook}/bin/sftpgo_external_auth_hook";
# track_quota:
# - 0: disable quota tracking
# - 1: quota is updated on every upload/delete, even if user has no quota restriction
# - 2: quota is updated on every upload/delete, but only if user/folder has a quota restriction (default, i think)
# track_quota = 2;
};
};
};
users.users.sftpgo.extraGroups = [
"export"
"media"
];
systemd.services.sftpgo = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
ReadWritePaths = [ "/var/export" ];
Restart = "always";
RestartSec = "20s";
UMask = lib.mkForce "0002";
};
};
}

View File

@@ -1,172 +0,0 @@
# docs:
# - <https://github.com/drakkan/sftpgo>
# - config options: <https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md>
# - config defaults: <https://github.com/drakkan/sftpgo/blob/main/sftpgo.json>
# - nixos options: <repo:nixos/nixpkgs:nixos/modules/services/web-apps/sftpgo.nix>
# - nixos example: <repo:nixos/nixpkgs:nixos/tests/sftpgo.nix>
#
# sftpgo is a FTP server that also supports WebDAV, SFTP, and web clients.
{ config, lib, pkgs, sane-lib, ... }:
let
external_auth_hook = pkgs.static-nix-shell.mkPython3 {
pname = "external_auth_hook";
srcRoot = ./.;
pkgs = [ "python3.pkgs.passlib" ];
};
# Client initiates a FTP "control connection" on port 21.
# - this handles the client -> server commands, and the server -> client status, but not the actual data
# - file data, directory listings, etc need to be transferred on an ephemeral "data port".
# - 50000-50100 is a common port range for this.
# 50000 is used by soulseek.
passiveStart = 50050;
passiveEnd = 50070;
in
{
sane.ports.ports = {
"21" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
description = "colin-FTP server";
};
"990" = {
protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true;
description = "colin-FTPS server";
};
} // (sane-lib.mapToAttrs
(port: {
name = builtins.toString port;
value = {
protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true;
description = "colin-FTP server data port range";
};
})
(lib.range passiveStart passiveEnd)
);
# use nginx/acme to produce a cert for FTPS
services.nginx.virtualHosts."ftp.uninsane.org" = {
addSSL = true;
enableACME = true;
};
sane.dns.zones."uninsane.org".inet.CNAME."ftp" = "native";
services.sftpgo = {
enable = true;
group = "export";
package = pkgs.sftpgo.overrideAttrs (upstream: {
patches = (upstream.patches or []) ++ [
# fix for compatibility with kodi:
# ftp LIST operation returns entries over-the-wire like:
# - dgrwxrwxr-x 1 ftp ftp 9 Apr 9 15:05 Videos
# however not all clients understand all mode bits (like that `g`, indicating SGID / group sticky bit).
# instead, only send mode bits which are well-understood.
# the full set of bits, from which i filter, is found here: <https://pkg.go.dev/io/fs#FileMode>
./safe_fileinfo.patch
];
});
settings = {
ftpd = {
bindings = [
{
# binding this means any wireguard client can connect
address = "10.0.10.5";
port = 21;
debug = true;
}
{
# binding this means any wireguard client can connect
address = "10.0.10.5";
port = 990;
debug = true;
tls_mode = 2; # 2 = "implicit FTPS": client negotiates TLS before any FTP command.
}
{
# binding this means any LAN client can connect (also WAN traffic forwarded from the gateway)
address = "10.78.79.51";
port = 21;
debug = true;
}
{
# binding this means any LAN client can connect (also WAN traffic forwarded from the gateway)
address = "10.78.79.51";
port = 990;
debug = true;
tls_mode = 2; # 2 = "implicit FTPS": client negotiates TLS before any FTP command.
}
{
# binding this means any doof client can connect (TLS only)
address = config.sane.netns.doof.veth.initns.ipv4;
port = 990;
debug = true;
tls_mode = 2; # 2 = "implicit FTPS": client negotiates TLS before any FTP command.
}
{
# binding this means any LAN client can connect via `ftp.uninsane.org` (TLS only)
address = config.sane.netns.doof.wg.address.ipv4;
port = 990;
debug = true;
tls_mode = 2; # 2 = "implicit FTPS": client negotiates TLS before any FTP command.
}
];
# active mode is susceptible to "bounce attacks", without much benefit over passive mode
disable_active_mode = true;
hash_support = true;
passive_port_range = {
start = passiveStart;
end = passiveEnd;
};
certificate_file = "/var/lib/acme/ftp.uninsane.org/full.pem";
certificate_key_file = "/var/lib/acme/ftp.uninsane.org/key.pem";
banner = ''
Welcome, friends, to Colin's FTP server! Also available via NFS on the same host, but LAN-only.
Read-only access (LAN clients see everything; WAN clients can only see /pub):
Username: "anonymous"
Password: "anonymous"
CONFIGURE YOUR CLIENT FOR "PASSIVE" MODE, e.g. `ftp --passive ftp.uninsane.org`.
Please let me know if anything's broken or not as it should be. Otherwise, browse and transfer freely :)
'';
};
data_provider = {
driver = "memory";
external_auth_hook = lib.getExe external_auth_hook;
# track_quota:
# - 0: disable quota tracking
# - 1: quota is updated on every upload/delete, even if user has no quota restriction
# - 2: quota is updated on every upload/delete, but only if user/folder has a quota restriction (default, i think)
# track_quota = 2;
};
};
};
users.users.sftpgo.extraGroups = [
"export"
"media"
"nginx" # to access certs
];
systemd.services.sftpgo = {
after = [ "network-online.target" ]; #< so that it reliably binds to all interfaces/netns's?
wants = [ "network-online.target" ];
unitConfig.RequiresMountsFor = [
"/var/export/media"
"/var/export/playground"
];
serviceConfig.ReadWritePaths = [ "/var/export" ];
serviceConfig.Restart = "always";
serviceConfig.RestartSec = "20s";
serviceConfig.UMask = lib.mkForce "0002";
};
}

View File

@@ -1,32 +0,0 @@
diff --git a/internal/ftpd/handler.go b/internal/ftpd/handler.go
index 036c3977..33211261 100644
--- a/internal/ftpd/handler.go
+++ b/internal/ftpd/handler.go
@@ -169,7 +169,7 @@ func (c *Connection) Stat(name string) (os.FileInfo, error) {
}
return nil, err
}
- return fi, nil
+ return vfs.NewFileInfo(name, fi.IsDir(), fi.Size(), fi.ModTime(), false), nil
}
// Name returns the name of this connection
@@ -315,7 +315,17 @@ func (c *Connection) ReadDir(name string) (ftpserver.DirLister, error) {
}, nil
}
- return c.ListDir(name)
+ lister, err := c.ListDir(name)
+ if err != nil {
+ return nil, err
+ }
+ return &patternDirLister{
+ DirLister: lister,
+ pattern: "*",
+ lastCommand: c.clientContext.GetLastCommand(),
+ dirName: name,
+ connectionPath: c.clientContext.Path(),
+ }, nil
}
// GetHandle implements ClientDriverExtentionFileTransfer

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3 -p python3.pkgs.passlib #!nix-shell -i python3 -p "python3.withPackages (ps: [ ])"
# vim: set filetype=python : # vim: set filetype=python :
# #
# available environment variables: # available environment variables:
@@ -37,16 +37,14 @@
# - it seems (empirically) that a user can't cd above their home directory. # - it seems (empirically) that a user can't cd above their home directory.
# though i don't have a reference for that in the docs. # though i don't have a reference for that in the docs.
import crypt
import json import json
import os import os
import passlib.hosts
from hmac import compare_digest from hmac import compare_digest
authFail = dict(username="") authFail = dict(username="")
PERM_DENY = []
PERM_LIST = [ "list" ]
PERM_RO = [ "list", "download" ] PERM_RO = [ "list", "download" ]
PERM_RW = [ PERM_RW = [
# read-only: # read-only:
@@ -69,12 +67,8 @@ TRUSTED_CREDS = [
# /etc/shadow style creds. # /etc/shadow style creds.
# mkpasswd -m sha-512 # mkpasswd -m sha-512
# $<method>$<salt>$<hash> # $<method>$<salt>$<hash>
"$6$Zq3c2u4ghUH4S6EP$pOuRt13sEKfX31OqPbbd1LuhS21C9MICMc94iRdTAgdAcJ9h95gQH/6Jf6Ie4Obb0oxQtojRJ1Pd/9QHOlFMW.", #< m. rocket boy "$6$Zq3c2u4ghUH4S6EP$pOuRt13sEKfX31OqPbbd1LuhS21C9MICMc94iRdTAgdAcJ9h95gQH/6Jf6Ie4Obb0oxQtojRJ1Pd/9QHOlFMW." #< m. rocket boy
"$6$B0NLGNdCL51PNse1$46G.aA1ATWIv5v.jUsKf4F3NS7emV2jB2gkZ3MytZtMvw2pjniHmRl0fywRjKW9TuXTeK9T50v.H0f2BaQ4PT1", #< v. telephony
] ]
TRUSTED_VIEWING_OR_PLAYGROUND_CREDS = [
# "$6$iikDajz5b.YH1.on$tfSzzBEtX8IeDiJJXCasOTxRTd7cFDKXU6dhlWYVhK6xDeJhV2fh6bmm1WIHItjIth9Eh9zNgUB8xibMIWCm/." # fedi (2024-08-27); music appreciation
];
def mkAuthOk(username: str, permissions: dict[str, list[str]]) -> dict: def mkAuthOk(username: str, permissions: dict[str, list[str]]) -> dict:
return dict( return dict(
@@ -116,10 +110,12 @@ def isLan(ip: str) -> bool:
def isWireguard(ip: str) -> bool: def isWireguard(ip: str) -> bool:
return ip.startswith("10.0.10.") return ip.startswith("10.0.10.")
def isTrustedCred(password: str, credlist: list[str] = TRUSTED_CREDS) -> bool: def isTrustedCred(password: str) -> bool:
for cred in credlist: for cred in TRUSTED_CREDS:
if passlib.hosts.linux_context.verify(password, cred): _, method, salt, hash_ = cred.split("$")
return True # assert method == "6", f"unrecognized crypt entry: {cred}"
if crypt.crypt(password, f"${method}${salt}") == cred:
return True
return False return False
@@ -133,30 +129,12 @@ def getAuthResponse(ip: str, username: str, password: str) -> dict:
return mkAuthOk(username, permissions = { return mkAuthOk(username, permissions = {
"/": PERM_RW, "/": PERM_RW,
"/playground": PERM_RW, "/playground": PERM_RW,
"/.public_for_test": PERM_RO,
"/media/Music": PERM_RO, #< i am too picky about Music organization
})
if isTrustedCred(password, TRUSTED_VIEWING_OR_PLAYGROUND_CREDS) and username != "colin":
return mkAuthOk(username, permissions = {
# error prone, but... not the worst if i miss something
"/": PERM_LIST,
"/media/archive": PERM_DENY,
"/media/Books": PERM_RO,
"/media/collections": PERM_DENY,
"/media/games": PERM_RO,
"/media/Music": PERM_RO,
"/media/Pictures": PERM_RO,
"/media/torrents": PERM_DENY,
"/media/Videos": PERM_RO,
"/playground": PERM_RW,
"/.public_for_test": PERM_RO,
}) })
if isWireguard(ip): if isWireguard(ip):
# allow any user from wireguard # allow any user from wireguard
return mkAuthOk(username, permissions = { return mkAuthOk(username, permissions = {
"/": PERM_RW, "/": PERM_RW,
"/playground": PERM_RW, "/playground": PERM_RW,
"/.public_for_test": PERM_RO,
}) })
if isLan(ip): if isLan(ip):
if username == "anonymous": if username == "anonymous":
@@ -164,19 +142,7 @@ def getAuthResponse(ip: str, username: str, password: str) -> dict:
return mkAuthOk("anonymous", permissions = { return mkAuthOk("anonymous", permissions = {
"/": PERM_RO, "/": PERM_RO,
"/playground": PERM_RW, "/playground": PERM_RW,
"/.public_for_test": PERM_RO,
}) })
if username == "anonymous":
# anonymous users from the www can have even more limited access.
# mostly because i need an easy way to test WAN connectivity :-)
return mkAuthOk("anonymous", permissions = {
# "/": PERM_DENY,
"/": PERM_LIST, #< REQUIRED, even for lftp to list a subdir
"/media": PERM_DENY,
"/playground": PERM_DENY,
"/.public_for_test": PERM_RO,
# "/README.md": PERM_RO, #< does not work
})
return authFail return authFail

View File

@@ -10,7 +10,6 @@
# ``` # ```
{ config, lib, pkgs, sane-lib, ... }: { config, lib, pkgs, sane-lib, ... }:
lib.mkIf false #< 2024/07/04: i haven't actively used this for months
{ {
sops.secrets."freshrss_passwd" = { sops.secrets."freshrss_passwd" = {
owner = config.users.users.freshrss.name; owner = config.users.users.freshrss.name;

View File

@@ -1,38 +0,0 @@
# gerbera UPNP/media server
# accessible from TVs on the LAN
# unauthenticated admin and playback UI at http://servo:49152/
#
# supposedly does transcoding, but i poked at it for 10 minutes and couldn't get that working
#
# compatibility:
# - LG TV: music: all working
# - LG TV: videos: mixed
{ lib, ... }:
lib.mkIf false #< XXX(2024-11-17): WORKS, but no better than any other service; slow to index and transcoding doesn't work
{
sane.ports.ports."1900" = {
protocol = [ "udp" ];
visibleTo.lan = true;
description = "colin-upnp-for-gerbera";
};
sane.ports.ports."49152" = {
protocol = [ "tcp" "udp" ]; # TODO: is udp required?
visibleTo.lan = true;
description = "colin-gerbera-http";
};
sane.persist.sys.byStore.plaintext = [
# persist the index database, since it takes a good 30 minutes to scan the media collection
{ user = "mediatomb"; group = "mediatomb"; mode = "0700"; path = "/var/lib/gerbera"; method = "bind"; }
];
services.mediatomb.enable = true;
services.mediatomb.serverName = "servo";
services.mediatomb.transcoding = true;
services.mediatomb.mediaDirectories = [
{ path = "/var/media/Music"; recursive = true; hidden-files = false; }
{ path = "/var/media/Videos/Film"; recursive = true; hidden-files = false; }
{ path = "/var/media/Videos/Shows"; recursive = true; hidden-files = false; }
];
users.users.mediatomb.extraGroups = [ "media" ];
}

View File

@@ -1,33 +1,22 @@
# config options: <https://docs.gitea.io/en-us/administration/config-cheat-sheet/> # config options: <https://docs.gitea.io/en-us/administration/config-cheat-sheet/>
# TODO: service shouldn't run as `git` user, but as `gitea`
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
{ user = "git"; group = "gitea"; mode = "0750"; path = "/var/lib/gitea"; method = "bind"; } # TODO: mode? could be more granular
{ user = "git"; group = "gitea"; path = "/var/lib/gitea"; method = "bind"; }
]; ];
sane.programs.gitea.enableFor.user.colin = true; # for admin, and monitoring
services.gitea.enable = true; services.gitea.enable = true;
services.gitea.user = "git"; # default is 'gitea' services.gitea.user = "git"; # default is 'gitea'
services.gitea.database.type = "postgres";
services.gitea.database.user = "git";
services.gitea.appName = "Perfectly Sane Git"; services.gitea.appName = "Perfectly Sane Git";
# services.gitea.disableRegistration = true; # services.gitea.disableRegistration = true;
services.gitea.database.createDatabase = false; # can only createDatabase if user ("git") == dbname ("gitea") services.gitea.database.createDatabase = false; #< silence warning which wants db user and name to be equal
services.gitea.database.type = "postgres"; # TODO: remove this after merge: <https://github.com/NixOS/nixpkgs/pull/268849>
services.gitea.database.user = "git";
# createDatabase=false means manually specify the connection; see: <https://github.com/NixOS/nixpkgs/pull/268849>
services.gitea.database.name = "gitea";
services.gitea.database.socket = "/run/postgresql"; #< would have been set if createDatabase = true services.gitea.database.socket = "/run/postgresql"; #< would have been set if createDatabase = true
services.postgresql.enable = true;
services.postgresql.ensureDatabases = [ "gitea" ];
services.postgresql.ensureUsers = [{
name = "git";
# ensureDBOwnership = true; # not possible if db name ("gitea") != db username ("git"); one-time manual setup required to grant user ownership of the relevant db
}];
# gitea doesn't create the git user # gitea doesn't create the git user
users.users.git = { users.users.git = {
description = "Gitea Service"; description = "Gitea Service";
@@ -49,41 +38,28 @@
ROOT_URL = "https://git.uninsane.org/"; ROOT_URL = "https://git.uninsane.org/";
}; };
service = { service = {
# timeout for email approval. 5760 = 4 days. 10080 = 7 days # timeout for email approval. 5760 = 4 days
ACTIVE_CODE_LIVE_MINUTES = 10080; ACTIVE_CODE_LIVE_MINUTES = 5760;
# REGISTER_EMAIL_CONFIRM = false; # REGISTER_EMAIL_CONFIRM = false;
# REGISTER_EMAIL_CONFIRM = true; #< override REGISTER_MANUAL_CONFIRM # REGISTER_MANUAL_CONFIRM = true;
REGISTER_MANUAL_CONFIRM = true; REGISTER_EMAIL_CONFIRM = true;
# not sure what this notifies *on*... # not sure what this notified on?
ENABLE_NOTIFY_MAIL = true; ENABLE_NOTIFY_MAIL = true;
# defaults to image-based captcha. # defaults to image-based captcha.
# also supports recaptcha (with custom URLs) or hCaptcha. # also supports recaptcha (with custom URLs) or hCaptcha.
ENABLE_CAPTCHA = true; ENABLE_CAPTCHA = true;
NOREPLY_ADDRESS = "noreply.anonymous.git@uninsane.org"; NOREPLY_ADDRESS = "noreply.anonymous.git@uninsane.org";
EMAIL_DOMAIN_BLOCKLIST = lib.concatStringsSep ", " [
"*.claychoen.top"
"*.gemmasmith.co.uk"
"*.jenniferlawrence.uk"
"*.sarahconnor.co.uk"
"*.marymarshall.co.uk"
];
};
session = {
COOKIE_SECURE = true;
# keep me logged in for 30 days
SESSION_LIFE_TIME = 60 * 60 * 24 * 30;
}; };
session.COOKIE_SECURE = true;
repository = { repository = {
DEFAULT_BRANCH = "master"; DEFAULT_BRANCH = "master";
ENABLE_PUSH_CREATE_USER = true;
ENABLE_PUSH_CREATE_ORG = true;
}; };
other = { other = {
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false; SHOW_FOOTER_TEMPLATE_LOAD_TIME = false;
}; };
ui = { ui = {
# options: "gitea-auto" (adapt to system theme), "gitea-dark", "gitea-light" # options: "auto", "gitea", "arc-green"
# DEFAULT_THEME = "gitea-auto"; DEFAULT_THEME = "arc-green";
# cache frontend assets if true # cache frontend assets if true
# USE_SERVICE_WORKER = true; # USE_SERVICE_WORKER = true;
}; };
@@ -92,10 +68,9 @@
# alternative is to use nixos-level config: # alternative is to use nixos-level config:
# services.gitea.mailerPasswordFile = ... # services.gitea.mailerPasswordFile = ...
ENABLED = true; ENABLED = true;
MAILER_TYPE = "sendmail";
FROM = "notify.git@uninsane.org"; FROM = "notify.git@uninsane.org";
PROTOCOL = "sendmail"; SENDMAIL_PATH = "${pkgs.postfix}/bin/sendmail";
SENDMAIL_PATH = lib.getExe' pkgs.postfix "sendmail";
SENDMAIL_ARGS = "--"; # most "sendmail" programs take options, "--" will prevent an email address being interpreted as an option.
}; };
time = { time = {
# options: ANSIC, UnixDate, RubyDate, RFC822, RFC822Z, RFC850, RFC1123, RFC1123Z, RFC3339, RFC3339Nano, Kitchen, Stamp, StampMilli, StampMicro, StampNano # options: ANSIC, UnixDate, RubyDate, RFC822, RFC822Z, RFC850, RFC1123, RFC1123Z, RFC3339, RFC3339Nano, Kitchen, Stamp, StampMilli, StampMicro, StampNano
@@ -104,75 +79,31 @@
}; };
}; };
systemd.services.gitea.wants = [ "postgresql.service" ];
systemd.services.gitea.serviceConfig = { systemd.services.gitea.serviceConfig = {
# nix default is AF_UNIX AF_INET AF_INET6. # nix default is AF_UNIX AF_INET AF_INET6.
# we need more protos for sendmail to work. i thought it only needed +AF_LOCAL, but that didn't work. # we need more protos for sendmail to work. i thought it only needed +AF_LOCAL, but that didn't work.
RestrictAddressFamilies = lib.mkForce "~"; RestrictAddressFamilies = lib.mkForce "~";
# add maildrop to allow sendmail to work # add maildrop to allow sendmail to work
ReadWritePaths = [ ReadWritePaths = lib.mkForce [
"/var/lib/postfix/queue/maildrop" "/var/lib/postfix/queue/maildrop"
"/var/lib/gitea"
]; ];
# rate limit the restarts to prevent systemd from disabling it
RestartSec = 5;
RestartMaxDelaySec = 30;
StartLimitBurst = 120;
RestartSteps = 5;
};
# services.openssh.settings.UsePAM = true; #< required for `git` user to authenticate
services.anubis.instances."git.uninsane.org" = {
settings.TARGET = "http://127.0.0.1:3000";
# allow IM clients/etc to show embeds/previews, else they just show "please verify you aren't a bot..."
botPolicy.openGraph.enabled = true;
}; };
# hosted git (web view and for `git <cmd>` use # hosted git (web view and for `git <cmd>` use
# TODO: enable publog? # TODO: enable publog?
services.nginx.virtualHosts."git.uninsane.org" = let services.nginx.virtualHosts."git.uninsane.org" = {
# XXX(2025-07-24): gitea's still being crawled, even with robots.txt.
# the load is less than when Anthropic first started, but it's still pretty high (like 600%).
# place behind anubis to prevent AI crawlers from hogging my CPU (gitea is slow to render pages).
proxyPassHeavy = "http://unix:${config.services.anubis.instances."git.uninsane.org".settings.BIND}";
# but anubis breaks embeds, so only protect the expensive repos.
proxyPassLight = "http://127.0.0.1:3000";
proxyTo = proxy: root: {
proxyPass = proxy;
recommendedProxySettings = true;
};
in {
forceSSL = true; # gitea complains if served over a different protocol than its config file says forceSSL = true; # gitea complains if served over a different protocol than its config file says
enableACME = true; enableACME = true;
# inherit kTLS; # inherit kTLS;
extraConfig = ''
client_max_body_size 100m;
'';
locations."/" = { locations."/" = {
proxyPass = proxyPassLight; proxyPass = "http://127.0.0.1:3000";
recommendedProxySettings = true;
}; };
# selectively proxy the heavyweight items through anubis.
# a typical interaction is:
# nginx:/colin/linux -> anubis:/colin/linux -> browser is served a loading page
# -> nginx:.within.website/x/cmd/anubis/api/pass-challenge?response=... -> anubis:.within.website/x/cmd/anubis/api/pass-challenge?response=... -> browser is forwarded to /colin/linux
# -> nginx:/colin/linux -> anubis:/colin/linux -> gitea:/colin/linux -> browser is served the actual content
locations."/.within.website/" = proxyTo proxyPassHeavy;
locations."/colin/linux" = proxyTo proxyPassHeavy;
locations."/colin/nixpkgs" = proxyTo proxyPassHeavy;
locations."/colin/opencellid-mirror" = proxyTo proxyPassHeavy;
locations."/colin/podcastindex-db-mirror" = proxyTo proxyPassHeavy;
# fuck you @anthropic
# locations."= /robots.txt".extraConfig = ''
# return 200 "User-agent: *\nDisallow: /\n";
# '';
# gitea serves all `raw` files as content-type: plain, but i'd like to serve them as their actual content type. # gitea serves all `raw` files as content-type: plain, but i'd like to serve them as their actual content type.
# or at least, enough to make specific pages viewable (serving unoriginal content as arbitrary content type is dangerous). # or at least, enough to make specific pages viewable (serving unoriginal content as arbitrary content type is dangerous).
locations."~ ^/colin/phone-case-cq/raw/.*.html" = { locations."~ ^/colin/phone-case-cq/raw/.*.html" = {
proxyPass = proxyPassLight; proxyPass = "http://127.0.0.1:3000";
recommendedProxySettings = true;
extraConfig = '' extraConfig = ''
proxy_hide_header Content-Type; proxy_hide_header Content-Type;
default_type text/html; default_type text/html;
@@ -180,8 +111,7 @@
''; '';
}; };
locations."~ ^/colin/phone-case-cq/raw/.*.js" = { locations."~ ^/colin/phone-case-cq/raw/.*.js" = {
proxyPass = proxyPassLight; proxyPass = "http://127.0.0.1:3000";
recommendedProxySettings = true;
extraConfig = '' extraConfig = ''
proxy_hide_header Content-Type; proxy_hide_header Content-Type;
default_type text/html; default_type text/html;
@@ -195,7 +125,7 @@
sane.ports.ports."22" = { sane.ports.ports."22" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.doof = true; visibleTo.wan = true;
description = "colin-git@git.uninsane.org"; description = "colin-git@git.uninsane.org";
}; };
} }

View File

@@ -1,5 +1,4 @@
{ lib, pkgs, ... }: { pkgs, ... }:
lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several months
{ {
# based on <https://bytes.fyi/real-time-goaccess-reports-with-nginx/> # based on <https://bytes.fyi/real-time-goaccess-reports-with-nginx/>
# log-format setting can be derived with this tool if custom: # log-format setting can be derived with this tool if custom:
@@ -11,7 +10,7 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several mon
description = "GoAccess server monitoring"; description = "GoAccess server monitoring";
serviceConfig = { serviceConfig = {
ExecStart = '' ExecStart = ''
${lib.getExe pkgs.goaccess} \ ${pkgs.goaccess}/bin/goaccess \
-f /var/log/nginx/public.log \ -f /var/log/nginx/public.log \
--log-format=VCOMBINED \ --log-format=VCOMBINED \
--real-time-html \ --real-time-html \
@@ -21,27 +20,25 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several mon
--ignore-panel=HOSTS \ --ignore-panel=HOSTS \
--ws-url=wss://sink.uninsane.org:443/ws \ --ws-url=wss://sink.uninsane.org:443/ws \
--port=7890 \ --port=7890 \
-o /var/lib/goaccess/index.html -o /var/lib/uninsane/sink/index.html
''; '';
ExecReload = "${lib.getExe' pkgs.coreutils "kill"} -HUP $MAINPID"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Type = "simple"; Type = "simple";
Restart = "on-failure"; Restart = "on-failure";
RestartSec = "10s"; RestartSec = "10s";
# hardening # hardening
# TODO: run as `goaccess` user and add `goaccess` user to group `nginx`. WorkingDirectory = "/tmp";
NoNewPrivileges = true; NoNewPrivileges = true;
PrivateDevices = "yes";
PrivateTmp = true; PrivateTmp = true;
ProtectHome = "read-only"; ProtectHome = "read-only";
ProtectSystem = "strict";
SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @privileged @reboot @resources @setuid @swap @raw-io";
ReadOnlyPaths = "/";
ReadWritePaths = [ "/proc/self" "/var/lib/uninsane/sink" ];
PrivateDevices = "yes";
ProtectKernelModules = "yes"; ProtectKernelModules = "yes";
ProtectKernelTunables = "yes"; ProtectKernelTunables = "yes";
ProtectSystem = "strict";
ReadOnlyPaths = [ "/var/log/nginx" ];
ReadWritePaths = [ "/proc/self" "/var/lib/goaccess" ];
StateDirectory = "goaccess";
SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @privileged @reboot @resources @setuid @swap @raw-io";
WorkingDirectory = "/var/lib/goaccess";
}; };
after = [ "network.target" ]; after = [ "network.target" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
@@ -52,11 +49,10 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several mon
addSSL = true; addSSL = true;
enableACME = true; enableACME = true;
# inherit kTLS; # inherit kTLS;
root = "/var/lib/goaccess"; root = "/var/lib/uninsane/sink";
locations."/ws" = { locations."/ws" = {
proxyPass = "http://127.0.0.1:7890"; proxyPass = "http://127.0.0.1:7890";
recommendedProxySettings = true;
# XXX not sure how much of this is necessary # XXX not sure how much of this is necessary
extraConfig = '' extraConfig = ''
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -1,149 +0,0 @@
# TODO: split this file apart into smaller files to make it easier to understand
{ config, lib, ... }:
let
dyn-dns = config.sane.services.dyn-dns;
nativeAddrs = lib.mapAttrs (_name: builtins.head) config.sane.dns.zones."uninsane.org".inet.A;
in
{
sane.ports.ports."53" = {
protocol = [ "udp" "tcp" ];
visibleTo.lan = true;
# visibleTo.wan = true;
visibleTo.ovpns = true;
visibleTo.doof = true;
description = "colin-dns-hosting";
};
sane.dns.zones."uninsane.org".TTL = 900;
# SOA record structure: <https://en.wikipedia.org/wiki/SOA_record#Structure>
# SOA MNAME RNAME (... rest)
# MNAME = Master name server for this zone. this is where update requests should be sent.
# RNAME = admin contact (encoded email address)
# Serial = YYYYMMDDNN, where N is incremented every time this file changes, to trigger secondary NS to re-fetch it.
# Refresh = how frequently secondary NS should query master
# Retry = how long secondary NS should wait until re-querying master after a failure (must be < Refresh)
# Expire = how long secondary NS should continue to reply to queries after master fails (> Refresh + Retry)
sane.dns.zones."uninsane.org".inet = {
SOA."@" = ''
ns1.uninsane.org. admin-dns.uninsane.org. (
2023092101 ; Serial
4h ; Refresh
30m ; Retry
7d ; Expire
5m) ; Negative response TTL
'';
TXT."rev" = "2023092101";
CNAME."native" = "%CNAMENATIVE%";
A."@" = "%ANATIVE%";
A."servo.wan" = "%AWAN%";
A."servo.doof" = "%ADOOF%";
A."servo.lan" = config.sane.hosts.by-name."servo".lan-ip;
A."servo.hn" = config.sane.hosts.by-name."servo".wg-home.ip;
# XXX NS records must also not be CNAME
# it's best that we keep this identical, or a superset of, what org. lists as our NS.
# so, org. can specify ns2/ns3 as being to the VPN, with no mention of ns1. we provide ns1 here.
A."ns1" = "%ANATIVE%";
A."ns2" = "%ADOOF%";
A."ovpns" = "%AOVPNS%";
NS."@" = [
"ns1.uninsane.org."
"ns2.uninsane.org."
];
};
services.hickory-dns.settings.zones = builtins.attrNames config.sane.dns.zones;
networking.nat.enable = true; #< TODO: try removing this?
# networking.nat.extraCommands = ''
# # redirect incoming DNS requests from LAN addresses
# # to the LAN-specialized DNS service
# # N.B.: use the `nixos-*` chains instead of e.g. PREROUTING
# # because they get cleanly reset across activations or `systemctl restart firewall`
# # instead of accumulating cruft
# iptables -t nat -A nixos-nat-pre -p udp --dport 53 \
# -m iprange --src-range 10.78.76.0-10.78.79.255 \
# -j DNAT --to-destination :1053
# iptables -t nat -A nixos-nat-pre -p tcp --dport 53 \
# -m iprange --src-range 10.78.76.0-10.78.79.255 \
# -j DNAT --to-destination :1053
# '';
# sane.ports.ports."1053" = {
# # because the NAT above redirects in nixos-nat-pre, LAN requests behave as though they arrived on the external interface at the redirected port.
# # TODO: try nixos-nat-post instead?
# # TODO: or, don't NAT from port 53 -> port 1053, but rather nat from LAN addr to a loopback addr.
# # - this is complicated in that loopback is a different interface than eth0, so rewriting the destination address would cause the packets to just be dropped by the interface
# protocol = [ "udp" "tcp" ];
# visibleTo.lan = true;
# description = "colin-redirected-dns-for-lan-namespace";
# };
sane.services.hickory-dns.enable = true;
sane.services.hickory-dns.instances = let
mkSubstitutions = flavor: {
"%ADOOF%" = config.sane.netns.doof.wg.address.ipv4;
"%ANATIVE%" = nativeAddrs."servo.${flavor}";
"%AOVPNS%" = config.sane.netns.ovpns.wg.address.ipv4;
"%AWAN%" = "$(cat '${dyn-dns.ipPath}')";
"%CNAMENATIVE%" = "servo.${flavor}";
};
in
{
doof = {
substitutions = mkSubstitutions "doof";
listenAddrsIpv4 = [
config.sane.netns.doof.veth.initns.ipv4
config.sane.netns.doof.wg.address.ipv4
nativeAddrs."servo.lan"
# config.sane.netns.ovpns.veth.initns.ipv4
];
};
# hn = {
# substitutions = mkSubstitutions "hn";
# listenAddrsIpv4 = [ nativeAddrs."servo.hn" ];
# enableRecursiveResolver = true; #< allow wireguard clients to use this as their DNS resolver
# # extraConfig = {
# # zones = [
# # {
# # # forward the root zone to the local DNS resolver
# # # to allow wireguard clients to use this as their DNS resolver
# # zone = ".";
# # zone_type = "Forward";
# # stores = {
# # type = "forward";
# # name_servers = [
# # {
# # socket_addr = "127.0.0.53:53";
# # protocol = "udp";
# # trust_nx_responses = true;
# # }
# # ];
# # };
# # }
# # ];
# # };
# };
# lan = {
# substitutions = mkSubstitutions "lan";
# listenAddrsIpv4 = [ nativeAddrs."servo.lan" ];
# # port = 1053;
# };
# wan = {
# substitutions = mkSubstitutions "wan";
# listenAddrsIpv4 = [
# nativeAddrs."servo.lan"
# ];
# };
};
systemd.services.hickory-dns-doof.after = [
# service will fail to bind the veth, otherwise
"netns-doof-veth.service"
];
sane.services.dyn-dns.restartOnChange = lib.map (c: "${c.service}.service") (builtins.attrValues config.sane.services.hickory-dns.instances);
}

View File

@@ -10,7 +10,7 @@
lib.mkIf false # i don't actively use ipfs anymore lib.mkIf false # i don't actively use ipfs anymore
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
# TODO: mode? could be more granular # TODO: mode? could be more granular
{ user = "261"; group = "261"; path = "/var/lib/ipfs"; method = "bind"; } { user = "261"; group = "261"; path = "/var/lib/ipfs"; method = "bind"; }
]; ];
@@ -27,7 +27,6 @@ lib.mkIf false # i don't actively use ipfs anymore
locations."/" = { locations."/" = {
proxyPass = "http://127.0.0.1:8080"; proxyPass = "http://127.0.0.1:8080";
recommendedProxySettings = true;
extraConfig = '' extraConfig = ''
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Ipfs-Gateway-Prefix ""; proxy_set_header X-Ipfs-Gateway-Prefix "";

View File

@@ -0,0 +1,33 @@
{ ... }:
{
sane.persist.sys.byStore.plaintext = [
# TODO: mode? we only need this to save Indexer creds ==> migrate to config?
{ user = "root"; group = "root"; path = "/var/lib/jackett"; method = "bind"; }
];
services.jackett.enable = true;
systemd.services.jackett.after = [ "wireguard-wg-ovpns.service" ];
systemd.services.jackett.partOf = [ "wireguard-wg-ovpns.service" ];
systemd.services.jackett.serviceConfig = {
# run this behind the OVPN static VPN
NetworkNamespacePath = "/run/netns/ovpns";
# patch jackett to listen on the public interfaces
# ExecStart = lib.mkForce "${pkgs.jackett}/bin/Jackett --NoUpdates --DataFolder /var/lib/jackett/.config/Jackett --ListenPublic";
};
# jackett torrent search
services.nginx.virtualHosts."jackett.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
locations."/" = {
# proxyPass = "http://ovpns.uninsane.org:9117";
proxyPass = "http://10.0.1.6:9117";
recommendedProxySettings = true;
};
};
sane.dns.zones."uninsane.org".inet.CNAME."jackett" = "native";
}

View File

@@ -1,69 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.jackett;
in
{
sane.persist.sys.byStore.private = [
# TODO: mode? we only need this to save Indexer creds ==> migrate to config?
{ user = "jackett"; group = "jackett"; path = "/var/lib/jackett"; method = "bind"; }
];
services.jackett.enable = true;
# run this behind the OVPN static VPN
sane.netns.ovpns.services = [ "jackett" ];
systemd.services.jackett = {
serviceConfig.ExecStartPre = [
# abort if public IP is not as expected
"${lib.getExe pkgs.sane-scripts.ip-check} --no-upnp --expect ${config.sane.netns.ovpns.wg.address.ipv4}"
];
# patch in `--ListenPublic` so that it's reachable from the netns veth.
# this also makes it reachable from the VPN pub address. oh well.
serviceConfig.ExecStart = lib.mkForce "${lib.getExe' cfg.package "Jackett"} --ListenPublic --NoUpdates --DataFolder '${cfg.dataDir}'";
serviceConfig.RestartSec = "30s";
# hardening (systemd-analyze security jackett)
# TODO: upstream into nixpkgs
serviceConfig.StateDirectory = "jackett";
serviceConfig.LockPersonality = true;
serviceConfig.NoNewPrivileges = true;
# serviceConfig.MemoryDenyWriteExecute = true; #< Failed to create CoreCLR, HRESULT: 0x80004005
serviceConfig.PrivateDevices = true;
serviceConfig.PrivateMounts = true;
serviceConfig.PrivateTmp = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProcSubset = "pid";
serviceConfig.ProtectClock = true;
serviceConfig.ProtectControlGroups = true;
serviceConfig.ProtectHome = true;
serviceConfig.ProtectHostname = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectKernelModules = true;
serviceConfig.ProtectKernelTunables = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProtectSystem = "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
serviceConfig.RestrictNamespaces = true;
serviceConfig.RestrictSUIDSGID = true;
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.SystemCallFilter = [ "@system-service" "~@privileged" ];
};
# jackett torrent search
services.nginx.virtualHosts."jackett.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
locations."/" = {
proxyPass = "http://${config.sane.netns.ovpns.veth.netns.ipv4}:9117";
recommendedProxySettings = true;
};
locations."= /robots.txt".extraConfig = ''
return 200 "User-agent: *\nDisallow: /\n";
'';
};
sane.dns.zones."uninsane.org".inet.CNAME."jackett" = "native";
}

View File

@@ -0,0 +1,127 @@
# configuration options (today i don't store my config in nix):
#
# - jellyfin-web can be statically configured (result/share/jellyfin-web/config.json)
# - <https://jellyfin.org/docs/general/clients/web-config>
# - configure server list, plugins, "menuLinks", colors
#
# - jellfyin server is configured in /var/lib/jellfin/
# - root/default/<LibraryType>/
# - <LibraryName>.mblink: contains the directory name where this library lives
# - options.xml: contains preferences which were defined in the web UI during import
# - e.g. `EnablePhotos`, `EnableChapterImageExtraction`, etc.
# - config/encoding.xml: transcoder settings
# - config/system.xml: misc preferences like log file duration, audiobook resume settings, etc.
# - data/jellyfin.db: maybe account definitions? internal state?
{ config, lib, ... }:
{
# https://jellyfin.org/docs/general/networking/index.html
sane.ports.ports."1900" = {
protocol = [ "udp" ];
visibleTo.lan = true;
description = "colin-upnp-for-jellyfin";
};
sane.ports.ports."7359" = {
protocol = [ "udp" ];
visibleTo.lan = true;
description = "colin-jellyfin-specific-client-discovery";
# ^ not sure if this is necessary: copied this port from nixos jellyfin.openFirewall
};
# not sure if 8096/8920 get used either:
sane.ports.ports."8096" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
description = "colin-jellyfin-http-lan";
};
sane.ports.ports."8920" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
description = "colin-jellyfin-https-lan";
};
sane.persist.sys.byStore.plaintext = [
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin"; method = "bind"; }
];
sane.fs."/var/lib/jellyfin/config/logging.json" = {
# "Emby.Dlna" logging: <https://jellyfin.org/docs/general/networking/dlna>
symlink.text = ''
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning",
"Emby.Dlna": "Debug",
"Emby.Dlna.Eventing": "Debug"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": [ "FromLogContext", "WithThreadId" ]
}
}
'';
wantedBeforeBy = [ "jellyfin.service" ];
};
# Jellyfin multimedia server
# this is mostly taken from the official jellfin.org docs
services.nginx.virtualHosts."jelly.uninsane.org" = {
addSSL = true;
enableACME = true;
# inherit kTLS;
locations."/" = {
proxyPass = "http://127.0.0.1:8096";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering when the nginx proxy gets very resource heavy upon streaming
proxy_buffering off;
'';
};
# locations."/web/" = {
# proxyPass = "http://127.0.0.1:8096/web/index.html";
# extraConfig = ''
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header X-Forwarded-Protocol $scheme;
# proxy_set_header X-Forwarded-Host $http_host;
# '';
# };
locations."/socket" = {
proxyPass = "http://127.0.0.1:8096";
extraConfig = ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
'';
};
};
sane.dns.zones."uninsane.org".inet.CNAME."jelly" = "native";
services.jellyfin.enable = true;
}

View File

@@ -1,173 +0,0 @@
# configuration options (today only a *subset* of the config is done in nix)
# - jellyfin-web can be statically configured (result/share/jellyfin-web/config.json)
# - <https://jellyfin.org/docs/general/clients/web-config>
# - configure server list, plugins, "menuLinks", colors
#
# - jellfyin server is configured in /var/lib/jellfin/
# - root/default/<LibraryType>/
# - <LibraryName>.mblink: contains the directory name where this library lives
# - options.xml: contains preferences which were defined in the web UI during import
# - e.g. `EnablePhotos`, `EnableChapterImageExtraction`, etc.
# - config/encoding.xml: transcoder settings
# - config/system.xml: misc preferences like log file duration, audiobook resume settings, etc.
# - data/jellyfin.db: maybe account definitions? internal state?
#
# N.B.: default install DOES NOT SUPPORT DLNA out of the box.
# one must install it as a "plugin", which can be done through the UI.
{ config, lib, ... }:
# lib.mkIf false #< XXX(2024-11-17): disabled because it hasn't been working for months; web UI hangs on load, TVs see no files
{
config = lib.mkIf (config.sane.maxBuildCost >= 2) {
# https://jellyfin.org/docs/general/networking/index.html
sane.ports.ports."1900" = {
protocol = [ "udp" ];
visibleTo.lan = true;
description = "colin-upnp-for-jellyfin";
};
sane.ports.ports."7359" = {
protocol = [ "udp" ];
visibleTo.lan = true;
description = "colin-jellyfin-specific-client-discovery";
# ^ not sure if this is necessary: copied this port from nixos jellyfin.openFirewall
};
# not sure if 8096/8920 get used either:
sane.ports.ports."8096" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
description = "colin-jellyfin-http-lan";
};
sane.ports.ports."8920" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
description = "colin-jellyfin-https-lan";
};
sane.persist.sys.byStore.plaintext = [
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin/data"; method = "bind"; }
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin/metadata"; method = "bind"; }
# TODO: ship plugins statically, via nix. that'll be less fragile
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin/plugins/DLNA_5.0.0.0"; method = "bind"; }
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin/root"; method = "bind"; }
];
sane.persist.sys.byStore.ephemeral = [
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin/log"; method = "bind"; }
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin/transcodes"; method = "bind"; }
];
services.jellyfin.enable = true;
users.users.jellyfin.extraGroups = [ "media" ];
sane.fs."/var/lib/jellyfin".dir.acl = {
user = "jellyfin";
group = "jellyfin";
mode = "0700";
};
# `"Jellyfin.Plugin.Dlna": "Debug"` logging: <https://jellyfin.org/docs/general/networking/dlna>
# TODO: switch Dlna back to 'Information' once satisfied with stability
sane.fs."/var/lib/jellyfin/config/logging.json".symlink.text = ''
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning",
"Jellyfin.Plugin.Dlna": "Debug"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": [ "FromLogContext", "WithThreadId" ]
}
}
'';
sane.fs."/var/lib/jellyfin/config/network.xml".file.text = ''
<?xml version="1.0" encoding="utf-8"?>
<NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<BaseUrl />
<EnableHttps>false</EnableHttps>
<RequireHttps>false</RequireHttps>
<InternalHttpPort>8096</InternalHttpPort>
<InternalHttpsPort>8920</InternalHttpsPort>
<PublicHttpPort>8096</PublicHttpPort>
<PublicHttpsPort>8920</PublicHttpsPort>
<AutoDiscovery>true</AutoDiscovery>
<EnableUPnP>false</EnableUPnP>
<EnableIPv4>true</EnableIPv4>
<EnableIPv6>false</EnableIPv6>
<EnableRemoteAccess>true</EnableRemoteAccess>
<LocalNetworkSubnets>
<string>10.78.76.0/22</string>
</LocalNetworkSubnets>
<KnownProxies>
<string>127.0.0.1</string>
<string>localhost</string>
<string>10.78.79.1</string>
</KnownProxies>
<IgnoreVirtualInterfaces>false</IgnoreVirtualInterfaces>
<VirtualInterfaceNames />
<EnablePublishedServerUriByRequest>false</EnablePublishedServerUriByRequest>
<PublishedServerUriBySubnet />
<RemoteIPFilter />
<IsRemoteIPFilterBlacklist>false</IsRemoteIPFilterBlacklist>
</NetworkConfiguration>
'';
# guest user id is `5ad194d60dca41de84b332950ffc4308`
sane.fs."/var/lib/jellyfin/plugins/configurations/Jellyfin.Plugin.Dlna.xml".file.text = ''
<?xml version="1.0" encoding="utf-8"?>
<DlnaPluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<EnablePlayTo>true</EnablePlayTo>
<ClientDiscoveryIntervalSeconds>60</ClientDiscoveryIntervalSeconds>
<BlastAliveMessages>true</BlastAliveMessages>
<AliveMessageIntervalSeconds>180</AliveMessageIntervalSeconds>
<SendOnlyMatchedHost>true</SendOnlyMatchedHost>
<DefaultUserId>5ad194d6-0dca-41de-84b3-32950ffc4308</DefaultUserId>
</DlnaPluginConfiguration>
'';
# fix LG TV to play more files.
# there are certain files for which it only supports Direct Play (not even "Direct Stream" -- but "Direct Play").
# this isn't a 100% fix: patching the profile allows e.g. Azumanga Daioh to play,
# but A Place Further Than the Universe still fails as before.
#
# profile is based on upstream: <https://github.com/jellyfin/jellyfin-plugin-dlna>
sane.fs."/var/lib/jellyfin/plugins/DLNA_5.0.0.0/profiles/LG Smart TV.xml".symlink.target = ./dlna/user/LG_Smart_TV.xml;
# XXX(2024-11-17): old method, but the file referenced seems not to be used and setting just it causes failures:
# > [DBG] Jellyfin.Plugin.Dlna.ContentDirectory.ContentDirectoryService: Not eligible for DirectPlay due to unsupported subtitles
# sane.fs."/var/lib/jellyfin/plugins/configurations/dlna/user/LG Smart TV.xml".symlink.target = ./dlna/user/LG_Smart_TV.xml;
systemd.services.jellyfin.unitConfig.RequiresMountsFor = [
"/var/media"
];
# Jellyfin multimedia server
# this is mostly taken from the official jellfin.org docs
services.nginx.virtualHosts."jelly.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
locations."/" = {
proxyPass = "http://127.0.0.1:8096";
proxyWebsockets = true;
recommendedProxySettings = true;
# extraConfig = ''
# # Disable buffering when the nginx proxy gets very resource heavy upon streaming
# proxy_buffering off;
# '';
};
};
sane.dns.zones."uninsane.org".inet.CNAME."jelly" = "native";
};
}

View File

@@ -1,91 +0,0 @@
<?xml version="1.0"?>
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>LG Smart TV</Name>
<Identification>
<ModelName>LG TV</ModelName>
<Headers />
</Identification>
<Manufacturer>Jellyfin</Manufacturer>
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
<ModelName>Jellyfin Server</ModelName>
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
<ModelNumber>01</ModelNumber>
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
<AlbumArtPn>JPEG_SM</AlbumArtPn>
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
<MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" />
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders>
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="ts,mpegts,avi,mkv,m2ts" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264,hevc" type="Video" />
<DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264,mpeg4,hevc" type="Video" />
<DirectPlayProfile container="mp3" type="Audio" />
<DirectPlayProfile container="jpeg" type="Photo" />
<DirectPlayProfile container="" audioCodec="" videoCodec="" type="Video" />
</DirectPlayProfiles>
<TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles>
<ContainerProfiles>
<ContainerProfile type="Photo">
<Conditions>
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
</Conditions>
</ContainerProfile>
</ContainerProfiles>
<CodecProfiles>
<CodecProfile type="Video" codec="mpeg4">
<Conditions>
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
</Conditions>
<ApplyConditions />
</CodecProfile>
<CodecProfile type="Video" codec="h264">
<Conditions>
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
</Conditions>
<ApplyConditions />
</CodecProfile>
<CodecProfile type="VideoAudio" codec="ac3,eac3,aac,mp3">
<Conditions>
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
</Conditions>
<ApplyConditions />
</CodecProfile>
</CodecProfiles>
<ResponseProfiles>
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
<Conditions />
</ResponseProfile>
<ResponseProfile container="ts,mpegts" type="Video" mimeType="video/mpeg">
<Conditions />
</ResponseProfile>
</ResponseProfiles>
<SubtitleProfiles>
<SubtitleProfile format="srt" method="Embed" />
<SubtitleProfile format="srt" method="External" />
</SubtitleProfiles>
</Profile>

View File

@@ -1,42 +1,27 @@
{ config, lib, pkgs, ... }: # how to update wikipedia snapshot:
# - browse for later snapshots:
# - <https://mirror.accum.se/mirror/wikimedia.org/other/kiwix/zim/wikipedia>
# - DL directly, or via rsync (resumable):
# - `rsync --progress --append-verify rsync://mirror.accum.se/mirror/wikimedia.org/other/kiwix/zim/wikipedia/wikipedia_en_all_maxi_2022-05.zim .`
{ ... }:
{ {
config = lib.mkIf (config.sane.maxBuildCost >= 3) { sane.persist.sys.byStore.ext = [
sane.services.kiwix-serve = { { user = "colin"; group = "users"; path = "/var/lib/kiwix"; method = "bind"; }
enable = true; ];
port = 8013;
zimPaths = with pkgs.zimPackages; [
alpinelinux_en_all_maxi.zimPath
archlinux_en_all_maxi.zimPath
bitcoin_en_all_maxi.zimPath
devdocs_en_nix.zimPath
gentoo_en_all_maxi.zimPath
# khanacademy_en_all.zimPath #< TODO: enable
openstreetmap-wiki_en_all_maxi.zimPath
psychonautwiki_en_all_maxi.zimPath
rationalwiki_en_all_maxi.zimPath
# wikipedia_en_100.zimPath
wikipedia_en_all_maxi.zimPath
# wikipedia_en_all_mini.zimPath
zimgit-food-preparation_en.zimPath
zimgit-medicine_en.zimPath
zimgit-post-disaster_en.zimPath
zimgit-water_en.zimPath
];
};
services.nginx.virtualHosts."w.uninsane.org" = { sane.services.kiwix-serve = {
forceSSL = true; enable = true;
enableACME = true; port = 8013;
# inherit kTLS; zimPaths = [ "/var/lib/kiwix/wikipedia_en_all_maxi_2023-11.zim" ];
locations."/" = {
proxyPass = "http://127.0.0.1:8013";
recommendedProxySettings = true;
};
locations."= /robots.txt".extraConfig = ''
return 200 "User-agent: *\nDisallow: /\n";
'';
};
sane.dns.zones."uninsane.org".inet.CNAME."w" = "native";
}; };
services.nginx.virtualHosts."w.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
locations."/".proxyPass = "http://127.0.0.1:8013";
};
sane.dns.zones."uninsane.org".inet.CNAME."w" = "native";
} }

View File

@@ -1,9 +1,8 @@
{ config, lib, ... }: { config, ... }:
let let
svc-cfg = config.services.komga; svc-cfg = config.services.komga;
inherit (svc-cfg) user group port stateDir; inherit (svc-cfg) user group port stateDir;
in in
lib.mkIf false #< 2024/09/30: disabled because i haven't used this for several months
{ {
sane.persist.sys.byStore.plaintext = [ sane.persist.sys.byStore.plaintext = [
{ inherit user group; mode = "0700"; path = stateDir; method = "bind"; } { inherit user group; mode = "0700"; path = stateDir; method = "bind"; }
@@ -13,15 +12,11 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used this for several
services.komga.port = 11319; # chosen at random services.komga.port = 11319; # chosen at random
services.nginx.virtualHosts."komga.uninsane.org" = { services.nginx.virtualHosts."komga.uninsane.org" = {
forceSSL = true; addSSL = true;
enableACME = true; enableACME = true;
locations."/" = { locations."/" = {
proxyPass = "http://127.0.0.1:${builtins.toString port}"; proxyPass = "http://127.0.0.1:${builtins.toString port}";
recommendedProxySettings = true;
}; };
locations."= /robots.txt".extraConfig = ''
return 200 "User-agent: *\nDisallow: /\n";
'';
}; };
sane.dns.zones."uninsane.org".inet.CNAME."komga" = "native"; sane.dns.zones."uninsane.org".inet.CNAME."komga" = "native";
} }

View File

@@ -5,175 +5,86 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
inherit (builtins) toString;
inherit (lib) mkForce;
uiPort = 1234; # default ui port is 1234 uiPort = 1234; # default ui port is 1234
backendPort = 8536; # default backend port is 8536 backendPort = 8536; # default backend port is 8536
#^ i guess the "backend" port is used for federation? #^ i guess the "backend" port is used for federation?
pict-rs = pkgs.pict-rs; pict-rs = pkgs.pict-rs;
# pict-rs = pkgs.pict-rs.overrideAttrs (upstream: {
# # as of v0.4.2, all non-GIF video is forcibly transcoded.
# # that breaks lemmy, because of the request latency.
# # and it eats up hella CPU.
# # pict-rs is iffy around video altogether: mp4 seems the best supported.
# # XXX: this patch no longer applies after 0.5.10 -> 0.5.11 update.
# # git log is hard to parse, but *suggests* that video is natively supported
# # better than in the 0.4.2 days, e.g. 5fd59fc5b42d31559120dc28bfef4e5002fb509e
# # "Change commandline flag to allow disabling video, since it is enabled by default"
# postPatch = (upstream.postPatch or "") + ''
# substituteInPlace src/validate.rs \
# --replace 'if transcode_options.needs_reencode() {' 'if false {'
# '';
# });
in {
services.lemmy = {
enable = true;
settings.hostname = "lemmy.uninsane.org";
# federation.debug forces outbound federation queries to be run synchronously
# N.B.: this option might not be read for 0.17.0+? <https://github.com/LemmyNet/lemmy/blob/c32585b03429f0f76d1e4ff738786321a0a9df98/RELEASES.md#upgrade-instructions>
# settings.federation.debug = true;
settings.port = backendPort;
ui.port = uiPort;
database.createLocally = true;
nginx.enable = true;
};
systemd.services.lemmy.serviceConfig = {
# fix to use a normal user so we can configure perms correctly
DynamicUser = mkForce false;
User = "lemmy";
Group = "lemmy";
};
systemd.services.lemmy.environment = {
RUST_BACKTRACE = "full";
# RUST_LOG = "debug";
# RUST_LOG = "trace";
# upstream defaults LEMMY_DATABASE_URL = "postgres:///lemmy?host=/run/postgresql";
# - Postgres complains that we didn't specify a user
# lemmy formats the url as:
# - postgres://{user}:{password}@{host}:{port}/{database}
# SO suggests (https://stackoverflow.com/questions/3582552/what-is-the-format-for-the-postgresql-connection-string-url):
# - postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
# LEMMY_DATABASE_URL = "postgres://lemmy@/run/postgresql"; # connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: FATAL: database "run/postgresql" does not exist
# LEMMY_DATABASE_URL = "postgres://lemmy?host=/run/postgresql"; # no PostgreSQL user name specified in startup packet
# LEMMY_DATABASE_URL = mkForce "postgres://lemmy@?host=/run/postgresql"; # WORKS
LEMMY_DATABASE_URL = mkForce "postgres://lemmy@/lemmy?host=/run/postgresql";
};
users.groups.lemmy = {};
users.users.lemmy = {
group = "lemmy";
isSystemUser = true;
};
services.nginx.virtualHosts."lemmy.uninsane.org" = {
forceSSL = true;
enableACME = true;
};
sane.dns.zones."uninsane.org".inet.CNAME."lemmy" = "native";
#v DO NOT REMOVE: defaults to 0.3, instead of latest, so always need to explicitly set this.
services.pict-rs.package = pict-rs;
# pict-rs configuration is applied in this order: # pict-rs configuration is applied in this order:
# - via toml # - via toml
# - via env vars (overrides everything above) # - via env vars (overrides everything above)
# - via CLI flags (overrides everything above) # - via CLI flags (overrides everything above)
# some of the CLI flags have defaults, making it the only actual way to configure certain things even when docs claim otherwise. # some of the CLI flags have defaults, making it the only actual way to configure certain things even when docs claim otherwise.
# CLI args: <https://git.asonix.dog/asonix/pict-rs#user-content-running> # CLI args: <https://git.asonix.dog/asonix/pict-rs#user-content-running>
# TOML args: <https://git.asonix.dog/asonix/pict-rs/src/branch/main/pict-rs.toml> systemd.services.pict-rs.serviceConfig.ExecStart = lib.mkForce (lib.concatStringsSep " " [
toml = pkgs.formats.toml { }; "${lib.getBin pict-rs}/bin/pict-rs run"
tomlConfig = toml.generate "pict-rs.toml" pictrsConfig; "--media-video-max-frame-count" (builtins.toString (30*60*60))
pictrsConfig = { "--media-process-timeout 120"
media.process_timeout = 120; "--media-video-allow-audio" # allow audio
media.video.allow_audio = true; ]);
media.video.max_frame_count = 30 * 60 * 60;
};
in {
config = lib.mkIf (config.sane.maxBuildCost >= 2) {
services.lemmy = {
enable = true;
settings.hostname = "lemmy.uninsane.org";
# federation.debug forces outbound federation queries to be run synchronously
# N.B.: this option might not be read for 0.17.0+? <https://github.com/LemmyNet/lemmy/blob/c32585b03429f0f76d1e4ff738786321a0a9df98/RELEASES.md#upgrade-instructions>
# settings.federation.debug = true;
settings.port = backendPort;
ui.port = uiPort;
database.createLocally = true;
nginx.enable = true;
};
systemd.services.lemmy.environment = {
RUST_BACKTRACE = "full";
RUST_LOG = "error";
# RUST_LOG = "warn";
# RUST_LOG = "debug";
# RUST_LOG = "trace";
# upstream defaults LEMMY_DATABASE_URL = "postgres:///lemmy?host=/run/postgresql";
# - Postgres complains that we didn't specify a user
# lemmy formats the url as:
# - postgres://{user}:{password}@{host}:{port}/{database}
# SO suggests (https://stackoverflow.com/questions/3582552/what-is-the-format-for-the-postgresql-connection-string-url):
# - postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
# LEMMY_DATABASE_URL = "postgres://lemmy@/run/postgresql"; # connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: FATAL: database "run/postgresql" does not exist
# LEMMY_DATABASE_URL = "postgres://lemmy?host=/run/postgresql"; # no PostgreSQL user name specified in startup packet
# LEMMY_DATABASE_URL = lib.mkForce "postgres://lemmy@?host=/run/postgresql"; # WORKS
LEMMY_DATABASE_URL = lib.mkForce "postgres://lemmy@/lemmy?host=/run/postgresql";
};
users.groups.lemmy = {};
users.users.lemmy = {
group = "lemmy";
isSystemUser = true;
};
services.nginx.virtualHosts."lemmy.uninsane.org" = {
forceSSL = true;
enableACME = true;
};
sane.dns.zones."uninsane.org".inet.CNAME."lemmy" = "native";
systemd.services.lemmy = {
# fix to use a normal user so we can configure perms correctly
# XXX(2024-07-28): this hasn't been rigorously tested:
# possible that i've set something too strict and won't notice right away
serviceConfig.DynamicUser = lib.mkForce false;
serviceConfig.User = "lemmy";
serviceConfig.Group = "lemmy";
# switch postgres from Requires -> Wants, so that postgres may restart without taking lemmy down with it.
requires = lib.mkForce [];
wants = [ "postgresql.service" ];
# hardening (systemd-analyze security lemmy)
# a handful of these are specified in upstream nixpkgs, but mostly not
serviceConfig.LockPersonality = true;
serviceConfig.NoNewPrivileges = true;
serviceConfig.MemoryDenyWriteExecute = true;
serviceConfig.PrivateDevices = true;
serviceConfig.PrivateMounts = true;
serviceConfig.PrivateTmp = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProcSubset = "pid";
serviceConfig.ProtectClock = true;
serviceConfig.ProtectControlGroups = true;
serviceConfig.ProtectHome = true;
serviceConfig.ProtectHostname = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectKernelModules = true;
serviceConfig.ProtectKernelTunables = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProtectSystem = "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
serviceConfig.RestrictNamespaces = true;
serviceConfig.RestrictSUIDSGID = true;
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.SystemCallFilter = [ "@system-service" ];
};
systemd.services.lemmy-ui = {
# hardening (systemd-analyze security lemmy-ui)
# TODO: upstream into nixpkgs
serviceConfig.LockPersonality = true;
serviceConfig.NoNewPrivileges = true;
# serviceConfig.MemoryDenyWriteExecute = true; #< it uses v8, JIT
serviceConfig.PrivateDevices = true;
serviceConfig.PrivateMounts = true;
serviceConfig.PrivateTmp = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProcSubset = "pid";
serviceConfig.ProtectClock = true;
serviceConfig.ProtectControlGroups = true;
serviceConfig.ProtectHome = true;
serviceConfig.ProtectHostname = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectKernelModules = true;
serviceConfig.ProtectKernelTunables = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProtectSystem = "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
serviceConfig.RestrictNamespaces = true;
serviceConfig.RestrictSUIDSGID = true;
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.SystemCallFilter = [ "@system-service" "@pkey" "@sandbox" ];
};
#v DO NOT REMOVE: defaults to 0.3, instead of latest, so always need to explicitly set this.
services.pict-rs.package = pict-rs;
systemd.services.pict-rs = {
serviceConfig.ExecStart = lib.mkForce (lib.concatStringsSep " " [
(lib.getExe pict-rs)
"--config-file"
tomlConfig
"run"
]);
# hardening (systemd-analyze security pict-rs)
# TODO: upstream into nixpkgs
serviceConfig.LockPersonality = true;
serviceConfig.NoNewPrivileges = true;
serviceConfig.MemoryDenyWriteExecute = true;
serviceConfig.PrivateDevices = true;
serviceConfig.PrivateMounts = true;
serviceConfig.PrivateTmp = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProcSubset = "pid";
serviceConfig.ProtectClock = true;
serviceConfig.ProtectControlGroups = true;
serviceConfig.ProtectHome = true;
serviceConfig.ProtectHostname = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectKernelModules = true;
serviceConfig.ProtectKernelTunables = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProtectSystem = "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
serviceConfig.RestrictNamespaces = true;
serviceConfig.RestrictSUIDSGID = true;
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.SystemCallFilter = [ "@system-service" ];
};
};
} }

View File

@@ -1,6 +1,6 @@
# docs: <https://nixos.wiki/wiki/Matrix> # docs: <https://nixos.wiki/wiki/Matrix>
# docs: <https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-synapse> # docs: <https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-synapse>
# example config: <https://github.com/element-hq/synapse/blob/develop/docs/sample_config.yaml> # example config: <https://github.com/matrix-org/synapse/blob/develop/docs/sample_config.yaml>
# #
# ENABLING PUSH NOTIFICATIONS (with UnifiedPush/ntfy): # ENABLING PUSH NOTIFICATIONS (with UnifiedPush/ntfy):
# - Matrix "pushers" API spec: <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset> # - Matrix "pushers" API spec: <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset>
@@ -12,9 +12,7 @@
# - delete a notification destination by setting `kind` to `null` (otherwise, request is identical to above) # - delete a notification destination by setting `kind` to `null` (otherwise, request is identical to above)
# #
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let
ntfy = config.services.ntfy-sh.enable;
in
{ {
imports = [ imports = [
./discord-puppet.nix ./discord-puppet.nix
@@ -22,17 +20,19 @@ in
./signal.nix ./signal.nix
]; ];
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/matrix-synapse"; method = "bind"; } { user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/matrix-synapse"; method = "bind"; }
]; ];
services.matrix-synapse.enable = true; services.matrix-synapse.enable = true;
services.matrix-synapse.log.root.level = "ERROR"; # accepts "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL" (?)
services.matrix-synapse.settings = { services.matrix-synapse.settings = {
# this changes the default log level from INFO to WARN.
# maybe there's an easier way?
log_config = ./synapse-log_level.yaml;
server_name = "uninsane.org"; server_name = "uninsane.org";
# services.matrix-synapse.enable_registration_captcha = true; # services.matrix-synapse.enable_registration_captcha = true;
# services.matrix-synapse.enable_registration_without_verification = true; # services.matrix-synapse.enable_registration_without_verification = true;
# enable_registration = true; enable_registration = true;
# services.matrix-synapse.registration_shared_secret = "<shared key goes here>"; # services.matrix-synapse.registration_shared_secret = "<shared key goes here>";
# default for listeners is port = 8448, tls = true, x_forwarded = false. # default for listeners is port = 8448, tls = true, x_forwarded = false.
@@ -70,30 +70,21 @@ in
config.sops.secrets."matrix_synapse_secrets.yaml".path config.sops.secrets."matrix_synapse_secrets.yaml".path
]; ];
# tune restart settings to ensure systemd doesn't disable it, and we don't overwhelm postgres systemd.services.matrix-synapse.postStart = ''
systemd.services.matrix-synapse.serviceConfig.RestartSec = 5; ACCESS_TOKEN=$(${pkgs.coreutils}/bin/cat ${config.sops.secrets.matrix_access_token.path})
systemd.services.matrix-synapse.serviceConfig.RestartMaxDelaySec = 20; TOPIC=$(${pkgs.coreutils}/bin/cat ${config.sops.secrets.ntfy-sh-topic.path})
systemd.services.matrix-synapse.serviceConfig.StartLimitBurst = 120;
systemd.services.matrix-synapse.serviceConfig.RestartSteps = 3;
# switch postgres from Requires -> Wants, so that postgres may restart without taking matrix down with it.
systemd.services.matrix-synapse.requires = lib.mkForce [];
systemd.services.matrix-synapse.wants = [ "postgresql.service" ];
systemd.services.matrix-synapse.postStart = lib.optionalString ntfy ''
ACCESS_TOKEN=$(${lib.getExe' pkgs.coreutils "cat"} ${config.sops.secrets.matrix_access_token.path})
TOPIC=$(${lib.getExe' pkgs.coreutils "cat"} ${config.sops.secrets.ntfy-sh-topic.path})
echo "ensuring ntfy push gateway" echo "ensuring ntfy push gateway"
${lib.getExe pkgs.curl} \ ${pkgs.curl}/bin/curl \
--header "Authorization: Bearer $ACCESS_TOKEN" \ --header "Authorization: Bearer $ACCESS_TOKEN" \
--data "{ \"app_display_name\": \"ntfy-adapter\", \"app_id\": \"ntfy.uninsane.org\", \"data\": { \"url\": \"https://ntfy.uninsane.org/_matrix/push/v1/notify\", \"format\": \"event_id_only\" }, \"device_display_name\": \"ntfy-adapter\", \"kind\": \"http\", \"lang\": \"en-US\", \"profile_tag\": \"\", \"pushkey\": \"$TOPIC\" }" \ --data "{ \"app_display_name\": \"ntfy-adapter\", \"app_id\": \"ntfy.uninsane.org\", \"data\": { \"url\": \"https://ntfy.uninsane.org/_matrix/push/v1/notify\", \"format\": \"event_id_only\" }, \"device_display_name\": \"ntfy-adapter\", \"kind\": \"http\", \"lang\": \"en-US\", \"profile_tag\": \"\", \"pushkey\": \"$TOPIC\" }" \
localhost:8008/_matrix/client/v3/pushers/set localhost:8008/_matrix/client/v3/pushers/set
echo "registered push gateways:" echo "registered push gateways:"
${lib.getExe pkgs.curl} \ ${pkgs.curl}/bin/curl \
--header "Authorization: Bearer $ACCESS_TOKEN" \ --header "Authorization: Bearer $ACCESS_TOKEN" \
localhost:8008/_matrix/client/v3/pushers \ localhost:8008/_matrix/client/v3/pushers \
| ${lib.getExe pkgs.jq} . | ${pkgs.jq}/bin/jq .
''; '';
@@ -123,7 +114,6 @@ in
locations."/" = { locations."/" = {
proxyPass = "http://127.0.0.1:8008"; proxyPass = "http://127.0.0.1:8008";
recommendedProxySettings = true;
extraConfig = '' extraConfig = ''
# allow uploading large files (matrix enforces a separate limit, downstream) # allow uploading large files (matrix enforces a separate limit, downstream)
client_max_body_size 512m; client_max_body_size 512m;
@@ -171,5 +161,5 @@ in
owner = config.users.users.matrix-synapse.name; owner = config.users.users.matrix-synapse.name;
}; };
# provide access to ntfy-sh-topic secret # provide access to ntfy-sh-topic secret
users.users.matrix-synapse.extraGroups = lib.optionals ntfy [ "ntfy-sh" ]; users.users.matrix-synapse.extraGroups = [ "ntfy-sh" ];
} }

View File

@@ -5,7 +5,7 @@
# - recommended to use mautrix-discord: <https://github.com/NixOS/nixpkgs/pull/200462> # - recommended to use mautrix-discord: <https://github.com/NixOS/nixpkgs/pull/200462>
lib.mkIf false lib.mkIf false
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/mx-puppet-discord"; method = "bind"; } { user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/mx-puppet-discord"; method = "bind"; }
]; ];

View File

@@ -1,13 +1,15 @@
# config docs: # config docs:
# - <https://github.com/matrix-org/matrix-appservice-irc/blob/develop/config.sample.yaml> # - <https://github.com/matrix-org/matrix-appservice-irc/blob/develop/config.sample.yaml>
{ lib, ... }: # probably want to remove that.
{ config, lib, ... }:
let let
ircServer = { name, additionalAddresses ? [], ssl ? true, sasl ? true, port ? if ssl then 6697 else 6667 }: let ircServer = { name, additionalAddresses ? [], sasl ? true, port ? 6697 }: let
lowerName = lib.toLower name; lowerName = lib.toLower name;
in { in {
# XXX sasl: appservice doesn't support NickServ identification (only SASL, or PASS if sasl = false) # XXX sasl: appservice doesn't support NickServ identification (only SASL, or PASS if sasl = false)
inherit additionalAddresses name port sasl ssl; inherit name additionalAddresses sasl port;
ssl = true;
botConfig = { botConfig = {
# bot has no presence in IRC channel; only real Matrix users # bot has no presence in IRC channel; only real Matrix users
enabled = false; enabled = false;
@@ -99,7 +101,7 @@ in
}) })
]; ];
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
# TODO: mode? # TODO: mode?
{ user = "matrix-appservice-irc"; group = "matrix-appservice-irc"; path = "/var/lib/matrix-appservice-irc"; method = "bind"; } { user = "matrix-appservice-irc"; group = "matrix-appservice-irc"; path = "/var/lib/matrix-appservice-irc"; method = "bind"; }
]; ];
@@ -127,8 +129,6 @@ in
}; };
ircService = { ircService = {
logging.level = "warn"; # "error", "warn", "info", "debug"
mediaProxy.publicUrl = "https://irc.matrix.uninsane.org/media";
servers = { servers = {
"irc.esper.net" = ircServer { "irc.esper.net" = ircServer {
name = "esper"; name = "esper";
@@ -154,21 +154,8 @@ in
# notable channels: # notable channels:
# - #sxmo # - #sxmo
# - #sxmo-offtopic # - #sxmo-offtopic
# supposedly also available at <irc://37lnq2veifl4kar7.onion:6667/> (unofficial)
}; };
"irc.rizon.net" = ircServer { name = "Rizon"; }; "irc.rizon.net" = ircServer { name = "Rizon"; };
# "irc.sdf.org" = ircServer {
# # XXX(2024-11-06): seems it can't connect. "matrix-appservice-irc: WARN:Provisioner Provisioner only handles text 'yes'/'y' (from BASHy2-EU on irc.sdf.org)"
# # use instead? <https://lemmy.sdf.org/c/sdfpubnix>
# name = "sdf";
# # sasl = false;
# # notable channels (see: <https://sdf.org/?tutorials/irc-channels>)
# # - #sdf
# };
"wigle.net" = ircServer {
name = "WiGLE";
ssl = false;
};
}; };
}; };
}; };
@@ -178,17 +165,4 @@ in
# the service actively uses at least one of these, and both of them are fairly innocuous # the service actively uses at least one of these, and both of them are fairly innocuous
SystemCallFilter = lib.mkForce "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @setuid @swap"; SystemCallFilter = lib.mkForce "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @setuid @swap";
}; };
services.nginx.virtualHosts."irc.matrix.uninsane.org" = {
forceSSL = true;
enableACME = true;
locations."/media" = {
proxyPass = "http://127.0.0.1:11111";
recommendedProxySettings = true;
};
};
sane.dns.zones."uninsane.org".inet = {
CNAME."irc.matrix" = "native";
};
} }

View File

@@ -4,7 +4,7 @@
lib.mkIf false # disabled 2024/01/11: i don't use it, and pkgs.mautrix-signal had some API changes lib.mkIf false # disabled 2024/01/11: i don't use it, and pkgs.mautrix-signal had some API changes
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
{ user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; method = "bind"; } { user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; method = "bind"; }
{ user = "signald"; group = "signald"; path = "/var/lib/signald"; method = "bind"; } { user = "signald"; group = "signald"; path = "/var/lib/signald"; method = "bind"; }
]; ];

View File

@@ -0,0 +1,27 @@
version: 1
# In systemd's journal, loglevel is implicitly stored, so let's omit it
# from the message text.
formatters:
journal_fmt:
format: '%(name)s: [%(request)s] %(message)s'
filters:
context:
(): synapse.util.logcontext.LoggingContextFilter
request: ""
handlers:
journal:
class: systemd.journal.JournalHandler
formatter: journal_fmt
filters: [context]
SYSLOG_IDENTIFIER: synapse
# default log level: INFO
root:
level: WARN
handlers: [journal]
disable_existing_loggers: False

View File

@@ -1,39 +0,0 @@
# - `man 5 minidlna.conf`
# - `man 8 minidlnad`
#
# this is an extremely simple (but limited) DLNA server:
# - no web UI
# - no runtime configuration -- just statically configure media directories instead
# - no transcoding
# compatibility:
# - LG TV: music: all working
# - LG TV: videos: mixed. i can't see the pattern; HEVC works; H.264 sometimes works.
{ lib, ... }:
lib.mkIf false #< XXX(2024-11-17): WORKS, but i'm trying gerbera instead for hopefully better transcoding
{
sane.ports.ports."1900" = {
protocol = [ "udp" ];
visibleTo.lan = true;
description = "colin-upnp-for-minidlna";
};
sane.ports.ports."8200" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
description = "colin-minidlna-http";
};
services.minidlna.enable = true;
services.minidlna.settings = {
media_dir = [
# A/V/P to restrict a directory to audio/video/pictures
"A,/var/media/Music"
"V,/var/media/Videos/Film"
# "V,/var/media/Videos/Milkbags"
"V,/var/media/Videos/Shows"
];
notify_interval = 60;
};
users.users.minidlna.extraGroups = [ "media" ];
}

View File

@@ -1,66 +0,0 @@
# murmur is the server component of mumble.
# - docs: <https://www.mumble.info/documentation/>
# - config docs: <https://www.mumble.info/documentation/administration/config-file/>
#
# default port is 64738 (UDP and TCP)
#
# FIRST-RUN:
# - login from mumble client as `SuperUser`, password taken from `journalctl -u murmur`.
# - login from another machine and right click on self -> 'Register'
# - as SuperUser, right click on server root -> edit
# - Groups tab: select "admin", then add the other registered user to the group.
# - log out as SuperUser and manage the server using that other user now.
#
# USAGE:
# - 'auth' group = any user who has registered a cert with the server.
{ ... }:
{
sane.persist.sys.byStore.private = [
{ user = "murmur"; group = "murmur"; mode = "0700"; path = "/var/lib/murmur"; method = "bind"; }
];
services.murmur.enable = true;
services.murmur.welcometext = "welcome to Colin's mumble voice chat server";
# max bandwidth (bps) **per user**. i believe this affects both voice and uploads?
# mumble defaults to 558000, but nixos service defaults to 72000.
services.murmur.bandwidth = 558000;
services.murmur.imgMsgLength = 8 * 1024 * 1024;
services.murmur.sslCert = "/var/lib/acme/mumble.uninsane.org/fullchain.pem";
services.murmur.sslKey = "/var/lib/acme/mumble.uninsane.org/key.pem";
services.murmur.sslCa = "/etc/ssl/certs/ca-bundle.crt";
# allow clients on the LAN to discover this server
services.murmur.bonjour = true;
# mumble has a public server listing.
# my server doesn't associate with that registry (unless i specify registerPassword).
# however these settings appear to affect how the server presents itself to clients, regardless of registration.
services.murmur.registerName = "mumble.uninsane.org";
services.murmur.registerUrl = "https://mumble.uninsane.org";
services.murmur.registerHostname = "mumble.uninsane.org";
# defaultchannel=ID makes it so that unauthenticated users are placed in some specific channel when they join
services.murmur.extraConfig = ''
defaultchannel=2
'';
users.users.murmur.extraGroups = [
"nginx" # provide access to certs
];
services.nginx.virtualHosts."mumble.uninsane.org" = {
# allow ACME to procure a cert via nginx for this domain
enableACME = true;
};
sane.dns.zones."uninsane.org".inet = {
CNAME."mumble" = "native";
};
sane.ports.ports."64738" = {
protocol = [ "tcp" "udp" ];
visibleTo.lan = true;
visibleTo.doof = true;
description = "colin-mumble";
};
}

View File

@@ -34,10 +34,7 @@ lib.mkIf false #< i don't actively use navidrome
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
# inherit kTLS; # inherit kTLS;
locations."/" = { locations."/".proxyPass = "http://127.0.0.1:4533";
proxyPass = "http://127.0.0.1:4533";
recommendedProxySettings = true;
};
}; };
sane.dns.zones."uninsane.org".inet.CNAME."music" = "native"; sane.dns.zones."uninsane.org".inet.CNAME."music" = "native";

View File

@@ -0,0 +1,213 @@
# docs: <https://nixos.wiki/wiki/Nginx>
# docs: <https://nginx.org/en/docs/>
{ config, lib, pkgs, ... }:
let
# make the logs for this host "public" so that they show up in e.g. metrics
publog = vhost: lib.attrsets.unionOfDisjoint vhost {
extraConfig = (vhost.extraConfig or "") + ''
access_log /var/log/nginx/public.log vcombined;
'';
};
# kTLS = true; # in-kernel TLS for better perf
in
{
sane.ports.ports."80" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
visibleTo.ovpn = true; # so that letsencrypt can procure a cert for the mx record
description = "colin-http-uninsane.org";
};
sane.ports.ports."443" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-https-uninsane.org";
};
services.nginx.enable = true;
services.nginx.appendConfig = ''
# use 1 process per core.
# may want to increase worker_connections too, but `ulimit -n` must be increased first.
worker_processes auto;
'';
# this is the standard `combined` log format, with the addition of $host
# so that we have the virtualHost in the log.
# KEEP IN SYNC WITH GOACCESS
# goaccess calls this VCOMBINED:
# - <https://gist.github.com/jyap808/10570005>
services.nginx.commonHttpConfig = ''
log_format vcombined '$host:$server_port $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referrer" "$http_user_agent"';
access_log /var/log/nginx/private.log vcombined;
'';
# sets gzip_comp_level = 5
services.nginx.recommendedGzipSettings = true;
# enables OCSP stapling (so clients don't need contact the OCSP server -- i do instead)
# - doesn't seem to, actually: <https://www.ssllabs.com/ssltest/analyze.html?d=uninsane.org>
# caches TLS sessions for 10m
services.nginx.recommendedTlsSettings = true;
# enables sendfile, tcp_nopush, tcp_nodelay, keepalive_timeout 65
services.nginx.recommendedOptimisation = true;
# web blog/personal site
# alternative way to link stuff into the share:
# sane.fs."/var/www/sites/uninsane.org/share/Ubunchu".mount.bind = "/var/media/Books/Visual/HiroshiSeo/Ubunchu";
# sane.fs."/var/media/Books/Visual/HiroshiSeo/Ubunchu".dir = {};
services.nginx.virtualHosts."uninsane.org" = publog {
# a lot of places hardcode https://uninsane.org,
# and then when we mix http + non-https, we get CORS violations
# and things don't look right. so force SSL.
forceSSL = true;
enableACME = true;
# inherit kTLS;
# for OCSP stapling
sslTrustedCertificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
locations."/" = {
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
tryFiles = "$uri $uri/ @fallback";
};
# unversioned files
locations."@fallback" = {
root = "/var/www/sites/uninsane.org";
};
# uninsane.org/share/foo => /var/www/sites/uninsane.org/share/foo.
# special-cased to enable directory listings
locations."/share" = {
root = "/var/www/sites/uninsane.org";
extraConfig = ''
# autoindex => render directory listings
autoindex on;
# don't follow any symlinks when serving files
# otherwise it allows a directory escape
disable_symlinks on;
'';
};
# allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org
locations."= /.well-known/matrix/server".extraConfig =
let
# use 443 instead of the default 8448 port to unite
# the client-server and server-server port for simplicity
server = { "m.server" = "matrix.uninsane.org:443"; };
in ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON server}';
'';
locations."= /.well-known/matrix/client".extraConfig =
let
client = {
"m.homeserver" = { "base_url" = "https://matrix.uninsane.org"; };
"m.identity_server" = { "base_url" = "https://vector.im"; };
};
# ACAO required to allow element-web on any URL to request this json file
in ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}';
'';
# static URLs might not be aware of .well-known (e.g. registration confirmation URLs),
# so hack around that.
locations."/_matrix" = {
proxyPass = "http://127.0.0.1:8008";
};
locations."/_synapse" = {
proxyPass = "http://127.0.0.1:8008";
};
# allow ActivityPub clients to discover how to reach @user@uninsane.org
# see: https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3361/
# not sure this makes sense while i run multiple AP services (pleroma, lemmy)
# locations."/.well-known/nodeinfo" = {
# proxyPass = "http://127.0.0.1:4000";
# extraConfig = pleromaExtraConfig;
# };
# redirect common feed URIs to the canonical feed
locations."= /atom".extraConfig = "return 301 /atom.xml;";
locations."= /feed".extraConfig = "return 301 /atom.xml;";
locations."= /feed.xml".extraConfig = "return 301 /atom.xml;";
locations."= /rss".extraConfig = "return 301 /atom.xml;";
locations."= /rss.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/atom".extraConfig = "return 301 /atom.xml;";
locations."= /blog/atom.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/feed".extraConfig = "return 301 /atom.xml;";
locations."= /blog/feed.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/rss".extraConfig = "return 301 /atom.xml;";
locations."= /blog/rss.xml".extraConfig = "return 301 /atom.xml;";
};
# serve any site not listed above, if it's static.
# because we define it dynamically, SSL isn't trivial. support only http
# documented <https://nginx.org/en/docs/http/ngx_http_core_module.html#server_name>
services.nginx.virtualHosts."~^(?<domain>.+)$" = {
default = true;
addSSL = true;
enableACME = false;
sslCertificate = "/var/www/certs/wildcard/cert.pem";
sslCertificateKey = "/var/www/certs/wildcard/key.pem";
# sslCertificate = "/var/lib/acme/.minica/cert.pem";
# sslCertificateKey = "/var/lib/acme/.minica/key.pem";
# serverName = null;
locations."/" = {
# somehow this doesn't escape -- i get error 400 if i:
# curl 'http://..' --resolve '..:80:127.0.0.1'
root = "/var/www/sites/$domain";
# tryFiles = "$domain/$uri $domain/$uri/ =404";
};
};
security.acme.acceptTerms = true;
security.acme.defaults.email = "admin.acme@uninsane.org";
sane.persist.sys.byStore.plaintext = [
{ user = "acme"; group = "acme"; path = "/var/lib/acme"; method = "bind"; }
{ user = "colin"; group = "users"; path = "/var/www/sites"; method = "bind"; }
];
# let's encrypt default chain looks like:
# - End-entity certificate ← R3 ← ISRG Root X1 ← DST Root CA X3
# - <https://community.letsencrypt.org/t/production-chain-changes/150739>
# DST Root CA X3 expired in 2021 (?)
# the alternative chain is:
# - End-entity certificate ← R3 ← ISRG Root X1 (self-signed)
# using this alternative chain grants more compatibility for services like ejabberd
# but might decrease compatibility with very old clients that don't get updates (e.g. old android, iphone <= 4).
# security.acme.defaults.extraLegoFlags = [
security.acme.certs."uninsane.org" = rec {
# ISRG Root X1 results in lets encrypt sending the same chain as default,
# just without the final ISRG Root X1 ← DST Root CA X3 link.
# i.e. we could alternative clip the last item and achieve the exact same thing.
extraLegoRunFlags = [
"--preferred-chain" "ISRG Root X1"
];
extraLegoRenewFlags = extraLegoRunFlags;
};
# TODO: alternatively, we could clip the last cert IF it's expired,
# optionally outputting that to a new cert file.
# security.acme.defaults.postRun = "";
# create a self-signed SSL certificate for use with literally any domain.
# browsers will reject this, but proxies and local testing tools can be configured
# to accept it.
system.activationScripts.generate-x509-self-signed.text = ''
mkdir -p /var/www/certs/wildcard
test -f /var/www/certs/wildcard/key.pem || ${pkgs.openssl}/bin/openssl \
req -x509 -newkey rsa:4096 \
-keyout /var/www/certs/wildcard/key.pem \
-out /var/www/certs/wildcard/cert.pem \
-sha256 -nodes -days 3650 \
-addext 'subjectAltName=DNS:*' \
-subj '/CN=self-signed'
chmod 640 /var/www/certs/wildcard/{key,cert}.pem
chown root:nginx /var/www/certs/wildcard /var/www/certs/wildcard/{key,cert}.pem
'';
}

View File

@@ -1,111 +0,0 @@
# docs: <https://nixos.wiki/wiki/Nginx>
# docs: <https://nginx.org/en/docs/>
{ lib, pkgs, ... }:
{
imports = [
./uninsane.org.nix
./waka.laka.osaka
];
sane.ports.ports."80" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.ovpns = true; # so that letsencrypt can procure a cert for the mx record
visibleTo.doof = true;
description = "colin-http-uninsane.org";
};
sane.ports.ports."443" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.doof = true;
description = "colin-https-uninsane.org";
};
services.nginx.enable = true;
users.users.nginx.extraGroups = [ "anubis" ];
# nginxStable is one release behind nginxMainline.
# nginx itself recommends running mainline; nixos defaults to stable.
# services.nginx.package = pkgs.nginxMainline;
# XXX(2024-07-31): nixos defaults to zlib-ng -- supposedly more performant, but spams log with
# "gzip filter failed to use preallocated memory: ..."
# XXX(2025-07-24): "gzip filter" spam is gone => use default nginx package
# services.nginx.package = pkgs.nginxMainline.override { zlib = pkgs.zlib; };
services.nginx.appendConfig = ''
# use 1 process per core.
# may want to increase worker_connections too, but `ulimit -n` must be increased first.
worker_processes auto;
'';
# this is the standard `combined` log format, with the addition of $host
# so that we have the virtualHost in the log.
# KEEP IN SYNC WITH GOACCESS
# goaccess calls this VCOMBINED:
# - <https://gist.github.com/jyap808/10570005>
services.nginx.commonHttpConfig = ''
log_format vcombined '$host:$server_port $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referrer" "$http_user_agent"';
access_log /var/log/nginx/private.log vcombined;
'';
# enables gzip and sets gzip_comp_level = 5
services.nginx.recommendedGzipSettings = true;
# enables zstd and sets zstd_comp_level = 9
# services.nginx.recommendedZstdSettings = true; #< XXX(2025-07-18): nginx zstd integration is unmaintained in NixOS
# enables OCSP stapling (so clients don't need contact the OCSP server -- i do instead)
# - doesn't seem to, actually: <https://www.ssllabs.com/ssltest/analyze.html?d=uninsane.org>
# caches TLS sessions for 10m
services.nginx.recommendedTlsSettings = true;
# enables sendfile, tcp_nopush, tcp_nodelay, keepalive_timeout 65
services.nginx.recommendedOptimisation = true;
# serve any site not otherwise declared, if it's static.
# because we define it dynamically, SSL isn't trivial. support only http
# documented <https://nginx.org/en/docs/http/ngx_http_core_module.html#server_name>
services.nginx.virtualHosts."~^(?<domain>.+)$" = {
default = true;
addSSL = true;
enableACME = false;
sslCertificate = "/var/www/certs/wildcard/cert.pem";
sslCertificateKey = "/var/www/certs/wildcard/key.pem";
# sslCertificate = "/var/lib/acme/.minica/cert.pem";
# sslCertificateKey = "/var/lib/acme/.minica/key.pem";
# serverName = null;
locations."/" = {
# somehow this doesn't escape -- i get error 400 if i:
# curl 'http://..' --resolve '..:80:127.0.0.1'
root = "/var/www/sites/$domain";
# tryFiles = "$domain/$uri $domain/$uri/ =404";
};
};
security.acme.acceptTerms = true;
security.acme.defaults.email = "admin.acme@uninsane.org";
sane.persist.sys.byStore.plaintext = [
{ user = "acme"; group = "acme"; path = "/var/lib/acme"; method = "bind"; }
];
sane.persist.sys.byStore.private = [
{ user = "colin"; group = "users"; path = "/var/www/sites"; method = "bind"; }
];
sane.persist.sys.byStore.ephemeral = [
# logs *could* be persisted to private storage, but then there's the issue of
# "what if servo boots, isn't unlocked, and the whole / tmpfs is consumed by logs"
{ user = "nginx"; group = "nginx"; path = "/var/log/nginx"; method = "bind"; }
];
# create a self-signed SSL certificate for use with literally any domain.
# browsers will reject this, but proxies and local testing tools can be configured
# to accept it.
system.activationScripts.generate-x509-self-signed.text = ''
mkdir -p /var/www/certs/wildcard
test -f /var/www/certs/wildcard/key.pem || ${lib.getExe pkgs.openssl} \
req -x509 -newkey rsa:4096 \
-keyout /var/www/certs/wildcard/key.pem \
-out /var/www/certs/wildcard/cert.pem \
-sha256 -nodes -days 3650 \
-addext 'subjectAltName=DNS:*' \
-subj '/CN=self-signed'
chmod 640 /var/www/certs/wildcard/{key,cert}.pem
chown root:nginx /var/www/certs/wildcard /var/www/certs/wildcard/{key,cert}.pem
'';
}

View File

@@ -1,132 +0,0 @@
{ pkgs, ... }:
{
# alternative way to link stuff into the share:
# sane.fs."/var/www/sites/uninsane.org/share/Ubunchu".mount.bind = "/var/media/Books/Visual/HiroshiSeo/Ubunchu";
# sane.fs."/var/media/Books/Visual/HiroshiSeo/Ubunchu".dir = {};
services.nginx.virtualHosts."uninsane.org" = {
# a lot of places hardcode https://uninsane.org,
# and then when we mix http + non-https, we get CORS violations
# and things don't look right. so force SSL.
forceSSL = true;
enableACME = true;
# extraConfig = ''
# # "public" log so requests show up in goaccess metrics
# access_log /var/log/nginx/public.log vcombined;
# '';
locations."/" = {
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
tryFiles = "$uri $uri/ @fallback";
};
# unversioned files
locations."@fallback" = {
root = "/var/www/sites/uninsane.org";
extraConfig = ''
# instruct Google to not index these pages.
# see: <https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#xrobotstag>
add_header X-Robots-Tag 'none, noindex, nofollow';
# best-effort attempt to block archive.org from archiving these pages.
# reply with 403: Forbidden
# User Agent is *probably* "archive.org_bot"; maybe used to be "ia_archiver"
# source: <https://archive.org/details/archive.org_bot>
# additional UAs: <https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker>
#
# validate with: `curl -H 'User-Agent: "bot;archive.org_bot;like: something else"' -v https://uninsane.org/dne`
if ($http_user_agent ~* "(?:\b)archive.org_bot(?:\b)") {
return 403;
}
if ($http_user_agent ~* "(?:\b)archive.org(?:\b)") {
return 403;
}
if ($http_user_agent ~* "(?:\b)ia_archiver(?:\b)") {
return 403;
}
'';
};
# uninsane.org/share/foo => /var/www/sites/uninsane.org/share/foo.
# special-cased to enable directory listings
locations."/share" = {
root = "/var/www/sites/uninsane.org";
extraConfig = ''
# autoindex => render directory listings
autoindex on;
# don't follow any symlinks when serving files
# otherwise it allows a directory escape
disable_symlinks on;
'';
};
locations."/share/Milkbags/" = {
alias = "/var/media/Videos/Milkbags/";
extraConfig = ''
# autoindex => render directory listings
autoindex on;
# don't follow any symlinks when serving files
# otherwise it allows a directory escape
disable_symlinks on;
'';
};
locations."/share/Ubunchu/" = {
alias = "/var/media/Books/Visual/HiroshiSeo/Ubunchu/";
extraConfig = ''
# autoindex => render directory listings
autoindex on;
# don't follow any symlinks when serving files
# otherwise it allows a directory escape
disable_symlinks on;
'';
};
# allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org
locations."= /.well-known/matrix/server".extraConfig =
let
# use 443 instead of the default 8448 port to unite
# the client-server and server-server port for simplicity
server = { "m.server" = "matrix.uninsane.org:443"; };
in ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON server}';
'';
locations."= /.well-known/matrix/client".extraConfig =
let
client = {
"m.homeserver" = { "base_url" = "https://matrix.uninsane.org"; };
"m.identity_server" = { "base_url" = "https://vector.im"; };
};
# ACAO required to allow element-web on any URL to request this json file
in ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}';
'';
# static URLs might not be aware of .well-known (e.g. registration confirmation URLs),
# so hack around that.
locations."/_matrix".extraConfig = "return 301 https://matrix.uninsane.org$request_uri;";
locations."/_synapse".extraConfig = "return 301 https://matrix.uninsane.org$request_uri;";
# allow ActivityPub clients to discover how to reach @user@uninsane.org
# see: https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3361/
# not sure this makes sense while i run multiple AP services (pleroma, lemmy)
# locations."/.well-known/nodeinfo" = {
# proxyPass = "http://127.0.0.1:4000";
# extraConfig = pleromaExtraConfig;
# };
# redirect common feed URIs to the canonical feed
locations."= /atom".extraConfig = "return 301 /atom.xml;";
locations."= /feed".extraConfig = "return 301 /atom.xml;";
locations."= /feed.xml".extraConfig = "return 301 /atom.xml;";
locations."= /rss".extraConfig = "return 301 /atom.xml;";
locations."= /rss.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/atom".extraConfig = "return 301 /atom.xml;";
locations."= /blog/atom.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/feed".extraConfig = "return 301 /atom.xml;";
locations."= /blog/feed.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/rss".extraConfig = "return 301 /atom.xml;";
locations."= /blog/rss.xml".extraConfig = "return 301 /atom.xml;";
};
}

View File

@@ -1,35 +0,0 @@
{ config, pkgs, ... }:
let
wakaLakaOsaka = pkgs.linkFarm "waka-laka-osaka" {
"index.html" = ./index.html;
"waka.laka.for.osaka.mp4" = pkgs.fetchurl {
# saved from: <https://www.youtube.com/watch?v=ehB_7bBKprY>
url = "https://uninsane.org/share/Milkbags/PG_Plays_Video_Games-Waka_Laka_For_Osaka_4K.mp4";
hash = "sha256-UW0qR4btX4pZ1bJp4Oxk20m3mvQGj9HweLKO27JBTFs=";
};
};
in
{
services.nginx.virtualHosts."laka.osaka" = {
addSSL = true;
enableACME = true;
locations."/" = {
# redirect everything to waka.laka.osaka
return = "301 https://waka.laka.osaka$request_uri";
};
};
services.nginx.virtualHosts."waka.laka.osaka" = {
addSSL = true;
enableACME = true;
locations."/" = {
root = wakaLakaOsaka;
};
};
sane.dns.zones."laka.osaka".inet = {
SOA."@" = config.sane.dns.zones."uninsane.org".inet.SOA."@";
A."@" = config.sane.dns.zones."uninsane.org".inet.A."@";
NS."@" = config.sane.dns.zones."uninsane.org".inet.NS."@";
CNAME."waka" = "native.uninsane.org.";
};
}

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width initial-scale=1" />
<meta name="description" content="Waka Laka (for Osaka)" />
<title>Waka Laka (for Osaka)</title>
<style>
html,body {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
* {
margin: 0px;
padding: 0px;
border: 0px;
}
.bg-image {
width: 100%;
height: 100%;
min-width: 100%;
min-height: 100%;
position: fixed;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
}
body {
background-color: #000000;
}
</style>
</head>
<body>
<!-- TODO: how to autoplay video _without_ it being muted? -->
<video class="bg-image" id="waka-video" width="1440" height="1080"
autoplay loop muted
onclick="document.getElementById('waka-video').muted = !document.getElementById('waka-video').muted;"
>
<!-- from https://www.youtube.com/watch?v=ehB_7bBKprY -->
<!-- original and more info at https://www.aquilinestudios.org/wakalaka.html -->
<source src="waka.laka.for.osaka.mp4" type="video/mp4">
</video>
</body>
</html>

View File

@@ -6,7 +6,7 @@ lib.optionalAttrs false # disabled until i can be sure it's not gonna OOM my se
description = "build a nixos image with all updated deps"; description = "build a nixos image with all updated deps";
path = with pkgs; [ coreutils git nix ]; path = with pkgs; [ coreutils git nix ];
script = '' script = ''
working=$(mktemp -d nixos-prebuild.XXXXXX --tmpdir) working=$(mktemp -d /tmp/nixos-prebuild.XXXXXX)
pushd "$working" pushd "$working"
git clone https://git.uninsane.org/colin/nix-files.git \ git clone https://git.uninsane.org/colin/nix-files.git \
&& cd nix-files \ && cd nix-files \

View File

@@ -0,0 +1,21 @@
{ config, ... }:
{
services.nginx.virtualHosts."nixcache.uninsane.org" = {
addSSL = true;
enableACME = true;
# inherit kTLS;
# serverAliases = [ "nixcache" ];
locations."/".extraConfig = ''
proxy_pass http://localhost:${toString config.services.nix-serve.port};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
sane.dns.zones."uninsane.org".inet.CNAME."nixcache" = "native";
sane.services.nixserve.enable = true;
sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path;
}

View File

@@ -1,12 +1,12 @@
# ntfy: UnifiedPush notification delivery system # ntfy: UnifiedPush notification delivery system
# - used to get push notifications out of Matrix and onto a Phone (iOS, Android, or a custom client) # - used to get push notifications out of Matrix and onto a Phone (iOS, Android, or a custom client)
{ config, lib, ... }: { config, ... }:
{ {
imports = [ imports = [
./ntfy-waiter.nix ./ntfy-waiter.nix
./ntfy-sh.nix ./ntfy-sh.nix
]; ];
sops.secrets."ntfy-sh-topic" = lib.mkIf config.services.ntfy-sh.enable { sops.secrets."ntfy-sh-topic" = {
mode = "0440"; mode = "0440";
owner = config.users.users.ntfy-sh.name; owner = config.users.users.ntfy-sh.name;
group = config.users.users.ntfy-sh.name; group = config.users.users.ntfy-sh.name;

View File

@@ -29,9 +29,8 @@ let
# at the IP layer, to enable e.g. wake-on-lan. # at the IP layer, to enable e.g. wake-on-lan.
altPort = 2587; altPort = 2587;
in in
lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several months
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
# not 100% necessary to persist this, but ntfy does keep a 12hr (by default) cache # not 100% necessary to persist this, but ntfy does keep a 12hr (by default) cache
# for pushing notifications to users who become offline. # for pushing notifications to users who become offline.
# ACLs also live here. # ACLs also live here.
@@ -47,7 +46,7 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several mon
# defaults to 45s. # defaults to 45s.
# note that the client may still do its own TCP-level keepalives, typically every 30s # note that the client may still do its own TCP-level keepalives, typically every 30s
keepalive-interval = "15m"; keepalive-interval = "15m";
log-level = "info"; # trace, debug, info (default), warn, error log-level = "trace"; # trace, debug, info (default), warn, error
auth-default-access = "deny-all"; auth-default-access = "deny-all";
}; };
systemd.services.ntfy-sh.serviceConfig.DynamicUser = lib.mkForce false; systemd.services.ntfy-sh.serviceConfig.DynamicUser = lib.mkForce false;
@@ -59,7 +58,7 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several mon
# note that this will fail upon first run, i.e. before ntfy has created its db. # note that this will fail upon first run, i.e. before ntfy has created its db.
# just restart the service. # just restart the service.
topic=$(cat ${config.sops.secrets.ntfy-sh-topic.path}) topic=$(cat ${config.sops.secrets.ntfy-sh-topic.path})
${lib.getExe' pkgs.ntfy-sh "ntfy"} access everyone "$topic" read-write ${pkgs.ntfy-sh}/bin/ntfy access everyone "$topic" read-write
''; '';
@@ -87,7 +86,7 @@ lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several mon
sane.ports.ports."${builtins.toString altPort}" = { sane.ports.ports."${builtins.toString altPort}" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.doof = true; visibleTo.wan = true;
description = "colin-ntfy.uninsane.org"; description = "colin-ntfy.uninsane.org";
}; };
} }

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#!nix-shell -i python3 -p ntfy-sh -p python3 #!nix-shell -i python3 -p "python3.withPackages (ps: [ ])" -p ntfy-sh
import argparse import argparse
import logging import logging

View File

@@ -14,7 +14,7 @@ let
silence = port - portLow; silence = port - portLow;
flags = lib.optional cfg.verbose "--verbose"; flags = lib.optional cfg.verbose "--verbose";
cli = [ cli = [
(lib.getExe cfg.package) "${cfg.package}/bin/ntfy-waiter"
"--port" "--port"
"${builtins.toString port}" "${builtins.toString port}"
"--silence" "--silence"
@@ -31,7 +31,7 @@ let
ExecStart = lib.concatStringsSep " " cli; ExecStart = lib.concatStringsSep " " cli;
}; };
after = [ "network.target" ]; after = [ "network.target" ];
wantedBy = [ "ntfy-sh.service" ]; wantedBy = [ "default.target" ];
}; };
}; };
in in
@@ -39,7 +39,7 @@ in
options = with lib; { options = with lib; {
sane.ntfy-waiter.enable = mkOption { sane.ntfy-waiter.enable = mkOption {
type = types.bool; type = types.bool;
default = config.services.ntfy-sh.enable; default = true;
}; };
sane.ntfy-waiter.verbose = mkOption { sane.ntfy-waiter.verbose = mkOption {
type = types.bool; type = types.bool;
@@ -47,7 +47,7 @@ in
}; };
sane.ntfy-waiter.package = mkOption { sane.ntfy-waiter.package = mkOption {
type = types.package; type = types.package;
default = pkgs.static-nix-shell.mkPython3 { default = pkgs.static-nix-shell.mkPython3Bin {
pname = "ntfy-waiter"; pname = "ntfy-waiter";
srcRoot = ./.; srcRoot = ./.;
pkgs = [ "ntfy-sh" ]; pkgs = [ "ntfy-sh" ];
@@ -62,8 +62,8 @@ in
sane.ports.ports = lib.mkMerge (lib.forEach portRange (port: { sane.ports.ports = lib.mkMerge (lib.forEach portRange (port: {
"${builtins.toString port}" = { "${builtins.toString port}" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-notification-waiter-${builtins.toString (port - portLow + 1)}-of-${builtins.toString numPorts}"; description = "colin-notification-waiter-${builtins.toString (port - portLow + 1)}-of-${builtins.toString numPorts}";
}; };
})); }));

View File

@@ -7,216 +7,206 @@
# to run it in a oci-container: <https://github.com/barrucadu/nixfiles/blob/master/services/pleroma.nix> # to run it in a oci-container: <https://github.com/barrucadu/nixfiles/blob/master/services/pleroma.nix>
# #
# admin frontend: <https://fed.uninsane.org/pleroma/admin> # admin frontend: <https://fed.uninsane.org/pleroma/admin>
{ config, lib, pkgs, ... }: { config, pkgs, ... }:
let let
logLevel = "warning"; logLevel = "warn";
# logLevel = "debug"; # logLevel = "debug";
in in
{ {
config = lib.mkIf (config.sane.maxBuildCost >= 2) { sane.persist.sys.byStore.plaintext = [
sane.persist.sys.byStore.private = [ { user = "pleroma"; group = "pleroma"; path = "/var/lib/pleroma"; method = "bind"; }
# contains media i've uploaded to the server ];
{ user = "pleroma"; group = "pleroma"; path = "/var/lib/pleroma"; method = "bind"; } services.pleroma.enable = true;
]; services.pleroma.secretConfigFile = config.sops.secrets.pleroma_secrets.path;
services.pleroma.enable = true; services.pleroma.configs = [
services.pleroma.secretConfigFile = config.sops.secrets.pleroma_secrets.path; ''
services.pleroma.configs = [ import Config
''
import Config
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
url: [host: "fed.uninsane.org", scheme: "https", port: 443], url: [host: "fed.uninsane.org", scheme: "https", port: 443],
http: [ip: {127, 0, 0, 1}, port: 4040] http: [ip: {127, 0, 0, 1}, port: 4000]
# secret_key_base: "{secrets.pleroma.secret_key_base}", # secret_key_base: "{secrets.pleroma.secret_key_base}",
# signing_salt: "{secrets.pleroma.signing_salt}" # signing_salt: "{secrets.pleroma.signing_salt}"
config :pleroma, :instance, config :pleroma, :instance,
name: "Perfectly Sane", name: "Perfectly Sane",
description: "Single-user Pleroma instance", description: "Single-user Pleroma instance",
email: "admin.pleroma@uninsane.org", email: "admin.pleroma@uninsane.org",
notify_email: "notify.pleroma@uninsane.org", notify_email: "notify.pleroma@uninsane.org",
limit: 5000, limit: 5000,
registrations_open: true, registrations_open: true,
account_approval_required: true, account_approval_required: true,
max_pinned_statuses: 5, max_pinned_statuses: 5,
external_user_synchronization: true external_user_synchronization: true
# docs: https://hexdocs.pm/swoosh/Swoosh.Adapters.Sendmail.html # docs: https://hexdocs.pm/swoosh/Swoosh.Adapters.Sendmail.html
# test mail config with sudo -u pleroma ./bin/pleroma_ctl email test --to someone@somewhere.net # test mail config with sudo -u pleroma ./bin/pleroma_ctl email test --to someone@somewhere.net
config :pleroma, Pleroma.Emails.Mailer, config :pleroma, Pleroma.Emails.Mailer,
enabled: true, enabled: true,
adapter: Swoosh.Adapters.Sendmail, adapter: Swoosh.Adapters.Sendmail,
cmd_path: "${lib.getExe' pkgs.postfix "sendmail"}" cmd_path: "${pkgs.postfix}/bin/sendmail"
config :pleroma, Pleroma.User, config :pleroma, Pleroma.User,
restricted_nicknames: [ "admin", "uninsane", "root" ] restricted_nicknames: [ "admin", "uninsane", "root" ]
config :pleroma, :media_proxy, config :pleroma, :media_proxy,
enabled: false, enabled: false,
redirect_on_failure: true redirect_on_failure: true
#base_url: "https://cache.pleroma.social" #base_url: "https://cache.pleroma.social"
# see for reference: # see for reference:
# - `force_custom_plan`: <https://docs.pleroma.social/backend/configuration/postgresql/#disable-generic-query-plans> # - `force_custom_plan`: <https://docs.pleroma.social/backend/configuration/postgresql/#disable-generic-query-plans>
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres, adapter: Ecto.Adapters.Postgres,
username: "pleroma", username: "pleroma",
database: "pleroma", database: "pleroma",
hostname: "localhost", hostname: "localhost",
pool_size: 10, pool_size: 10,
prepare: :named, prepare: :named,
parameters: [ parameters: [
plan_cache_mode: "force_custom_plan" plan_cache_mode: "force_custom_plan"
] ]
# XXX: prepare: :named is needed only for PG <= 12 # XXX: prepare: :named is needed only for PG <= 12
# prepare: :named, # prepare: :named,
# password: "{secrets.pleroma.db_password}", # password: "{secrets.pleroma.db_password}",
# Configure web push notifications # Configure web push notifications
config :web_push_encryption, :vapid_details, config :web_push_encryption, :vapid_details,
subject: "mailto:notify.pleroma@uninsane.org" subject: "mailto:notify.pleroma@uninsane.org"
# public_key: "{secrets.pleroma.vapid_public_key}", # public_key: "{secrets.pleroma.vapid_public_key}",
# private_key: "{secrets.pleroma.vapid_private_key}" # private_key: "{secrets.pleroma.vapid_private_key}"
# config :joken, default_signer: "{secrets.pleroma.joken_default_signer}" # config :joken, default_signer: "{secrets.pleroma.joken_default_signer}"
config :pleroma, :database, rum_enabled: false config :pleroma, :database, rum_enabled: false
config :pleroma, :instance, static_dir: "/var/lib/pleroma/instance/static" config :pleroma, :instance, static_dir: "/var/lib/pleroma/instance/static"
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
config :pleroma, configurable_from_database: false config :pleroma, configurable_from_database: false
# strip metadata from uploaded images # strip metadata from uploaded images
config :pleroma, Pleroma.Upload, filters: [Pleroma.Upload.Filter.Exiftool.StripLocation] config :pleroma, Pleroma.Upload, filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
# fix log spam: <https://git.pleroma.social/pleroma/pleroma/-/issues/1659> # TODO: GET /api/pleroma/captcha is broken
# specifically, remove LAN addresses from `reserved` # there was a nixpkgs PR to fix this around 2022/10 though.
config :pleroma, Pleroma.Web.Plugs.RemoteIp, config :pleroma, Pleroma.Captcha,
enabled: true, enabled: false,
reserved: ["127.0.0.0/8", "::1/128", "fc00::/7", "172.16.0.0/12"] method: Pleroma.Captcha.Native
# TODO: GET /api/pleroma/captcha is broken
# there was a nixpkgs PR to fix this around 2022/10 though.
config :pleroma, Pleroma.Captcha,
enabled: false,
method: Pleroma.Captcha.Native
# (enabled by colin) # (enabled by colin)
# Enable Strict-Transport-Security once SSL is working: # Enable Strict-Transport-Security once SSL is working:
config :pleroma, :http_security, config :pleroma, :http_security,
sts: true sts: true
# docs: https://docs.pleroma.social/backend/configuration/cheatsheet/#logger # docs: https://docs.pleroma.social/backend/configuration/cheatsheet/#logger
config :logger, config :logger,
backends: [{ExSyslogger, :ex_syslogger}] backends: [{ExSyslogger, :ex_syslogger}]
config :logger, :ex_syslogger, config :logger, :ex_syslogger,
level: :${logLevel} level: :${logLevel}
# policies => list of message rewriting facilities to be enabled # policies => list of message rewriting facilities to be enabled
# transparence => whether to publish these rules in node_info (and /about) # transparence => whether to publish these rules in node_info (and /about)
config :pleroma, :mrf, config :pleroma, :mrf,
policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy], policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy],
transparency: true transparency: true
# reject => { host, reason } # reject => { host, reason }
config :pleroma, :mrf_simple, config :pleroma, :mrf_simple,
reject: [ {"threads.net", "megacorp"}, {"*.threads.net", "megacorp"} ] reject: [ {"threads.net", "megacorp"}, {"*.threads.net", "megacorp"} ]
# reject: [ [host: "threads.net", reason: "megacorp"], [host: "*.threads.net", reason: "megacorp"] ] # reject: [ [host: "threads.net", reason: "megacorp"], [host: "*.threads.net", reason: "megacorp"] ]
# XXX colin: not sure if this actually _does_ anything # XXX colin: not sure if this actually _does_ anything
# better to steal emoji from other instances? # better to steal emoji from other instances?
# - <https://docs.pleroma.social/backend/configuration/cheatsheet/#mrf_steal_emoji> # - <https://docs.pleroma.social/backend/configuration/cheatsheet/#mrf_steal_emoji>
config :pleroma, :emoji, config :pleroma, :emoji,
shortcode_globs: ["/emoji/**/*.png"], shortcode_globs: ["/emoji/**/*.png"],
groups: [ groups: [
"Cirno": "/emoji/cirno/*.png", "Cirno": "/emoji/cirno/*.png",
"Kirby": "/emoji/kirby/*.png", "Kirby": "/emoji/kirby/*.png",
"Bun": "/emoji/bun/*.png", "Bun": "/emoji/bun/*.png",
"Yuru Camp": "/emoji/yuru_camp/*.png", "Yuru Camp": "/emoji/yuru_camp/*.png",
] ]
'' ''
]; ];
systemd.services.pleroma.path = [ systemd.services.pleroma.path = [
# something inside pleroma invokes `sh` w/o specifying it by path, so this is needed to allow pleroma to start # something inside pleroma invokes `sh` w/o specifying it by path, so this is needed to allow pleroma to start
pkgs.bash pkgs.bash
# used by Pleroma to strip geo tags from uploads # used by Pleroma to strip geo tags from uploads
pkgs.exiftool pkgs.exiftool
# config.sane.programs.exiftool.package #< XXX(2024-10-20): breaks image uploading # i saw some errors when pleroma was shutting down about it not being able to find `awk`. probably not critical
# i saw some errors when pleroma was shutting down about it not being able to find `awk`. probably not critical pkgs.gawk
# config.sane.programs.gawk.package # needed for email operations like password reset
# needed for email operations like password reset pkgs.postfix
pkgs.postfix ];
];
systemd.services.pleroma = { systemd.services.pleroma.serviceConfig = {
# postgres can be slow to service early requests, preventing pleroma from starting on the first try # postgres can be slow to service early requests, preventing pleroma from starting on the first try
serviceConfig.Restart = "on-failure"; Restart = "on-failure";
serviceConfig.RestartSec = "10s"; RestartSec = "10s";
};
# hardening (systemd-analyze security pleroma) # systemd.services.pleroma.serviceConfig = {
# XXX(2024-07-28): this hasn't been rigorously tested: # # required for sendmail. see https://git.pleroma.social/pleroma/pleroma/-/issues/2259
# possible that i've set something too strict and won't notice right away # NoNewPrivileges = lib.mkForce false;
# make sure to test: # PrivateTmp = lib.mkForce false;
# - image/media uploading # CapabilityBoundingSet = lib.mkForce "~";
serviceConfig.CapabilityBoundingSet = lib.mkForce [ "" "" ]; # nixos default is `~CAP_SYS_ADMIN` # };
serviceConfig.LockPersonality = true;
serviceConfig.NoNewPrivileges = true;
serviceConfig.MemoryDenyWriteExecute = true;
serviceConfig.PrivateDevices = lib.mkForce true; #< dunno why nixpkgs has this set false; it seems to work as true
serviceConfig.PrivateMounts = true;
serviceConfig.PrivateTmp = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProtectProc = "invisible"; # this is required to allow pleroma to send email.
serviceConfig.ProcSubset = "all"; #< needs /proc/sys/kernel/overflowuid for bwrap # raw `sendmail` works, but i think pleroma's passing it some funny flags or something, idk.
# hack to fix that.
users.users.pleroma.extraGroups = [ "postdrop" ];
serviceConfig.ProtectClock = true; # Pleroma server and web interface
serviceConfig.ProtectControlGroups = true; # TODO: enable publog?
serviceConfig.ProtectHome = true; services.nginx.virtualHosts."fed.uninsane.org" = {
serviceConfig.ProtectKernelModules = true; forceSSL = true; # pleroma redirects to https anyway
serviceConfig.ProtectSystem = lib.mkForce "strict"; enableACME = true;
serviceConfig.RemoveIPC = true; # inherit kTLS;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; locations."/" = {
proxyPass = "http://127.0.0.1:4000";
recommendedProxySettings = true;
# documented: https://git.pleroma.social/pleroma/pleroma/-/blob/develop/installation/pleroma.nginx
extraConfig = ''
# XXX colin: this block is in the nixos examples: i don't understand all of it
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
if ($request_method = OPTIONS) {
return 204;
}
serviceConfig.RestrictSUIDSGID = true; add_header X-XSS-Protection "1; mode=block";
serviceConfig.SystemCallArchitectures = "native"; add_header X-Permitted-Cross-Domain-Policies none;
serviceConfig.SystemCallFilter = [ "@system-service" "@mount" "@sandbox" ]; #< "sandbox" might not actually be necessary add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy same-origin;
add_header X-Download-Options noopen;
serviceConfig.ProtectHostname = false; #< else brap can't mount /proc # proxy_http_version 1.1;
serviceConfig.ProtectKernelLogs = false; #< else breaks exiftool ("bwrap: Can't mount proc on /newroot/proc: Operation not permitted") # proxy_set_header Upgrade $http_upgrade;
serviceConfig.ProtectKernelTunables = false; #< else breaks exiftool # proxy_set_header Connection "upgrade";
serviceConfig.RestrictNamespaces = false; # media uploads require bwrap # # proxy_set_header Host $http_host;
}; # proxy_set_header Host $host;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# this is required to allow pleroma to send email. # colin: added this due to Pleroma complaining in its logs
# raw `sendmail` works, but i think pleroma's passing it some funny flags or something, idk. # proxy_set_header X-Real-IP $remote_addr;
# hack to fix that. # proxy_set_header X-Forwarded-Proto $scheme;
users.users.pleroma.extraGroups = [ "postdrop" ];
# Pleroma server and web interface # NB: this defines the maximum upload size
# TODO: enable publog? client_max_body_size 16m;
services.nginx.virtualHosts."fed.uninsane.org" = { '';
forceSSL = true; # pleroma redirects to https anyway
enableACME = true;
# inherit kTLS;
locations."/" = {
proxyPass = "http://127.0.0.1:4040";
recommendedProxySettings = true;
# documented: https://git.pleroma.social/pleroma/pleroma/-/blob/develop/installation/pleroma.nginx
extraConfig = ''
# client_max_body_size defines the maximum upload size
client_max_body_size 16m;
'';
};
};
sane.dns.zones."uninsane.org".inet.CNAME."fed" = "native";
sops.secrets."pleroma_secrets" = {
owner = config.users.users.pleroma.name;
}; };
}; };
sane.dns.zones."uninsane.org".inet.CNAME."fed" = "native";
sops.secrets."pleroma_secrets" = {
owner = config.users.users.pleroma.name;
};
} }

View File

@@ -1,4 +1,4 @@
{ lib, pkgs, ... }: { pkgs, ... }:
let let
GiB = n: MiB 1024*n; GiB = n: MiB 1024*n;
@@ -6,9 +6,9 @@ let
KiB = n: 1024*n; KiB = n: 1024*n;
in in
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
{ user = "postgres"; group = "postgres"; mode = "0750"; path = "/var/lib/postgresql"; method = "bind"; } # TODO: mode?
{ user = "postgres"; group = "postgres"; mode = "0750"; path = "/var/backup/postgresql"; method = "bind"; } { user = "postgres"; group = "postgres"; path = "/var/lib/postgresql"; method = "bind"; }
]; ];
services.postgresql.enable = true; services.postgresql.enable = true;
@@ -29,10 +29,9 @@ in
# - as `sudo su postgres`: # - as `sudo su postgres`:
# - `cd /var/lib/postgreql` # - `cd /var/lib/postgreql`
# - `psql -f state.sql` # - `psql -f state.sql`
# (for a compressed dump: `gunzip --stdout state.sql.gz | psql`)
# - restart dependent services (maybe test one at a time) # - restart dependent services (maybe test one at a time)
services.postgresql.package = pkgs.postgresql_16; services.postgresql.package = pkgs.postgresql_15;
# XXX colin: for a proper deploy, we'd want to include something for Pleroma here too. # XXX colin: for a proper deploy, we'd want to include something for Pleroma here too.
@@ -45,46 +44,34 @@ in
# LC_CTYPE = "C"; # LC_CTYPE = "C";
# ''; # '';
# perf tuning
# - for recommended values see: <https://pgtune.leopard.in.ua/>
# - for official docs (sparse), see: <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
services.postgresql.settings = { services.postgresql.settings = {
# perf tuning # DB Version: 15
# - for recommended values see: <https://pgtune.leopard.in.ua/>
# - for official docs (sparse), see: <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
# DB Version: 16
# OS Type: linux # OS Type: linux
# DB Type: web # DB Type: web
# vvv artificially constrained because the server's resources are shared across maaany services # Total Memory (RAM): 32 GB
# Total Memory (RAM): 12 GB
# CPUs num: 12 # CPUs num: 12
# Data Storage: ssd # Data Storage: ssd
max_connections = 200; max_connections = 200;
shared_buffers = "3GB"; shared_buffers = "8GB";
effective_cache_size = "9GB"; effective_cache_size = "24GB";
maintenance_work_mem = "768MB"; maintenance_work_mem = "2GB";
checkpoint_completion_target = 0.9; checkpoint_completion_target = 0.9;
wal_buffers = "16MB"; wal_buffers = "16MB";
default_statistics_target = 100; default_statistics_target = 100;
random_page_cost = 1.1; random_page_cost = 1.1;
effective_io_concurrency = 200; effective_io_concurrency = 200;
work_mem = "3932kB"; work_mem = "10485kB";
min_wal_size = "1GB"; min_wal_size = "1GB";
max_wal_size = "4GB"; max_wal_size = "4GB";
max_worker_processes = 12; max_worker_processes = 12;
max_parallel_workers_per_gather = 4; max_parallel_workers_per_gather = 4;
max_parallel_workers = 12; max_parallel_workers = 12;
max_parallel_maintenance_workers = 4; max_parallel_maintenance_workers = 4;
# DEBUG OPTIONS:
log_min_messages = "DEBUG1";
}; };
# regulate the restarts, so that systemd never disables it
systemd.services.postgresql.serviceConfig.Restart = lib.mkForce "on-failure";
systemd.services.postgresql.serviceConfig.RestartSec = 2;
systemd.services.postgresql.serviceConfig.RestartMaxDelaySec = 10;
systemd.services.postgresql.serviceConfig.RestartSteps = 4;
systemd.services.postgresql.serviceConfig.StartLimitBurst = 120;
# systemd.services.postgresql.serviceConfig.TimeoutStartSec = "14400s"; #< 14400 = 4 hours; recoveries are long
# daily backups to /var/backup # daily backups to /var/backup
services.postgresqlBackup.enable = true; services.postgresqlBackup.enable = true;

View File

@@ -1,81 +0,0 @@
#!/bin/sh
# source: <https://gist.githubusercontent.com/troykelly/616df024050dd50744dde4a9579e152e/raw/fe84e53cedf0caa6903604894454629a15867439/reindex_and_refresh_collation.sh>
#
# run this whenever postgres complains like:
# > WARNING: database "gitea" has a collation version mismatch
# > DETAIL: The database was created using collation version 2.39, but the operating system provides version 2.40.
# > HINT: Rebuild all objects in this database that use the default collation and run ALTER DATABASE gitea REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
#
# this script checks which databases are in need of a collation update,
# and re-collates them as appropriate.
# invoking this script should have low perf impact in the non-upgrade case,
# so safe to do this as a cron job.
#
# invoke as postgres user
log_info() {
>&2 echo "$@"
}
list_databases() {
log_info "Retrieving list of databases from the PostgreSQL server..."
psql --dbname="postgres" -Atc \
"SELECT datname FROM pg_database WHERE datistemplate = false"
}
refresh_collation_version() {
local db=$1
log_info "Refreshing collation version for database: $db..."
psql --dbname="$db" -c \
"ALTER DATABASE \"$db\" REFRESH COLLATION VERSION;"
}
check_collation_mismatches() {
local error=
log_info "Checking for collation mismatches in all databases..."
# Loop through each database and check for mismatching collations in table columns.
while IFS= read -r db; do
if [ -n "$db" ]; then
log_info "Checking database: $db for collation mismatches..."
local mismatches=$(psql --dbname="$db" -Atc \
"SELECT 'Mismatch in table ' || table_name || ' column ' || column_name || ' with collation ' || collation_name
FROM information_schema.columns
WHERE collation_name IS NOT NULL AND collation_name <> 'default' AND table_schema = 'public'
EXCEPT
SELECT 'No mismatch - default collation of ' || datcollate || ' used.'
FROM pg_database WHERE datname = '$db';"
)
if [ -z "$mismatches" ]; then
log_info "No collation mismatches found in database: $db"
else
# Print an informational message to stderr.
log_info "Collation mismatches found in database: $db:"
log_info "$mismatches"
error=1
fi
fi
done
if [ -n "$error" ]; then
exit 1
fi
}
log_info "Starting the reindexing and collation refresh process for all databases..."
databases=$(list_databases)
if [ -z "$databases" ]; then
log_info "No databases found for reindexing or collation refresh. Please check connection details to PostgreSQL server."
exit 1
fi
for db in $databases; do
refresh_collation_version "$db"
done
# Checking for collation mismatches after reindexing and collation refresh.
# Pass the list of databases to the check_collation_mismatches function through stdin.
echo "$databases" | check_collation_mismatches
log_info "Reindexing and collation refresh process completed."

View File

@@ -49,62 +49,60 @@
# - disable or fix bosh (jabber over http): # - disable or fix bosh (jabber over http):
# - "certmanager: No certificate/key found for client_https port 0" # - "certmanager: No certificate/key found for client_https port 0"
{ config, lib, pkgs, ... }: { lib, pkgs, ... }:
let let
# enables very verbose logging # enables very verbose logging
enableDebug = false; enableDebug = false;
in in
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
# TODO: mode?
{ user = "prosody"; group = "prosody"; path = "/var/lib/prosody"; method = "bind"; } { user = "prosody"; group = "prosody"; path = "/var/lib/prosody"; method = "bind"; }
]; ];
sane.ports.ports."5000" = { sane.ports.ports."5000" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-prosody-fileshare-proxy65"; description = "colin-xmpp-prosody-fileshare-proxy65";
}; };
sane.ports.ports."5222" = { sane.ports.ports."5222" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-client-to-server"; description = "colin-xmpp-client-to-server";
}; };
sane.ports.ports."5223" = { sane.ports.ports."5223" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpps-client-to-server"; # XMPP over TLS description = "colin-xmpps-client-to-server"; # XMPP over TLS
}; };
sane.ports.ports."5269" = { sane.ports.ports."5269" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true; visibleTo.wan = true;
description = "colin-xmpp-server-to-server"; description = "colin-xmpp-server-to-server";
}; };
sane.ports.ports."5270" = { sane.ports.ports."5270" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true; visibleTo.wan = true;
description = "colin-xmpps-server-to-server"; # XMPP over TLS description = "colin-xmpps-server-to-server"; # XMPP over TLS
}; };
sane.ports.ports."5280" = { sane.ports.ports."5280" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-bosh"; description = "colin-xmpp-bosh";
}; };
sane.ports.ports."5281" = { sane.ports.ports."5281" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.doof = true;
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-prosody-https"; # necessary? description = "colin-xmpp-prosody-https"; # necessary?
}; };
users.users.prosody.extraGroups = [ users.users.prosody.extraGroups = [
"nginx" # provide access to certs "nginx" # provide access to certs
"ntfy-sh" # access to secret ntfy topic "ntfy-sh" # access to secret ntfy topic
"turnserver" # to access the coturn shared secret
]; ];
security.acme.certs."uninsane.org".extraDomainNames = [ security.acme.certs."uninsane.org".extraDomainNames = [
@@ -150,8 +148,14 @@ in
# pointing it to /var/lib/acme doesn't quite work because it expects the private key # pointing it to /var/lib/acme doesn't quite work because it expects the private key
# to be named `privkey.pem` instead of acme's `key.pem` # to be named `privkey.pem` instead of acme's `key.pem`
# <https://prosody.im/doc/certificates#automatic_location> # <https://prosody.im/doc/certificates#automatic_location>
environment.etc."prosody/certs/uninsane.org/fullchain.pem".source = "/var/lib/acme/uninsane.org/fullchain.pem"; sane.fs."/etc/prosody/certs/uninsane.org/fullchain.pem" = {
environment.etc."prosody/certs/uninsane.org/privkey.pem".source = "/var/lib/acme/uninsane.org/key.pem"; symlink.target = "/var/lib/acme/uninsane.org/fullchain.pem";
wantedBeforeBy = [ "prosody.service" ];
};
sane.fs."/etc/prosody/certs/uninsane.org/privkey.pem" = {
symlink.target = "/var/lib/acme/uninsane.org/key.pem";
wantedBeforeBy = [ "prosody.service" ];
};
services.prosody = { services.prosody = {
enable = true; enable = true;
@@ -173,7 +177,7 @@ in
domain = "conference.xmpp.uninsane.org"; domain = "conference.xmpp.uninsane.org";
} }
]; ];
httpFileShare.domain = "upload.xmpp.uninsane.org"; uploadHttp.domain = "upload.xmpp.uninsane.org";
virtualHosts = { virtualHosts = {
# "Prosody requires at least one enabled VirtualHost to function. You can # "Prosody requires at least one enabled VirtualHost to function. You can
@@ -237,7 +241,6 @@ in
# legacy coturn integration # legacy coturn integration
# see: <https://modules.prosody.im/mod_turncredentials.html> # see: <https://modules.prosody.im/mod_turncredentials.html>
# "turncredentials" # "turncredentials"
] ++ lib.optionals config.services.ntfy-sh.enable [
"sane_ntfy" "sane_ntfy"
] ++ lib.optionals enableDebug [ ] ++ lib.optionals enableDebug [
"stanza_debug" #< logs EVERY stanza as debug: <https://prosody.im/doc/modules/mod_stanza_debug> "stanza_debug" #< logs EVERY stanza as debug: <https://prosody.im/doc/modules/mod_stanza_debug>
@@ -269,35 +272,18 @@ in
s2s_direct_tls_ports = { 5270 } s2s_direct_tls_ports = { 5270 }
turn_external_host = "turn.uninsane.org" turn_external_host = "turn.uninsane.org"
turn_external_secret = readAll("/run/secrets/coturn_shared_secret") turn_external_secret = readAll("/var/lib/coturn/shared_secret.bin")
-- turn_external_user = "prosody" -- turn_external_user = "prosody"
-- legacy mod_turncredentials integration -- legacy mod_turncredentials integration
-- turncredentials_host = "turn.uninsane.org" -- turncredentials_host = "turn.uninsane.org"
-- turncredentials_secret = readAll("/run/secrets/coturn_shared_secret") -- turncredentials_secret = readAll("/var/lib/coturn/shared_secret.bin")
ntfy_binary = "${pkgs.ntfy-sh}/bin/ntfy"
ntfy_topic = readAll("/run/secrets/ntfy-sh-topic")
-- s2s_require_encryption = true -- s2s_require_encryption = true
-- c2s_require_encryption = true -- c2s_require_encryption = true
'' + lib.optionalString config.services.ntfy-sh.enable ''
ntfy_binary = "${lib.getExe' pkgs.ntfy-sh "ntfy"}"
ntfy_topic = readAll("/run/secrets/ntfy-sh-topic")
''; '';
checkConfig = false; # secrets aren't available at build time
};
systemd.services.prosody = {
# hardening (systemd-analyze security prosody)
serviceConfig.LockPersonality = true;
serviceConfig.NoNewPrivileges = true;
serviceConfig.PrivateUsers = true;
serviceConfig.ProcSubset = "pid";
serviceConfig.ProtectClock = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectProc = "invisible";
serviceConfig.ProtectSystem = "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
}; };
} }

View File

@@ -7,12 +7,9 @@
# debugging: # debugging:
# - soulseek is just *flaky*. if you see e.g. DNS errors, even though you can't replicate them via `dig` or `getent ahostsv4`, just give it 10 minutes to work out: # - soulseek is just *flaky*. if you see e.g. DNS errors, even though you can't replicate them via `dig` or `getent ahostsv4`, just give it 10 minutes to work out:
# - "Soulseek.AddressException: Failed to resolve address 'vps.slsknet.org': Resource temporarily unavailable" # - "Soulseek.AddressException: Failed to resolve address 'vps.slsknet.org': Resource temporarily unavailable"
{ config, lib, pkgs, ... }: { config, lib, ... }:
{ {
sane.persist.sys.byStore.ephemeral = [ sane.persist.sys.byStore.plaintext = [
# {data,downloads,incomplete,logs}: contains logs, search history, and downloads
# so, move the downloaded data to persistent storage regularly, or configure the downloads/incomplete dirs to point to persisted storage (in nixpkgs slskd config)
{ user = "slskd"; group = "media"; path = "/var/lib/slskd"; method = "bind"; } { user = "slskd"; group = "media"; path = "/var/lib/slskd"; method = "bind"; }
]; ];
sops.secrets."slskd_env" = { sops.secrets."slskd_env" = {
@@ -24,7 +21,8 @@
sane.ports.ports."50300" = { sane.ports.ports."50300" = {
protocol = [ "tcp" ]; protocol = [ "tcp" ];
# visibleTo.ovpns = true; #< not needed: it runs in the ovpns namespace # not visible to WAN: i run this in a separate netns
visibleTo.ovpn = true;
description = "colin-soulseek"; description = "colin-soulseek";
}; };
@@ -34,9 +32,8 @@
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
locations."/" = { locations."/" = {
proxyPass = "http://${config.sane.netns.ovpns.veth.netns.ipv4}:5030"; proxyPass = "http://10.0.1.6:5030";
proxyWebsockets = true; proxyWebsockets = true;
recommendedProxySettings = true;
}; };
}; };
@@ -72,22 +69,11 @@
}; };
systemd.services.slskd = { systemd.services.slskd = {
# run this behind the OVPN static VPN serviceConfig = {
serviceConfig.NetworkNamespacePath = "/run/netns/ovpns"; # run this behind the OVPN static VPN
serviceConfig.ExecStartPre = [ NetworkNamespacePath = "/run/netns/ovpns";
# abort if public IP is not as expected Restart = lib.mkForce "always"; # exits "success" when it fails to connect to soulseek server
"${lib.getExe pkgs.sane-scripts.ip-check} --no-upnp --expect ${config.sane.netns.ovpns.wg.address.ipv4}" RestartSec = "60s";
]; };
serviceConfig.Restart = lib.mkForce "always"; # exits "success" when it fails to connect to soulseek server
serviceConfig.RestartSec = "60s";
# hardening (systemd-analyze security slskd)
# upstream nixpkgs specifies moderate defaults; these are supplementary
# serviceConfig.MemoryDenyWriteExecute = true;
# serviceConfig.ProcSubset = "pid";
# serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
# serviceConfig.SystemCallArchitectures = "native";
# serviceConfig.SystemCallFilter = [ "@system-service" ];
}; };
} }

View File

@@ -22,23 +22,46 @@ let
--replace-fail 'set(TR_USER_AGENT_PREFIX "''${TR_SEMVER}")' 'set(TR_USER_AGENT_PREFIX "3.00")' --replace-fail 'set(TR_USER_AGENT_PREFIX "''${TR_SEMVER}")' 'set(TR_USER_AGENT_PREFIX "3.00")'
''; '';
}); });
download-dir = "/var/media/torrents"; #< keep in sync with consts embedded in `torrent-done` download-dir = "/var/media/torrents";
torrent-done = pkgs.static-nix-shell.mkBash { torrent-done = pkgs.writeShellApplication {
pname = "torrent-done"; name = "torrent-done";
srcRoot = ./.; runtimeInputs = with pkgs; [
pkgs = [ rsync
"acl" util-linux
"coreutils"
"findutils"
"rsync"
]; ];
text = ''
destructive() {
if [ -n "''${TR_DRY_RUN-}" ]; then
echo "$*"
else
"$@"
fi
}
if [[ "$TR_TORRENT_DIR" =~ ^.*freeleech.*$ ]]; then
# freeleech torrents have no place in my permanent library
echo "freeleech: nothing to do"
exit 0
fi
if ! [[ "$TR_TORRENT_DIR" =~ ^${download-dir}/.*$ ]]; then
echo "unexpected torrent dir, aborting: $TR_TORRENT_DIR"
exit 0
fi
REL_DIR="''${TR_TORRENT_DIR#${download-dir}/}"
MEDIA_DIR="/var/media/$REL_DIR"
destructive mkdir -p "$(dirname "$MEDIA_DIR")"
destructive rsync -arv "$TR_TORRENT_DIR/" "$MEDIA_DIR/"
# dedupe the whole media library.
# yeah, a bit excessive: move this to a cron job if that's problematic.
destructive hardlink /var/media --reflink=always --ignore-time --verbose
'';
}; };
in in
{ {
sane.persist.sys.byStore.private = [ sane.persist.sys.byStore.plaintext = [
# TODO: mode? we need this specifically for the stats tracking in .config/ # TODO: mode? we need this specifically for the stats tracking in .config/
{ user = "transmission"; group = config.users.users.transmission.group; path = "/var/lib/transmission"; method = "bind"; } { user = "transmission"; group = config.users.users.transmission.group; path = "/var/lib/transmission"; method = "bind"; }
{ user = "transmission"; group = config.users.users.transmission.group; path = "/var/backup/torrents"; method = "bind"; }
]; ];
users.users.transmission.extraGroups = [ "media" ]; users.users.transmission.extraGroups = [ "media" ];
@@ -58,8 +81,8 @@ in
# DOCUMENTATION/options list: <https://github.com/transmission/transmission/blob/main/docs/Editing-Configuration-Files.md#options> # DOCUMENTATION/options list: <https://github.com/transmission/transmission/blob/main/docs/Editing-Configuration-Files.md#options>
# message-level = 3; #< enable for debug logging. 0-3, default is 2. # message-level = 3; #< enable for debug logging. 0-3, default is 2.
# ovpns.veth.netns.ipv4 => allow rpc only from the root servo ns. it'll tunnel things to the net, if need be. # 0.0.0.0 => allow rpc from any host: we gate it via firewall and auth requirement
rpc-bind-address = config.sane.netns.ovpns.veth.netns.ipv4; rpc-bind-address = "0.0.0.0";
#rpc-host-whitelist = "bt.uninsane.org"; #rpc-host-whitelist = "bt.uninsane.org";
#rpc-whitelist = "*.*.*.*"; #rpc-whitelist = "*.*.*.*";
rpc-authentication-required = true; rpc-authentication-required = true;
@@ -69,10 +92,6 @@ in
rpc-password = "{503fc8928344f495efb8e1f955111ca5c862ce0656SzQnQ5"; rpc-password = "{503fc8928344f495efb8e1f955111ca5c862ce0656SzQnQ5";
rpc-whitelist-enabled = false; rpc-whitelist-enabled = false;
# force behind ovpns in case the NetworkNamespace fails somehow
bind-address-ipv4 = config.sane.netns.ovpns.wg.address.ipv4;
port-forwarding-enabled = false;
# hopefully, make the downloads world-readable # hopefully, make the downloads world-readable
# umask = 0; #< default is 2: i.e. deny writes from world # umask = 0; #< default is 2: i.e. deny writes from world
@@ -104,42 +123,24 @@ in
# - TR_TORRENT_NAME - Name of torrent (not filename) # - TR_TORRENT_NAME - Name of torrent (not filename)
# - TR_TORRENT_TRACKERS - A comma-delimited list of the torrent's trackers' announce URLs # - TR_TORRENT_TRACKERS - A comma-delimited list of the torrent's trackers' announce URLs
script-torrent-done-enabled = true; script-torrent-done-enabled = true;
script-torrent-done-filename = lib.getExe torrent-done; script-torrent-done-filename = "${torrent-done}/bin/torrent-done";
}; };
# run this behind the OVPN static VPN systemd.services.transmission.after = [ "wireguard-wg-ovpns.service" ];
sane.netns.ovpns.services = [ "transmission" ]; systemd.services.transmission.partOf = [ "wireguard-wg-ovpns.service" ];
systemd.services.transmission = { systemd.services.transmission.serviceConfig = {
environment.TR_DEBUG = "1"; # run this behind the OVPN static VPN
serviceConfig.ExecStartPre = [ NetworkNamespacePath = "/run/netns/ovpns";
# abort if public IP is not as expected Restart = "on-failure";
"${lib.getExe pkgs.sane-scripts.ip-check} --no-upnp --expect ${config.sane.netns.ovpns.wg.address.ipv4}" RestartSec = "30s";
]; BindPaths = [ "/var/media" ]; #< so it can move completed torrents into the media library
serviceConfig.Restart = "on-failure";
serviceConfig.RestartSec = "30s";
serviceConfig.BindPaths = [ "/var/media" ]; #< so it can move completed torrents into the media library
serviceConfig.SystemCallFilter = lib.mkForce [
# the torrent-done script does stuff which fails the nixos default syscall filter.
# allow a bunch of stuff, speculatively, to hopefully fix that:
"@aio"
"@basic-io"
"@chown"
"@file-system"
"@io-event"
"@process"
"@sandbox"
"@sync"
"@system-service"
"quotactl"
];
}; };
# service to automatically backup torrents i add to transmission # service to automatically backup torrents i add to transmission
systemd.services.backup-torrents = { systemd.services.backup-torrents = {
description = "archive torrents to storage not owned by transmission"; description = "archive torrents to storage not owned by transmission";
script = '' script = ''
${lib.getExe pkgs.rsync} -arv /var/lib/transmission/.config/transmission-daemon/torrents/ /var/backup/torrents/ ${pkgs.rsync}/bin/rsync -arv /var/lib/transmission/.config/transmission-daemon/torrents/ /var/backup/torrents/
''; '';
}; };
systemd.timers.backup-torrents = { systemd.timers.backup-torrents = {
@@ -158,15 +159,14 @@ in
# inherit kTLS; # inherit kTLS;
locations."/" = { locations."/" = {
# proxyPass = "http://ovpns.uninsane.org:9091"; # proxyPass = "http://ovpns.uninsane.org:9091";
proxyPass = "http://${config.sane.netns.ovpns.veth.netns.ipv4}:9091"; proxyPass = "http://10.0.1.6:9091";
recommendedProxySettings = true;
}; };
}; };
sane.dns.zones."uninsane.org".inet.CNAME."bt" = "native"; sane.dns.zones."uninsane.org".inet.CNAME."bt" = "native";
sane.ports.ports."51413" = { sane.ports.ports."51413" = {
protocol = [ "tcp" "udp" ]; protocol = [ "tcp" "udp" ];
# visibleTo.ovpns = true; #< not needed: it runs in the ovpns namespace visibleTo.ovpn = true;
description = "colin-bittorrent"; description = "colin-bittorrent";
}; };
} }

View File

@@ -1,111 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p acl -p bash -p coreutils -p findutils -p rsync
# transmission invokes this with no args, and the following env vars:
# - TR_TORRENT_DIR: full path to the folder i told transmission to download it to.
# e.g. "/var/media/torrents/Videos/Film/Jason.Bourne-2016"
# - TR_APP_VERSION
# - TR_TIME_LOCALTIME
# - TR_TORRENT_BYTES_DOWNLOADED
# - TR_TORRENT_HASH
# - TR_TORRENT_ID: local number to uniquely identify this torrent, used by e.g. transmission-remote.
# e.g. "67"
# - TR_TORRENT_LABELS
# - TR_TORRENT_NAME: file/folder name of the toplevel torrent item
# e.g. "Jason Bourne (2016) [2160p] [4K] [BluRay] [5.1] [YTS.MX]"
# - TR_TORRENT_PRIORITY
# - TR_TORRENT_TRACKERS
# optionally, set these variables for debugging (these are specific to my script and not used upstream):
# - TR_DRY_RUN=1
# - TR_DEBUG=1
DOWNLOAD_DIR=/var/media/torrents
destructive() {
if [ -n "${TR_DRY_RUN-}" ]; then
echo "[dry-run] $*"
else
debug "$@"
"$@"
fi
}
debug() {
if [ -n "${TR_DEBUG-}" ]; then
echo "$@"
fi
}
echo "TR_TORRENT_DIR=$TR_TORRENT_DIR TR_TORRENT_NAME=$TR_TORRENT_NAME torrent-done $*"
if [[ "$TR_TORRENT_DIR" =~ ^.*freeleech.*$ ]]; then
# freeleech torrents have no place in my permanent library
echo "freeleech: nothing to do"
exit 0
fi
if ! [[ "$TR_TORRENT_DIR" =~ ^$DOWNLOAD_DIR/.*$ ]]; then
echo "unexpected torrent dir, aborting: $TR_TORRENT_DIR"
exit 1
fi
TORRENT_PATH="$TR_TORRENT_DIR/$TR_TORRENT_NAME"
if [[ ! -e "$TORRENT_PATH" ]]; then
echo "torrent unexpectedly doesn't exist at $TORRENT_PATH. will try fallback"
TORRENT_PATH="$TR_TORRENT_DIR"
fi
if [[ -d "$TORRENT_PATH" ]]; then
# trailing slash so that rsync copies the directory contents, without creating an extra toplevel dir.
TORRENT_PATH="$TORRENT_PATH/"
elif [[ ! -e "$TORRENT_PATH" ]]; then
echo "torrent unexpectedly doesn't exist at TR_TORRENT_DIR=$TORRENT_PATH: bailing"
exit 1
fi
REL_DIR="${TR_TORRENT_DIR#$DOWNLOAD_DIR/}"
MEDIA_DIR="/var/media/$REL_DIR"
destructive mkdir -p "$(dirname "$MEDIA_DIR")"
destructive rsync -rlv "$TORRENT_PATH" "$MEDIA_DIR/"
# make the media rwx by anyone in the group
destructive find "$MEDIA_DIR" -type d -exec setfacl --recursive --modify d:g::rwx,o::rx {} \;
destructive find "$MEDIA_DIR" -type d -exec chmod g+rw,a+rx {} \;
destructive find "$MEDIA_DIR" -type f -exec chmod g+rw,a+r {} \;
# if there's a single directory inside the media dir, then inline that.
# TODO: this is probably obsolete now that i process TR_TORRENT_NAME
subdirs=("$MEDIA_DIR"/*)
debug "top-level items in torrent dir:" "${subdirs[@]}"
if [ ${#subdirs[@]} -eq 1 ]; then
dirname="${subdirs[0]}"
debug "exactly one top-level item, checking if directory: $dirname"
if [ -d "$dirname" ]; then
destructive mv "$dirname"/* "$MEDIA_DIR/" && destructive rmdir "$dirname"
fi
fi
# remove noisy files:
# -iname means "insensitive", but the syntax is NOT regex -- more similar to shell matching
destructive find "$MEDIA_DIR/" -type f \(\
-iname '*downloaded?from*' \
-o -iname '(xxxpav69).txt' \
-o -iname '*upcoming?releases*' \
-o -iname 'ETRG.mp4' \
-o -iname 'Encoded by*.txt' \
-o -iname 'PSArips.com.txt' \
-o -iname 'RARBG.com*' \
-o -iname 'RARBG.txt' \
-o -iname 'RARBG_DO_NOT_MIRROR.exe' \
-o -iname 'Tellytorrent.net.txt' \
-o -iname 'WWW.VPPV.LA.txt' \
-o -iname 'WWW.YIFY*.COM.jpg' \
-o -iname 'YIFY*.com.txt' \
-o -iname 'YTS*.com.txt' \
-o -iname 'YTSYify*.txt' \
-o -iname 'www.YTS*.jpg' \
\) -exec rm {} \;
# might want to keep, might want to remove:
# -o -iname 'info.txt'
# -o -iname 'source.txt'
# -o -iname 'sample.mkv'

View File

@@ -0,0 +1,210 @@
# TODO: split this file apart into smaller files to make it easier to understand
{ config, lib, pkgs, ... }:
let
nativeAddrs = lib.mapAttrs (_name: builtins.head) config.sane.dns.zones."uninsane.org".inet.A;
bindOvpn = "10.0.1.5";
in lib.mkMerge [
{
services.trust-dns.enable = true;
# don't bind to IPv6 until i explicitly test that stack
services.trust-dns.settings.listen_addrs_ipv6 = [];
services.trust-dns.quiet = true;
# FIXME(2023/11/26): services.trust-dns.debug doesn't log requests: use RUST_LOG=debug env for that.
# - see: <https://github.com/hickory-dns/hickory-dns/issues/2082>
# services.trust-dns.debug = true;
sane.ports.ports."53" = {
protocol = [ "udp" "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
visibleTo.ovpn = true;
description = "colin-dns-hosting";
};
sane.dns.zones."uninsane.org".TTL = 900;
# SOA record structure: <https://en.wikipedia.org/wiki/SOA_record#Structure>
# SOA MNAME RNAME (... rest)
# MNAME = Master name server for this zone. this is where update requests should be sent.
# RNAME = admin contact (encoded email address)
# Serial = YYYYMMDDNN, where N is incremented every time this file changes, to trigger secondary NS to re-fetch it.
# Refresh = how frequently secondary NS should query master
# Retry = how long secondary NS should wait until re-querying master after a failure (must be < Refresh)
# Expire = how long secondary NS should continue to reply to queries after master fails (> Refresh + Retry)
sane.dns.zones."uninsane.org".inet = {
SOA."@" = ''
ns1.uninsane.org. admin-dns.uninsane.org. (
2023092101 ; Serial
4h ; Refresh
30m ; Retry
7d ; Expire
5m) ; Negative response TTL
'';
TXT."rev" = "2023092101";
CNAME."native" = "%CNAMENATIVE%";
A."@" = "%ANATIVE%";
A."servo.wan" = "%AWAN%";
A."servo.lan" = config.sane.hosts.by-name."servo".lan-ip;
A."servo.hn" = config.sane.hosts.by-name."servo".wg-home.ip;
# XXX NS records must also not be CNAME
# it's best that we keep this identical, or a superset of, what org. lists as our NS.
# so, org. can specify ns2/ns3 as being to the VPN, with no mention of ns1. we provide ns1 here.
A."ns1" = "%ANATIVE%";
A."ns2" = "185.157.162.178";
A."ns3" = "185.157.162.178";
A."ovpns" = "185.157.162.178";
NS."@" = [
"ns1.uninsane.org."
"ns2.uninsane.org."
"ns3.uninsane.org."
];
};
services.trust-dns.settings.zones = [ "uninsane.org" ];
# TODO: can i transform this into some sort of service group?
# have `systemctl restart trust-dns.service` restart all the individual services?
systemd.services.trust-dns.serviceConfig = {
DynamicUser = lib.mkForce false;
User = "trust-dns";
Group = "trust-dns";
wantedBy = lib.mkForce [];
};
systemd.services.trust-dns.enable = false;
users.groups.trust-dns = {};
users.users.trust-dns = {
group = "trust-dns";
isSystemUser = true;
};
# sane.services.dyn-dns.restartOnChange = [ "trust-dns.service" ];
networking.nat.enable = true;
networking.nat.extraCommands = ''
# redirect incoming DNS requests from LAN addresses
# to the LAN-specialized DNS service
# N.B.: use the `nixos-*` chains instead of e.g. PREROUTING
# because they get cleanly reset across activations or `systemctl restart firewall`
# instead of accumulating cruft
iptables -t nat -A nixos-nat-pre -p udp --dport 53 \
-m iprange --src-range 10.78.76.0-10.78.79.255 \
-j DNAT --to-destination :1053
iptables -t nat -A nixos-nat-pre -p tcp --dport 53 \
-m iprange --src-range 10.78.76.0-10.78.79.255 \
-j DNAT --to-destination :1053
'';
sane.ports.ports."1053" = {
# because the NAT above redirects in nixos-nat-pre, LAN requests behave as though they arrived on the external interface at the redirected port.
# TODO: try nixos-nat-post instead?
# TODO: or, don't NAT from port 53 -> port 1053, but rather nat from LAN addr to a loopback addr.
# - this is complicated in that loopback is a different interface than eth0, so rewriting the destination address would cause the packets to just be dropped by the interface
protocol = [ "udp" "tcp" ];
visibleTo.lan = true;
description = "colin-redirected-dns-for-lan-namespace";
};
}
{
systemd.services =
let
sed = "${pkgs.gnused}/bin/sed";
stateDir = "/var/lib/trust-dns";
zoneTemplate = pkgs.writeText "uninsane.org.zone.in" config.sane.dns.zones."uninsane.org".rendered;
zoneDirFor = flavor: "${stateDir}/${flavor}";
zoneFor = flavor: "${zoneDirFor flavor}/uninsane.org.zone";
mkTrustDnsService = opts: flavor: let
flags = let baseCfg = config.services.trust-dns; in
(lib.optional baseCfg.debug "--debug") ++ (lib.optional baseCfg.quiet "--quiet");
flagsStr = builtins.concatStringsSep " " flags;
anative = nativeAddrs."servo.${flavor}";
toml = pkgs.formats.toml { };
configTemplate = opts.config or (toml.generate "trust-dns-${flavor}.toml" (
(
lib.filterAttrsRecursive (_: v: v != null) config.services.trust-dns.settings
) // {
listen_addrs_ipv4 = opts.listen or [ anative ];
}
));
configFile = "${stateDir}/${flavor}-config.toml";
port = opts.port or 53;
in {
description = "trust-dns Domain Name Server (serving ${flavor})";
unitConfig.Documentation = "https://trust-dns.org/";
preStart = ''
wan=$(cat '${config.sane.services.dyn-dns.ipPath}')
${sed} s/%AWAN%/$wan/ ${configTemplate} > ${configFile}
'' + lib.optionalString (!opts ? config) ''
mkdir -p ${zoneDirFor flavor}
${sed} \
-e s/%CNAMENATIVE%/servo.${flavor}/ \
-e s/%ANATIVE%/${anative}/ \
-e s/%AWAN%/$wan/ \
-e s/%AOVPNS%/185.157.162.178/ \
${zoneTemplate} > ${zoneFor flavor}
'';
serviceConfig = config.systemd.services.trust-dns.serviceConfig // {
ExecStart = ''
${pkgs.trust-dns}/bin/${pkgs.trust-dns.meta.mainProgram} \
--port ${builtins.toString port} \
--zonedir ${zoneDirFor flavor}/ \
--config ${configFile} ${flagsStr}
'';
};
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
};
in {
trust-dns-wan = mkTrustDnsService { listen = [ nativeAddrs."servo.lan" bindOvpn ]; } "wan";
trust-dns-lan = mkTrustDnsService { port = 1053; } "lan";
trust-dns-hn = mkTrustDnsService { port = 1053; } "hn";
trust-dns-hn-resolver = mkTrustDnsService {
config = pkgs.writeText "hn-resolver-config.toml" ''
# i host a resolver in the wireguard VPN so that clients can resolve DNS through the VPN.
# (that's what this file achieves).
#
# one would expect this resolver could host the authoritative zone for `uninsane.org`, and then forward everything else to the system resolver...
# and while that works for `dig`, it breaks for `nslookup` (and so `ssh`, etc).
#
# DNS responses include a flag for if the responding server is the authority of the zone queried.
# it seems that default Linux stub resolvers either:
# - expect DNSSEC when the response includes that bit, or
# - expect A records to be in the `answer` section instead of `additional` section.
# or perhaps something more nuanced. but for `nslookup` to be reliable, it has to talk to an
# instance of trust-dns which is strictly a resolver, with no authority.
# hence, this config: a resolver which forwards to the actual authority.
listen_addrs_ipv4 = ["${nativeAddrs."servo.hn"}"]
listen_addrs_ipv6 = []
[[zones]]
zone = "uninsane.org"
zone_type = "Forward"
stores = { type = "forward", name_servers = [{ socket_addr = "${nativeAddrs."servo.hn"}:1053", protocol = "udp", trust_nx_responses = true }] }
[[zones]]
# forward the root zone to the local DNS resolver
zone = "."
zone_type = "Forward"
stores = { type = "forward", name_servers = [{ socket_addr = "127.0.0.53:53", protocol = "udp", trust_nx_responses = true }] }
'';
} "hn-resolver";
};
sane.services.dyn-dns.restartOnChange = [
"trust-dns-wan.service"
"trust-dns-lan.service"
"trust-dns-hn.service"
# "trust-dns-hn-resolver.service" # doesn't need restart because it doesn't know about WAN IP
];
}
]

View File

@@ -1,6 +0,0 @@
{ ... }:
{
imports = [
./shelvacu.nix
];
}

View File

@@ -1,65 +0,0 @@
{ lib, pkgs, ... }:
{
users.users.shelvacu = {
isNormalUser = true;
home = "/home/shelvacu";
subUidRanges = [
{ startUid=300000; count=1; }
];
group = "users";
initialPassword = lib.mkDefault "";
shell = pkgs.bash;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKoy1TrmfhBGWtVedgOM1FB1oD2UdodN3LkBnnLx6Tug compute-deck"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAxAFFxQMXAgi+0cmGaNE/eAkVfEl91wafUqFIuAkI5I compute-deck-root"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINQ2c0GzlVMjV06CS7bWbCaAbzG2+7g5FCg/vClJPe0C fw"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGHLPOxRd68+DJ/bYmqn0wsgwwIcMSMyuU1Ya16hCb/m fw-root"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOre0FnYDm3arsFj9c/l5H2Q8mdmv7kmvq683pL4heru legtop"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINznGot+L8kYoVQqdLV/R17XCd1ILMoDCILOg+I3s5wC pixel9pro-nod"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDcRDekd8ZOYfQS5X95/yNof3wFYIbHqWeq4jY0+ywQX pro1x-nod"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJNFbzt0NHVTaptBI38YtwLG+AsmeNYy0Nr5yX2zZEPE root@vacuInstaller toptop-root"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICVeSzDkGTueZijB0xUa08e06ovAEwwZK/D+Cc7bo91g triple-dezert"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtwtao/TXbiuQOYJbousRPVesVcb/2nP0PCFUec0Nv8 triple-dezert-root"
];
};
security.sudo.extraRules = [
{
users = [ "shelvacu" ];
runAs = "postgres";
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
}
];
security.polkit.extraConfig = ''
// allow:
// - systemctl restart|start|stop SERVICE
polkit.addRule(function(action, subject) {
if (subject.user == "shelvacu" && action.id == "org.freedesktop.systemd1.manage-units") {
switch (action.lookup("verb")) {
// case "cancel":
// case "reenable":
case "restart":
// case "reload":
// case "reload-or-restart":
case "start":
case "stop":
// case "try-reload-or-restart":
// case "try-restart":
return polkit.Result.YES;
default:
}
}
})
'';
sane.persist.sys.byStore.private = [
{ path = "/home/shelvacu/persist"; user = "shelvacu"; group = "users"; mode = "0700"; }
];
}

View File

@@ -1,57 +0,0 @@
{ lib, pkgs, ... }:
{
boot.initrd.supportedFilesystems = [ "ext4" "btrfs" "ext2" "ext3" "vfat" ];
# useful emergency utils
boot.initrd.extraUtilsCommands = ''
copy_bin_and_libs ${lib.getExe' pkgs.btrfs-progs "btrfstune"}
copy_bin_and_libs ${lib.getExe' pkgs.e2fsprogs "resize2fs"}
copy_bin_and_libs ${lib.getExe' pkgs.gptfdisk "{cgdisk,gdisk}"}
copy_bin_and_libs ${lib.getExe' pkgs.mtools "mlabel"}
copy_bin_and_libs ${lib.getExe pkgs.nvme-cli}
copy_bin_and_libs ${lib.getExe' pkgs.smartmontools "smartctl"}
copy_bin_and_libs ${lib.getExe' pkgs.util-linux "{cfdisk,lsblk,lscpu}"}
'';
boot.kernelParams = [
"boot.shell_on_fail"
#v experimental full pre-emption for hopefully better call/audio latency on moby.
# also toggleable at runtime via /sys/kernel/debug/sched/preempt
# defaults to preempt=voluntary
# "preempt=full"
];
# other kernelParams:
# "boot.trace"
# "systemd.log_level=debug"
# "systemd.log_target=console"
# moby has to run recent kernels (defined elsewhere).
# meanwhile, kernel variation plays some minor role in things like sandboxing (landlock) and capabilities.
# - as of 2024/08/xx, my boot fails on 6.6, but works on 6.9 and (probably; recently) 6.8.
# simpler to keep near the latest kernel on all devices,
# and also makes certain that any weird system-level bugs i see aren't likely to be stale kernel bugs.
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
# boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_testing;
# hack in the `boot.shell_on_fail` arg since that doesn't always seem to work.
boot.initrd.preFailCommands = "allowShell=1";
# default: 4 (warn). 7 is debug
boot.consoleLogLevel = 7;
boot.loader.grub.enable = lib.mkDefault false;
# boot.loader.generic-extlinux-compatible.enable = lib.mkDefault true;
boot.loader.systemd-boot.enable = lib.mkDefault true;
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 20;
boot.loader.systemd-boot.edk2-uefi-shell.enable = lib.mkDefault true;
boot.loader.systemd-boot.memtest86.enable = lib.mkDefault
(lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.memtest86plus);
hardware.enableAllFirmware = true; # firmware with licenses that don't allow for redistribution. fuck lawyers, fuck IP, give me the goddamn firmware.
# hardware.enableRedistributableFirmware = true; # proprietary but free-to-distribute firmware (extraneous to `enableAllFirmware` option)
# default is 252274, which is too low particularly for servo.
# manifests as spurious "No space left on device" when trying to install watches,
# e.g. in dyn-dns by `systemctl start dyn-dns-watcher.path`.
# see: <https://askubuntu.com/questions/828779/failed-to-add-run-systemd-ask-password-to-directory-watch-no-space-left-on-dev>
boot.kernel.sysctl."fs.inotify.max_user_watches" = 4194304;
boot.kernel.sysctl."fs.inotify.max_user_instances" = 4194304;
}

View File

@@ -1,46 +1,33 @@
{ lib, pkgs, ... }: { config, lib, pkgs, ... }:
{ {
imports = [ imports = [
./boot.nix
./feeds.nix ./feeds.nix
./fs ./fs.nix
./hardware
./home ./home
./hosts.nix ./hosts.nix
./ids.nix ./ids.nix
./machine-id.nix ./machine-id.nix
./net ./net
./nix.nix ./nix
./persist.nix
./polyunfill.nix ./polyunfill.nix
./programs ./programs
./quirks.nix
./secrets.nix ./secrets.nix
./snapper.nix
./ssh.nix ./ssh.nix
./systemd.nix ./systemd.nix
./users ./users
]; ];
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
# this affects where nixos modules look for stateful data which might have been migrated across releases.
system.stateVersion = "21.11";
sane.nixcache.enable-trusted-keys = true; sane.nixcache.enable-trusted-keys = true;
sane.nixcache.enable = lib.mkDefault true; sane.nixcache.enable = lib.mkDefault true;
sane.persist.enable = lib.mkDefault true; sane.persist.enable = lib.mkDefault true;
sane.root-on-tmpfs = lib.mkDefault true; sane.root-on-tmpfs = lib.mkDefault true;
sane.programs.sysadminUtils.enableFor.system = lib.mkDefault true; sane.programs.sysadminUtils.enableFor.system = lib.mkDefault true;
sane.programs.sysadminExtraUtils.enableFor.system = lib.mkDefault true;
sane.programs.consoleUtils.enableFor.user.colin = lib.mkDefault true; sane.programs.consoleUtils.enableFor.user.colin = lib.mkDefault true;
services.buffyboard.enable = true; nixpkgs.config.allowUnfree = true; # NIXPKGS_ALLOW_UNFREE=1
services.buffyboard.settings.theme.default = "pmos-light"; nixpkgs.config.allowBroken = true; # NIXPKGS_ALLOW_BROKEN=1
# services.buffyboard.settings.quirks.fbdev_force_refresh = true;
services.buffyboard.extraFlags = [ "--verbose" ];
# irqbalance monitors interrupt count (as a daemon) and assigns high-frequency interrupts to different CPUs.
# that reduces contention between simultaneously-fired interrupts.
services.irqbalance.enable = true;
# time.timeZone = "America/Los_Angeles"; # time.timeZone = "America/Los_Angeles";
time.timeZone = "Etc/UTC"; # DST is too confusing for me => use a stable timezone time.timeZone = "Etc/UTC"; # DST is too confusing for me => use a stable timezone
@@ -52,7 +39,7 @@
# source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix> # source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix>
# modified to not error on boot (when /run/current-system doesn't exist) # modified to not error on boot (when /run/current-system doesn't exist)
if [ -d /run/current-system ]; then if [ -d /run/current-system ]; then
${lib.getExe pkgs.nvd} --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig" ${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
fi fi
''; '';
}; };

View File

@@ -1,15 +1,14 @@
# where to find good stuff? # where to find good stuff?
# - universal search/directory: <https://podcastindex.org> # - universal search/directory: <https://podcastindex.org>
# - the full database is downloadable
# - find adjacent podcasts: <https://rephonic.com/graph>
# - charts: <https://rephonic.com/charts/apple/united-states/technology>
# - list of lists: <https://en.wikipedia.org/wiki/Category:Lists_of_podcasts>
# - podcasts w/ a community: <https://lemmyverse.net/communities?query=podcast> # - podcasts w/ a community: <https://lemmyverse.net/communities?query=podcast>
# - podcast recs: # - podcast rec thread: <https://lemmy.ml/post/1565858>
# - active lemmy: <https://slrpnk.net/c/podcasts>
# - old thread: <https://lemmy.ml/post/1565858>
# #
# - paywall bypass / bootlegs: <https://jumble.top/> # candidates:
# - The Nonlinear Library (podcast): <https://forum.effectivealtruism.org/posts/JTZTBienqWEAjGDRv/listen-to-more-ea-content-with-the-nonlinear-library>
# - has ~10 posts per day, text-to-speech; i would need better tagging before adding this
# - <https://www.metaculus.com/questions/11102/introducing-the-metaculus-journal-podcast/>
# - dead since 2022/10 - 2023/03
{ lib, sane-data, ... }: { lib, sane-data, ... }:
let let
hourly = { freq = "hourly"; }; hourly = { freq = "hourly"; };
@@ -61,104 +60,60 @@ let
}; };
podcasts = [ podcasts = [
(fromDb "404media.co/the-404-media-podcast" // tech)
(fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech) # ACQ2 - more "Acquired" episodes (fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech) # ACQ2 - more "Acquired" episodes
(fromDb "adventofcomputing.com" // tech) # computing history (fromDb "allinchamathjason.libsyn.com" // pol)
(fromDb "api.oyez.org/podcasts/oral-arguments/2015" // pol) # Supreme Court Oral Arguments ("2015" in URL means nothing -- it's still updated)
(fromDb "anchor.fm/s/34c7232c/podcast/rss" // tech) # Civboot -- https://anchor.fm/civboot (fromDb "anchor.fm/s/34c7232c/podcast/rss" // tech) # Civboot -- https://anchor.fm/civboot
(fromDb "anchor.fm/s/2da69154/podcast/rss" // tech) # POD OF JAKE -- https://podofjake.com/ (fromDb "anchor.fm/s/2da69154/podcast/rss" // tech) # POD OF JAKE -- https://podofjake.com/
(fromDb "bluecityblues.org.podcastpage.io" // pol) # hosts overlap with Seattle Nice
(fromDb "buzzsprout.com/2126417" // tech) # Mystery AI Hype Theater 3000
(fromDb "cast.postmarketos.org" // tech) (fromDb "cast.postmarketos.org" // tech)
(fromDb "congressionaldish.libsyn.com" // pol) # Jennifer Briney (fromDb "congressionaldish.libsyn.com" // pol) # Jennifer Briney
(fromDb "craphound.com" // pol) # Cory Doctorow -- both podcast & text entries (fromDb "craphound.com" // pol) # Cory Doctorow -- both podcast & text entries
(fromDb "darknetdiaries.com" // tech) (fromDb "darknetdiaries.com" // tech)
(fromDb "dwarkeshpatel.com" // tech) (fromDb "feed.podbean.com/matrixlive/feed.xml" // tech) # Matrix (chat) Live
(fromDb "feeds.99percentinvisible.org/99percentinvisible" // pol) # 99% Invisible -- also available here: <https://feeds.simplecast.com/BqbsxVfO> (fromDb "feeds.99percentinvisible.org/99percentinvisible" // pol) # 99% Invisible -- also available here: <https://feeds.simplecast.com/BqbsxVfO>
(fromDb "feeds.acast.com/public/shows/lawfare" // pol) # <https://www.lawfaremedia.org/podcasts-multimedia/podcast/the-lawfare-podcast>
(fromDb "feeds.buzzsprout.com/2412334.rss") # Matt Stoller's _Organized Money_ <https://www.organizedmoney.fm/>
(fromDb "feeds.eff.org/howtofixtheinternet" // pol)
(fromDb "feeds.feedburner.com/80000HoursPodcast" // rat) (fromDb "feeds.feedburner.com/80000HoursPodcast" // rat)
(fromDb "feeds.feedburner.com/dancarlin/history" // rat) (fromDb "feeds.feedburner.com/dancarlin/history" // rat)
(fromDb "feeds.feedburner.com/radiolab" // pol) # Radiolab -- also available here, but ONLY OVER HTTP: <http://feeds.wnyc.org/radiolab> (fromDb "feeds.feedburner.com/radiolab" // pol) # Radiolab -- also available here, but ONLY OVER HTTP: <http://feeds.wnyc.org/radiolab>
(fromDb "feeds.megaphone.fm/CHTAL4990341033" // pol) # ChinaTalk: https://www.chinatalk.media/podcast
(fromDb "feeds.megaphone.fm/GLT1412515089" // pol) # JRE: Joe Rogan Experience
(fromDb "feeds.megaphone.fm/behindthebastards" // pol) # also Maggie Killjoy (fromDb "feeds.megaphone.fm/behindthebastards" // pol) # also Maggie Killjoy
(fromDb "feeds.megaphone.fm/cspantheweekly" // pol)
(fromDb "feeds.megaphone.fm/econ102") # Noah Smith + Erik Torenberg <https://www.podpage.com/econ102/>
(fromDb "feeds.megaphone.fm/history102") # <https://www.podpage.com/history-102-with-whatifalthist/>
(fromDb "feeds.megaphone.fm/recodedecode" // tech) # The Verge - Decoder (fromDb "feeds.megaphone.fm/recodedecode" // tech) # The Verge - Decoder
(fromDb "feeds.megaphone.fm/thiswontlast" // tech) # <https://www.podpage.com/thiswontlast/> (fromDb "feeds.simplecast.com/54nAGcIl" // pol) # The Daily
(fromDb "feeds.megaphone.fm/unexplainable") (fromDb "feeds.simplecast.com/82FI35Px" // pol) # Ezra Klein Show
(fromDb "feeds.simplecast.com/wgl4xEgL" // rat) # Econ Talk (fromDb "feeds.simplecast.com/wgl4xEgL" // rat) # Econ Talk
(fromDb "feeds.simplecast.com/xKJ93w_w" // uncat) # Atlas Obscura
(fromDb "feeds.transistor.fm/acquired" // tech) (fromDb "feeds.transistor.fm/acquired" // tech)
(fromDb "feeds.transistor.fm/complex-systems-with-patrick-mckenzie-patio11" // tech) # Patrick Mackenzie (from Bits About Money)
(fromDb "feeds.twit.tv/floss.xml" // tech)
(fromDb "fulltimenix.com" // tech) (fromDb "fulltimenix.com" // tech)
(fromDb "futureofcoding.org/episodes" // tech)
(fromDb "hackerpublicradio.org" // tech) (fromDb "hackerpublicradio.org" // tech)
(fromDb "lexfridman.com/podcast" // rat) (fromDb "lexfridman.com/podcast" // rat)
(fromDb "linktr.ee/betteroffline" // pol)
(fromDb "linuxdevtime.com" // tech)
(fromDb "malicious.life" // tech)
(fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies (fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies
(fromDb "motherearthnewsandfriends.libsyn.com" // uncat) # off-grid living
(fromDb "microarch.club" // tech)
(fromDb "nocturnepodcast.org")
(fromDb "omegataupodcast.net" // tech) # 3/4 German; 1/4 eps are English (fromDb "omegataupodcast.net" // tech) # 3/4 German; 1/4 eps are English
(fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol) # Maggie Killjoy -- referenced by Cory Doctorow (fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol) # Maggie Killjoy -- referenced by Cory Doctorow
(fromDb "omny.fm/shows/money-stuff-the-podcast") # Matt Levine
(fromDb "omny.fm/shows/stuff-you-should-know-1")
(fromDb "omny.fm/shows/the-dollop-with-dave-anthony-and-gareth-reynolds") # The Dollop history/comedy (fromDb "omny.fm/shows/the-dollop-with-dave-anthony-and-gareth-reynolds") # The Dollop history/comedy
(fromDb "omny.fm/shows/weird-little-guys") # Cool Zone Media
(fromDb "originstories.libsyn.com" // uncat) (fromDb "originstories.libsyn.com" // uncat)
(fromDb "podcast.ergaster.org/@flintandsilicon" // tech) # Thib's podcast: public interest tech, gnome, etc: <https://fed.uninsane.org/users/$ALLO9MZ5g5CsQTCBH6> (fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol)
(fromDb "pods.media/api/rss/feed/channel/unchained" // tech) # cryptocurrency happenings; rec via patio11
(fromDb "politicalorphanage.libsyn.com" // pol) (fromDb "politicalorphanage.libsyn.com" // pol)
(fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast (fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast
(fromDb "rss.acast.com/ft-tech-tonic" // tech) # Financial Time's: Tech Tonic (fromDb "rss.acast.com/deconstructed") # The Intercept - Deconstructed
(fromDb "rss.acast.com/ft-tech-tonic" // tech)
(fromDb "rss.acast.com/intercepted-with-jeremy-scahill") # The Intercept - Intercepted
(fromDb "rss.art19.com/60-minutes" // pol)
(fromDb "rss.art19.com/the-portal" // rat) # Eric Weinstein (fromDb "rss.art19.com/the-portal" // rat) # Eric Weinstein
(fromDb "seattlenice.buzzsprout.com" // pol) # Seattle Nice (fromDb "seattlenice.buzzsprout.com" // pol)
(fromDb "speedboatdope.com" // pol) # Chapo Trap House (premium feed)
(fromDb "srslywrong.com" // pol) (fromDb "srslywrong.com" // pol)
(fromDb "sharkbytes.transistor.fm" // tech) # Wireshark Podcast o_0 (fromDb "sharkbytes.transistor.fm" // tech) # Wireshark Podcast o_0
(fromDb "sharptech.fm/feed/podcast" // tech) # Ben Thompson (fromDb "sscpodcast.libsyn.com" // rat) # Astral Codex Ten
(fromDb "sscpodcast.libsyn.com" // rat) # Astral Codex Ten; Scott Alexander
(fromDb "talesfromthebridge.buzzsprout.com" // tech) # Sci-Fi? has Peter Watts; author of No Moods, Ads or Cutesy Fucking Icons (rifters.com) (fromDb "talesfromthebridge.buzzsprout.com" // tech) # Sci-Fi? has Peter Watts; author of No Moods, Ads or Cutesy Fucking Icons (rifters.com)
(fromDb "techtalesshow.com" // tech) # Corbin Davenport (fromDb "techwontsave.us" // pol) # rec by Cory Doctorow
(fromDb "theamphour.com" // tech) # The Amp Hour (fromDb "wakingup.libsyn.com" // pol) # Sam Harris
(fromDb "the-ben-marc-show.simplecast.com" // tech // pol) # Ben Horowitz + Marc Andreessen; love to hate em (fromDb "werenotwrong.fireside.fm" // pol)
(fromDb "timclicks.dev/compose-podcast" // tech) # Rust-heavy dev interviews
(fromDb "werenotwrong.fireside.fm" // pol) # We're Not Wrong
(fromDb "whycast.podcast.audio/@whycast" // tech) # What Hackers Yearn [for]: <https://why2025.org/>
(mkPod "https://sfconservancy.org/casts/the-corresponding-source/feeds/ogg/" // tech) (mkPod "https://sfconservancy.org/casts/the-corresponding-source/feeds/ogg/" // tech)
# (fromDb "allinchamathjason.libsyn.com" // pol)
# (fromDb "feed.podbean.com/matrixlive/feed.xml" // tech) # Matrix (chat) Live
# (fromDb "feeds.libsyn.com/421877" // rat) # Less Wrong Curated # (fromDb "feeds.libsyn.com/421877" // rat) # Less Wrong Curated
# (fromDb "feeds.megaphone.fm/hubermanlab" // uncat) # Daniel Huberman on sleep # (fromDb "feeds.megaphone.fm/hubermanlab" // uncat) # Daniel Huberman on sleep
# (fromDb "feeds.simplecast.com/54nAGcIl" // pol) # The Daily
# (fromDb "feeds.simplecast.com/82FI35Px" // pol) # Ezra Klein Show
# (fromDb "feeds.simplecast.com/l2i9YnTd" // tech // pol) # Hard Fork (NYtimes tech) # (fromDb "feeds.simplecast.com/l2i9YnTd" // tech // pol) # Hard Fork (NYtimes tech)
# (fromDb "feeds.simplecast.com/whlwDbyc" // tech) # Tech Lounge: <https://chrischinchilla.com/podcast/techlounge/>
# (fromDb "feeds.simplecast.com/xKJ93w_w" // uncat) # Atlas Obscura
# (fromDb "iheart.com/podcast/1119-away-days-podcast-reporti-275359753" // pol) # Away Days (Cool Zone Media)
# (fromDb "lastweekinai.com" // tech) # Last Week in AI
# (fromDb "mintcast.org" // tech)
# (fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol)
# (fromDb "podcast.sustainoss.org" // tech) # "Sustainable tech", only... it somehow manages to avoid any tech which is actually sustainable, and most of the time doesn't even talk about Open Source Software (!). normie/surface-level/"feel good"
# (fromDb "podcast.thelinuxexp.com" // tech) # low-brow linux/foss PR announcements # (fromDb "podcast.thelinuxexp.com" // tech) # low-brow linux/foss PR announcements
# (fromDb "politicspoliticspolitics.com" // pol) # don't judge me. Justin Robert Young.
# (fromDb "rss.acast.com/deconstructed") # The Intercept - Deconstructed
# (fromDb "rss.acast.com/intercepted-with-jeremy-scahill") # The Intercept - Intercepted
# (fromDb "rss.art19.com/60-minutes" // pol)
# (fromDb "rss.art19.com/your-welcome" // pol) # Michael Malice - Your Welcome -- also available here: <https://origin.podcastone.com/podcast?categoryID2=2232> # (fromDb "rss.art19.com/your-welcome" // pol) # Michael Malice - Your Welcome -- also available here: <https://origin.podcastone.com/podcast?categoryID2=2232>
# (fromDb "rss.prod.firstlook.media/deconstructed/podcast.rss" // pol) #< possible URL rot # (fromDb "rss.prod.firstlook.media/deconstructed/podcast.rss" // pol) #< possible URL rot
# (fromDb "rss.prod.firstlook.media/intercepted/podcast.rss" // pol) #< possible URL rot # (fromDb "rss.prod.firstlook.media/intercepted/podcast.rss" // pol) #< possible URL rot
# (fromDb "sites.libsyn.com/438684" // humor) # Quorators - digging up *weird* Quota questions
# (fromDb "techwontsave.us" // pol) # rec by Cory Doctorow, but way too info-sparse
# (fromDb "trashfuturepodcast.podbean.com" // pol) # rec by Cory Doctorow, but way rambly # (fromDb "trashfuturepodcast.podbean.com" // pol) # rec by Cory Doctorow, but way rambly
# (fromDb "wakingup.libsyn.com" // pol) # Sam Harris, but he just repeats himself now
# (mkPod "https://anchor.fm/s/21bc734/podcast/rss" // pol // infrequent) # Emerge: making sense of what's next -- <https://www.whatisemerging.com/emergepodcast> # (mkPod "https://anchor.fm/s/21bc734/podcast/rss" // pol // infrequent) # Emerge: making sense of what's next -- <https://www.whatisemerging.com/emergepodcast>
# (mkPod "https://audioboom.com/channels/5097784.rss" // tech) # Lateral with Tom Scott # (mkPod "https://audioboom.com/channels/5097784.rss" // tech) # Lateral with Tom Scott
# (mkPod "https://feeds.megaphone.fm/RUNMED9919162779" // pol // infrequent) # The Witch Trials of J.K. Rowling: <https://www.thefp.com/witchtrials> # (mkPod "https://feeds.megaphone.fm/RUNMED9919162779" // pol // infrequent) # The Witch Trials of J.K. Rowling: <https://www.thefp.com/witchtrials>
@@ -166,17 +121,14 @@ let
]; ];
texts = [ texts = [
(fromDb "ergaster.org/blog" // tech) # Thib's blog: public interest tech, gnome, etc: <https://fed.uninsane.org/users/$ALLO9MZ5g5CsQTCBH6>
(fromDb "acoup.blog/feed") # history, states. author: <https://historians.social/@bretdevereaux/following> (fromDb "acoup.blog/feed") # history, states. author: <https://historians.social/@bretdevereaux/following>
(fromDb "amosbbatto.wordpress.com" // tech) (fromDb "amosbbatto.wordpress.com" // tech)
(fromDb "anish.lakhwara.com" // tech) (fromDb "anish.lakhwara.com" // tech)
(fromDb "antipope.org") # Charles Stross
(fromDb "apenwarr.ca/log/rss.php" // tech) # CEO of tailscale
(fromDb "applieddivinitystudies.com" // rat) (fromDb "applieddivinitystudies.com" // rat)
(fromDb "artemis.sh" // tech) (fromDb "artemis.sh" // tech)
(fromDb "ascii.textfiles.com" // tech) # Jason Scott (fromDb "ascii.textfiles.com" // tech) # Jason Scott
(fromDb "austinvernon.site" // tech) (fromDb "austinvernon.site" // tech)
(fromDb "buttondown.email" // tech) # (fromDb "balajis.com" // pol) # Balaji
(fromDb "ben-evans.com/benedictevans" // pol) (fromDb "ben-evans.com/benedictevans" // pol)
(fromDb "bitbashing.io" // tech) (fromDb "bitbashing.io" // tech)
(fromDb "bitsaboutmoney.com" // uncat) (fromDb "bitsaboutmoney.com" // uncat)
@@ -185,9 +137,10 @@ let
(fromDb "blog.jmp.chat" // tech) (fromDb "blog.jmp.chat" // tech)
(fromDb "blog.rust-lang.org" // tech) (fromDb "blog.rust-lang.org" // tech)
(fromDb "blog.thalheim.io" // tech) # Mic92 (fromDb "blog.thalheim.io" // tech) # Mic92
(fromDb "blog.brixit.nl" // tech) # Martijn Braam
(fromDb "bunniestudios.com" // tech) # Bunnie Juang (fromDb "bunniestudios.com" // tech) # Bunnie Juang
(fromDb "capitolhillseattle.com" // pol) (fromDb "capitolhillseattle.com" // pol)
# (fromDb "drewdevault.com" // tech)
# (fromDb "econlib.org" // pol)
(fromDb "edwardsnowden.substack.com" // pol // text) (fromDb "edwardsnowden.substack.com" // pol // text)
(fromDb "fasterthanli.me" // tech) (fromDb "fasterthanli.me" // tech)
(fromDb "gwern.net" // rat) (fromDb "gwern.net" // rat)
@@ -198,17 +151,15 @@ let
(fromDb "interconnected.org/home/feed" // rat) # Matt Webb -- engineering-ish, but dreamy (fromDb "interconnected.org/home/feed" // rat) # Matt Webb -- engineering-ish, but dreamy
(fromDb "jeffgeerling.com" // tech) (fromDb "jeffgeerling.com" // tech)
(fromDb "jefftk.com" // tech) (fromDb "jefftk.com" // tech)
(fromDb "justine.lol" // tech)
(fromDb "jwz.org/blog" // tech // pol) # DNA lounge guy, loooong-time blogger
(fromDb "kill-the-newsletter.com/feeds/joh91bv7am2pnznv.xml" // pol) # Matt Levine - Money Stuff (fromDb "kill-the-newsletter.com/feeds/joh91bv7am2pnznv.xml" // pol) # Matt Levine - Money Stuff
(fromDb "kosmosghost.github.io/index.xml" // tech) (fromDb "kosmosghost.github.io/index.xml" // tech)
# (fromDb "lesswrong.com" // rat)
(fromDb "linmob.net" // tech) (fromDb "linmob.net" // tech)
(fromDb "lwn.net" // tech) (fromDb "lwn.net" // tech)
(fromDb "lynalden.com" // pol) (fromDb "lynalden.com" // pol)
(fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow (fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow
(fromDb "mg.lol" // tech) (fromDb "mg.lol" // tech)
(fromDb "mindingourway.com" // rat) (fromDb "mindingourway.com" // rat)
(fromDb "momi.ca" // tech) # Anjan, pmOS
(fromDb "morningbrew.com/feed" // pol) (fromDb "morningbrew.com/feed" // pol)
(fromDb "nixpkgs.news" // tech) (fromDb "nixpkgs.news" // tech)
(fromDb "overcomingbias.com" // rat) # Robin Hanson (fromDb "overcomingbias.com" // rat) # Robin Hanson
@@ -217,13 +168,13 @@ let
(fromDb "pomeroyb.com" // tech) (fromDb "pomeroyb.com" // tech)
(fromDb "postmarketos.org/blog" // tech) (fromDb "postmarketos.org/blog" // tech)
(fromDb "preposterousuniverse.com" // rat) # Sean Carroll (fromDb "preposterousuniverse.com" // rat) # Sean Carroll
(fromDb "profectusmag.com" // uncat)
(fromDb "project-insanity.org" // tech) # shared blog by a few NixOS devs, notably onny (fromDb "project-insanity.org" // tech) # shared blog by a few NixOS devs, notably onny
(fromDb "putanumonit.com" // rat) # mostly dating topics. not advice, or humor, but looking through a social lens (fromDb "putanumonit.com" // rat) # mostly dating topics. not advice, or humor, but looking through a social lens
(fromDb "richardcarrier.info" // rat) (fromDb "richardcarrier.info" // rat)
(fromDb "rifters.com/crawl" // uncat) # No Moods, Ads or Cutesy Fucking Icons (fromDb "rifters.com/crawl" // uncat) # No Moods, Ads or Cutesy Fucking Icons
(fromDb "righto.com" // tech) # Ken Shirriff (fromDb "righto.com" // tech) # Ken Shirriff
(fromDb "rootsofprogress.org" // rat) # Jason Crawford (fromDb "rootsofprogress.org" // rat) # Jason Crawford
(fromDb "samuel.dionne-riel.com" // tech) # SamuelDR
(fromDb "sagacioussuricata.com" // tech) # ian (Sanctuary) (fromDb "sagacioussuricata.com" // tech) # ian (Sanctuary)
(fromDb "semiaccurate.com" // tech) (fromDb "semiaccurate.com" // tech)
(fromDb "sideways-view.com" // rat) # Paul Christiano (fromDb "sideways-view.com" // rat) # Paul Christiano
@@ -231,82 +182,57 @@ let
(fromDb "slimemoldtimemold.com" // rat) (fromDb "slimemoldtimemold.com" // rat)
(fromDb "spectrum.ieee.org" // tech) (fromDb "spectrum.ieee.org" // tech)
(fromDb "stpeter.im/atom.xml" // pol) (fromDb "stpeter.im/atom.xml" // pol)
(fromDb "thediff.co" // pol) # Byrne Hobart
# (fromDb "theregister.com" // tech)
(fromDb "thisweek.gnome.org" // tech) (fromDb "thisweek.gnome.org" // tech)
(fromDb "tuxphones.com" // tech) (fromDb "tuxphones.com" // tech)
(fromDb "uninsane.org" // tech) (fromDb "uninsane.org" // tech)
(fromDb "unintendedconsequenc.es" // rat) (fromDb "unintendedconsequenc.es" // rat)
# (fromDb "vitalik.ca" // tech) # moved to vitalik.eth.limo
(fromDb "vitalik.eth.limo" // tech) # Vitalik Buterin (fromDb "vitalik.eth.limo" // tech) # Vitalik Buterin
(fromDb "weekinethereumnews.com" // tech) # (fromDb "webcurious.co.uk" // uncat) # link aggregator; defunct?
(fromDb "willow.phantoma.online") # wizard@xyzzy.link (fromDb "willow.phantoma.online") # wizard@xyzzy.link
(fromDb "xn--gckvb8fzb.com" // tech) (fromDb "xn--gckvb8fzb.com" // tech)
(fromDb "xorvoid.com" // tech)
(fromDb "www.thebignewsletter.com" // pol)
(mkSubstack "astralcodexten" // rat // daily) # Scott Alexander (mkSubstack "astralcodexten" // rat // daily) # Scott Alexander
(mkSubstack "chlamchowder" // tech) # details CPU advancements # (mkSubstack "doomberg" // tech // weekly) # articles are all pay-walled
(mkSubstack "eliqian" // rat // weekly) (mkSubstack "eliqian" // rat // weekly)
(mkSubstack "oversharing" // pol // daily) (mkSubstack "oversharing" // pol // daily)
(mkSubstack "samkriss" // humor // infrequent) (mkSubstack "samkriss" // humor // infrequent)
(mkText "http://benjaminrosshoffman.com/feed" // pol // weekly) (mkText "http://benjaminrosshoffman.com/feed" // pol // weekly)
(mkText "http://boginjr.com/feed" // tech // infrequent) (mkText "http://boginjr.com/feed" // tech // infrequent)
(mkText "https://forum.merveilles.town/rss.xml" // pol // infrequent) #quality RSS list here: <https://forum.merveilles.town/thread/57/share-your-rss-feeds%21-6/> (mkText "https://forum.merveilles.town/rss.xml" // pol // infrequent) #quality RSS list here: <https://forum.merveilles.town/thread/57/share-your-rss-feeds%21-6/>
(mkText "https://icm.museum/rss20.xml" // tech // infrequent) # Interim Computer Museum # (mkText "https://github.com/Kaiteki-Fedi/Kaiteki/commits/master.atom" // tech // infrequent)
(mkText "https://jvns.ca/atom.xml" // tech // weekly) # Julia Evans (mkText "https://jvns.ca/atom.xml" // tech // weekly) # Julia Evans
(mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent) (mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent)
(mkText "https://nixos.org/blog/announcements-rss.xml" // tech // infrequent) # more nixos stuff here, but unclear how to subscribe: <https://nixos.org/blog/categories.html> (mkText "https://nixos.org/blog/announcements-rss.xml" // tech // infrequent) # more nixos stuff here, but unclear how to subscribe: <https://nixos.org/blog/categories.html>
(mkText "https://nixos.org/blog/stories-rss.xml" // tech // weekly) (mkText "https://nixos.org/blog/stories-rss.xml" // tech // weekly)
(mkText "https://solar.lowtechmagazine.com/posts/index.xml" // tech // weekly) (mkText "https://solar.lowtechmagazine.com/posts/index.xml" // tech // weekly)
(mkText "https://www.stratechery.com/rss" // pol // weekly) # Ben Thompson
# (fromDb "balajis.com" // pol) # Balaji
# (fromDb "drewdevault.com" // tech)
# (fromDb "econlib.org" // pol)
# (fromDb "lesswrong.com" // rat)
# (fromDb "profectusmag.com" // pol) # some conservative/libertarian think tank
# (fromDb "thediff.co" // pol) # Byrne Hobart; 80% is subscriber-only
# (fromDb "thesideview.co" // uncat) # spiritual journal; RSS items are stubs
# (fromDb "theregister.com" // tech)
# (fromDb "vitalik.ca" // tech) # moved to vitalik.eth.limo
# (fromDb "webcurious.co.uk" // uncat) # link aggregator; defunct?
# (mkSubstack "doomberg" // tech // weekly) # articles are all pay-walled
# (mkText "https://github.com/Kaiteki-Fedi/Kaiteki/commits/master.atom" // tech // infrequent)
# (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly) # (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly)
# (mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly) # Matt Levine (preview/paywalled) # (mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly) # Matt Levine (preview/paywalled)
(mkText "https://www.stratechery.com/rss" // pol // weekly) # Ben Thompson
]; ];
videos = [ videos = [
(fromDb "youtube.com/@Channel5YouTube" // pol) (fromDb "youtube.com/@Channel5YouTube" // pol)
(fromDb "youtube.com/@ColdFusion")
(fromDb "youtube.com/@ContraPoints" // pol) (fromDb "youtube.com/@ContraPoints" // pol)
(fromDb "youtube.com/@Exurb1a") (fromDb "youtube.com/@Exurb1a")
(fromDb "youtube.com/@hbomberguy") (fromDb "youtube.com/@hbomberguy")
(fromDb "youtube.com/@JackStauber") (fromDb "youtube.com/@JackStauber")
(fromDb "youtube.com/@jaketran")
(fromDb "youtube.com/@kurzgesagt")
(fromDb "youtube.com/@mii_beta" // tech) # Baby Wogue / gnome reviewer
(fromDb "youtube.com/@Matrixdotorg" // tech) # Matrix Live
(fromDb "youtube.com/@NativLang") (fromDb "youtube.com/@NativLang")
(fromDb "youtube.com/@PolyMatter") (fromDb "youtube.com/@PolyMatter")
(fromDb "youtube.com/@scenesbyben" // pol) # video essays
(fromDb "youtube.com/@TechnologyConnections" // tech)
(fromDb "youtube.com/@theodd1sout")
(fromDb "youtube.com/@TomScottGo")
(fromDb "youtube.com/@TVW_Washington" // pol) # interviews with WA public officials
(fromDb "youtube.com/@veritasium")
(fromDb "youtube.com/@Vihart")
(fromDb "youtube.com/@InnuendoStudios" // pol) # breaks down the nastier political strategies, from a "politics is power" angle
# (fromDb "youtube.com/@CasuallyExplained" // pol)
# (fromDb "youtube.com/@ColdFusion")
# (fromDb "youtube.com/@rossmanngroup" // pol // tech) # Louis Rossmann # (fromDb "youtube.com/@rossmanngroup" // pol // tech) # Louis Rossmann
# (fromDb "youtube.com/@TheB1M") (fromDb "youtube.com/@TechnologyConnections" // tech)
# (fromDb "youtube.com/@tested" // tech) # Adam Savage (uploads too frequently) (fromDb "youtube.com/@TheB1M")
# (fromDb "youtube.com/@Vox") (fromDb "youtube.com/@TomScottGo")
# (fromDb "youtube.com/@Vsauce") # they're all like 1-minute long videos now? what happened @Vsauce? (fromDb "youtube.com/@Vihart")
(fromDb "youtube.com/@Vox")
(fromDb "youtube.com/@Vsauce")
]; ];
images = [ images = [
(fromDb "catandgirl.com" // img // humor) (fromDb "catandgirl.com" // img // humor)
(fromDb "davidrevoy.com" // img // art) (fromDb "davidrevoy.com" // img // art)
(fromDb "grumpy.website" // img // humor)
(fromDb "miniature-calendar.com" // img // art // daily) (fromDb "miniature-calendar.com" // img // art // daily)
(fromDb "pbfcomics.com" // img // humor) (fromDb "pbfcomics.com" // img // humor)
(fromDb "poorlydrawnlines.com/feed" // img // humor) (fromDb "poorlydrawnlines.com/feed" // img // humor)

235
hosts/common/fs.nix Normal file
View File

@@ -0,0 +1,235 @@
# docs
# - x-systemd options: <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
# - fuse options: `man mount.fuse`
{ config, lib, pkgs, sane-lib, utils, ... }:
let
fsOpts = rec {
common = [
"_netdev"
"noatime"
# user: allow any user with access to the device to mount the fs.
# note that this requires a suid `mount` binary; see: <https://zameermanji.com/blog/2022/8/5/using-fuse-without-root-on-linux/>
"user"
"x-systemd.requires=network-online.target"
"x-systemd.after=network-online.target"
"x-systemd.mount-timeout=10s" # how long to wait for mount **and** how long to wait for unmount
];
# x-systemd.automount: mount the fs automatically *on first access*.
# creates a `path-to-mount.automount` systemd unit.
automount = [ "x-systemd.automount" ];
# noauto: don't mount as part of remote-fs.target.
# N.B.: `remote-fs.target` is a dependency of multi-user.target, itself of graphical.target.
# hence, omitting `noauto` can slow down boots.
noauto = [ "noauto" ];
# lazyMount: defer mounting until first access from userspace.
# see: `man systemd.automount`, `man automount`, `man autofs`
lazyMount = noauto ++ automount;
wg = [
"x-systemd.requires=wireguard-wg-home.service"
"x-systemd.after=wireguard-wg-home.service"
];
fuse = [
"allow_other" # allow users other than the one who mounts it to access it. needed, if systemd is the one mounting this fs (as root)
# allow_root: allow root to access files on this fs (if mounted by non-root, else it can always access them).
# N.B.: if both allow_root and allow_other are specified, then only allow_root takes effect.
# "allow_root"
# default_permissions: enforce local permissions check. CRUCIAL if using `allow_other`.
# w/o this, permissions mode of sshfs is like:
# - sshfs runs all remote commands as the remote user.
# - if a local user has local permissions to the sshfs mount, then their file ops are sent blindly across the tunnel.
# - `allow_other` allows *any* local user to access the mount, and hence any local user can now freely become the remote mapped user.
# with default_permissions, sshfs doesn't tunnel file ops from users until checking that said user could perform said op on an equivalent local fs.
"default_permissions"
];
fuseColin = fuse ++ [
"uid=1000"
"gid=100"
];
ssh = common ++ fuse ++ [
"identityfile=/home/colin/.ssh/id_ed25519"
# i *think* idmap=user means that `colin` on `localhost` and `colin` on the remote are actually treated as the same user, even if their uid/gid differs?
# i.e., local colin's id is translated to/from remote colin's id on every operation?
"idmap=user"
];
sshColin = ssh ++ fuseColin ++ [
# follow_symlinks: remote files which are symlinks are presented to the local system as ordinary files (as the target of the symlink).
# if the symlink target does not exist, the presentation is unspecified.
# symlinks which point outside the mount ARE followed. so this is more capable than `transform_symlinks`
"follow_symlinks"
# symlinks on the remote fs which are absolute paths are presented to the local system as relative symlinks pointing to the expected data on the remote fs.
# only symlinks which would point inside the mountpoint are translated.
"transform_symlinks"
];
# sshRoot = ssh ++ [
# # we don't transform_symlinks because that breaks the validity of remote /nix stores
# "sftp_server=/run/wrappers/bin/sudo\\040/run/current-system/sw/libexec/sftp-server"
# ];
# in the event of hunt NFS mounts, consider:
# - <https://unix.stackexchange.com/questions/31979/stop-broken-nfs-mounts-from-locking-a-directory>
# NFS options: <https://linux.die.net/man/5/nfs>
# actimeo=n = how long (in seconds) to cache file/dir attributes (default: 3-60s)
# bg = retry failed mounts in the background
# retry=n = for how many minutes `mount` will retry NFS mount operation
# intr = allow Ctrl+C to abort I/O (it will error with `EINTR`)
# soft = on "major timeout", report I/O error to userspace
# softreval = on "major timeout", service the request using known-stale cache results instead of erroring -- if such cache data exists
# retrans=n = how many times to retry a NFS request before giving userspace a "server not responding" error (default: 3)
# timeo=n = number of *deciseconds* to wait for a response before retrying it (default: 600)
# note: client uses a linear backup, so the second request will have double this timeout, then triple, etc.
# proto=udp = encapsulate protocol ops inside UDP packets instead of a TCP session.
# requires `nfsvers=3` and a kernel compiled with `NFS_DISABLE_UDP_SUPPORT=n`.
# UDP might be preferable to TCP because the latter is liable to hang for ~100s (kernel TCP timeout) after a link drop.
# however, even UDP has issues with `umount` hanging.
#
# N.B.: don't change these without first testing the behavior of sandboxed apps on a flaky network.
nfs = common ++ [
# "actimeo=5"
# "bg"
"retrans=1"
"retry=0"
# "intr"
"soft"
"softreval"
"timeo=30"
"nofail" # don't fail remote-fs.target when this mount fails (not an option for sshfs else would be common)
# "proto=udp" # default kernel config doesn't support NFS over UDP: <https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1964093> (see comment 11).
# "nfsvers=3" # NFSv4+ doesn't support UDP at *all*. it's ok to omit nfsvers -- server + client will negotiate v3 based on udp requirement. but omitting causes confusing mount errors when the server is *offline*, because the client defaults to v4 and thinks the udp option is a config error.
# "x-systemd.idle-timeout=10" # auto-unmount after this much inactivity
];
# manually perform a ftp mount via e.g.
# curlftpfs -o ftpfs_debug=2,user=anonymous:anonymous,connect_timeout=10 -f -s ftp://servo-hn /mnt/my-ftp
ftp = common ++ fuseColin ++ [
# "ftpfs_debug=2"
"user=colin:ipauth"
"connect_timeout=10"
];
};
remoteHome = host: {
sane.programs.sshfs-fuse.enableFor.system = true;
fileSystems."/mnt/${host}/home" = {
device = "colin@${host}:/home/colin";
fsType = "fuse.sshfs";
options = fsOpts.sshColin ++ fsOpts.lazyMount;
noCheck = true;
};
sane.fs."/mnt/${host}/home" = sane-lib.fs.wanted {
dir.acl.user = "colin";
dir.acl.group = "users";
dir.acl.mode = "0700";
};
};
remoteServo = subdir: {
sane.programs.curlftpfs.enableFor.system = true;
sane.fs."/mnt/servo/${subdir}" = sane-lib.fs.wanted {
dir.acl.user = "colin";
dir.acl.group = "users";
dir.acl.mode = "0750";
};
fileSystems."/mnt/servo/${subdir}" = {
device = "ftp://servo-hn:/${subdir}";
noCheck = true;
fsType = "fuse.curlftpfs";
options = fsOpts.ftp ++ fsOpts.noauto ++ fsOpts.wg;
# fsType = "nfs";
# options = fsOpts.nfs ++ fsOpts.lazyMount ++ fsOpts.wg;
};
systemd.services."automount-servo-${utils.escapeSystemdPath subdir}" = let
fs = config.fileSystems."/mnt/servo/${subdir}";
in {
# this is a *flaky* network mount, especially on moby.
# if done as a normal autofs mount, access will eternally block when network is dropped.
# notably, this would block *any* sandboxed app which allows media access, whether they actually try to use that media or not.
# a practical solution is this: mount as a service -- instead of autofs -- and unmount on timeout error, in a restart loop.
# until the ftp handshake succeeds, nothing is actually mounted to the vfs, so this doesn't slow down any I/O when network is down.
description = "automount /mnt/servo/${subdir} in a fault-tolerant and non-blocking manner";
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
wantedBy = [ "default.target" ];
serviceConfig.Type = "simple";
serviceConfig.ExecStart = lib.escapeShellArgs [
"/usr/bin/env"
"PATH=/run/current-system/sw/bin"
"mount.${fs.fsType}"
"-f" # foreground (i.e. don't daemonize)
"-s" # single-threaded (TODO: it's probably ok to disable this?)
"-o"
(lib.concatStringsSep "," (lib.filter (o: !lib.hasPrefix "x-systemd." o) fs.options))
fs.device
"/mnt/servo/${subdir}"
];
# not sure if this configures a linear, or exponential backoff.
# but the first restart will be after `RestartSec`, and the n'th restart (n = RestartSteps) will be RestartMaxDelaySec after the n-1'th exit.
serviceConfig.Restart = "always";
serviceConfig.RestartSec = "10s";
serviceConfig.RestartMaxDelaySec = "120s";
serviceConfig.RestartSteps = "5";
};
};
in
lib.mkMerge [
{
# some services which use private directories error if the parent (/var/lib/private) isn't 700.
sane.fs."/var/lib/private".dir.acl.mode = "0700";
# in-memory compressed RAM
# defaults to compressing at most 50% size of RAM
# claimed compression ratio is about 2:1
# - but on moby w/ zstd default i see 4-7:1 (ratio lowers as it fills)
# note that idle overhead is about 0.05% of capacity (e.g. 2B per 4kB page)
# docs: <https://www.kernel.org/doc/Documentation/blockdev/zram.txt>
#
# to query effectiveness:
# `cat /sys/block/zram0/mm_stat`. whitespace separated fields:
# - *orig_data_size* (bytes)
# - *compr_data_size* (bytes)
# - mem_used_total (bytes)
# - mem_limit (bytes)
# - mem_used_max (bytes)
# - *same_pages* (pages which are e.g. all zeros (consumes no additional mem))
# - *pages_compacted* (pages which have been freed thanks to compression)
# - huge_pages (incompressible)
#
# see also:
# - `man zramctl`
zramSwap.enable = true;
# how much ram can be swapped into the zram device.
# this shouldn't be higher than the observed compression ratio.
# the default is 50% (why?)
# 100% should be "guaranteed" safe so long as the data is even *slightly* compressible.
# but it decreases working memory under the heaviest of loads by however much space the compressed memory occupies (e.g. 50% if 2:1; 25% if 4:1)
zramSwap.memoryPercent = 100;
# environment.pathsToLink = [
# # needed to achieve superuser access for user-mounted filesystems (see sshRoot above)
# # we can only link whole directories here, even though we're only interested in pkgs.openssh
# "/libexec"
# ];
programs.fuse.userAllowOther = true; #< necessary for `allow_other` or `allow_root` options.
}
(remoteHome "desko")
(remoteHome "lappy")
(remoteHome "moby")
# this granularity of servo media mounts is necessary to support sandboxing:
# for flaky mounts, we can only bind the mountpoint itself into the sandbox,
# so it's either this or unconditionally bind all of media/.
(remoteServo "media/archive")
(remoteServo "media/Books")
(remoteServo "media/collections")
# (remoteServo "media/datasets")
(remoteServo "media/freeleech")
(remoteServo "media/games")
(remoteServo "media/Music")
(remoteServo "media/Pictures/macros")
(remoteServo "media/Videos")
(remoteServo "playground")
]

Some files were not shown because too many files have changed in this diff Show More