Compare commits
1 Commits
master
...
wip-kaitek
Author | SHA1 | Date | |
---|---|---|---|
5152159691 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,2 @@
|
||||
/build
|
||||
/.working
|
||||
result
|
||||
result-*
|
||||
/secrets/local.nix
|
||||
|
64
.sops.yaml
64
.sops.yaml
@@ -1,64 +0,0 @@
|
||||
keys:
|
||||
- &user_desko_colin age1tnl4jfgacwkargzeqnhzernw29xx8mkv73xh6ufdyde6q7859slsnzf24x
|
||||
- &user_flowy_colin age1nw3z25gn6l8gxneqw43tp8d2354c83d9sn3r0dqy5tapakdwhyvse0j2cc
|
||||
- &user_lappy_colin age1j2pqnl8j0krdzk6npe93s4nnqrzwx978qrc0u570gzlamqpnje9sc8le2g
|
||||
- &user_servo_colin age1z8fauff34cdecr6sjkre260luzxcca05kpcwvhx988d306tpcejsp63znu
|
||||
- &user_moby_colin age1zsrsvd7j6l62fjxpfd2qnhqlk8wk4p8r0dtxpe4sdgnh2474095qdu7xj9
|
||||
- &host_crappy age1hl50ufuxnqy0jnk8fqeu4tclh4vte2xn2d59pxff0gun20vsmv5sp78chj
|
||||
- &host_desko age1vnw7lnfpdpjn62l3u5nyv5xt2c965k96p98kc43mcnyzpetrts9q54mc9v
|
||||
- &host_flowy age1azm6carlm6tdjup37u5dr40585vjujajev70u4glwd9sv7swa99sk6mswx
|
||||
- &host_lappy age1w7mectcjku6x3sd8plm8wkn2qfrhv9n6zhzlf329e2r2uycgke8qkf9dyn
|
||||
- &host_servo age1tzlyex2z6t88tg9h82943e39shxhmqeyr7ywhlwpdjmyqsndv3qq27x0rf
|
||||
- &host_moby age18vq5ktwgeaysucvw9t67drqmg5zd5c5k3le34yqxckkfj7wqdqgsd4ejmt
|
||||
creation_rules:
|
||||
- path_regex: secrets/common*
|
||||
key_groups:
|
||||
- age:
|
||||
- *user_desko_colin
|
||||
- *user_flowy_colin
|
||||
- *user_lappy_colin
|
||||
- *user_servo_colin
|
||||
- *user_moby_colin
|
||||
- *host_crappy
|
||||
- *host_desko
|
||||
- *host_flowy
|
||||
- *host_lappy
|
||||
- *host_servo
|
||||
- *host_moby
|
||||
- path_regex: secrets/servo*
|
||||
key_groups:
|
||||
- age:
|
||||
- *user_desko_colin
|
||||
- *user_flowy_colin
|
||||
- *user_lappy_colin
|
||||
- *user_servo_colin
|
||||
- *host_servo
|
||||
- path_regex: secrets/desko*
|
||||
key_groups:
|
||||
- age:
|
||||
- *user_desko_colin
|
||||
- *user_flowy_colin
|
||||
- *user_lappy_colin
|
||||
- *host_desko
|
||||
- path_regex: secrets/flowy*
|
||||
key_groups:
|
||||
- age:
|
||||
- *user_lappy_colin
|
||||
- *user_flowy_colin
|
||||
- *user_desko_colin
|
||||
- *host_flowy
|
||||
- path_regex: secrets/lappy*
|
||||
key_groups:
|
||||
- age:
|
||||
- *user_lappy_colin
|
||||
- *user_flowy_colin
|
||||
- *user_desko_colin
|
||||
- *host_lappy
|
||||
- path_regex: secrets/moby*
|
||||
key_groups:
|
||||
- age:
|
||||
- *user_desko_colin
|
||||
- *user_flowy_colin
|
||||
- *user_lappy_colin
|
||||
- *user_moby_colin
|
||||
- *host_moby
|
125
README.md
125
README.md
@@ -1,125 +0,0 @@
|
||||

|
||||
|
||||
# .❄️≡We|_c0m3 7o m`/ f14k≡❄️.
|
||||
|
||||
(er, it's not a flake anymore. welcome to my nix files.)
|
||||
|
||||
## What's Here
|
||||
|
||||
this is the top-level repo from which i configure/deploy all my NixOS machines:
|
||||
- desktop
|
||||
- laptop
|
||||
- server
|
||||
- mobile phone (Pinephone)
|
||||
|
||||
everything outside of [hosts/](./hosts/) and [secrets/](./secrets/) is intended for export, to be importable for use by 3rd parties.
|
||||
the only hard dependency for my exported pkgs/modules should be [nixpkgs][nixpkgs].
|
||||
building [hosts/](./hosts/) will require [sops][sops].
|
||||
|
||||
you might specifically be interested in these files (elaborated further in #key-points-of-interest):
|
||||
- [my packages](./pkgs/by-name)
|
||||
- [my implementation of impermanence](./modules/persist/default.nix)
|
||||
- my way of deploying dotfiles/configuring programs per-user:
|
||||
- [modules/fs/](./modules/fs/default.nix)
|
||||
- [modules/programs/](./modules/programs/default.nix)
|
||||
- [modules/users/](./modules/users/default.nix)
|
||||
|
||||
if you find anything here genuinely useful, message me so that i can work to upstream it!
|
||||
|
||||
[nixpkgs]: https://github.com/NixOS/nixpkgs
|
||||
[sops]: https://github.com/Mic92/sops-nix
|
||||
[uninsane-org]: https://uninsane.org
|
||||
|
||||
|
||||
## Using This Repo In Your Own Config
|
||||
|
||||
follow the instructions [here][NUR] to access my packages through the Nix User Repositories.
|
||||
|
||||
[NUR]: https://nur.nix-community.org/
|
||||
|
||||
|
||||
## Layout
|
||||
- `doc/`
|
||||
- instructions for tasks i find myself doing semi-occasionally in this repo.
|
||||
- `hosts/`
|
||||
- configs which aren't factored with external use in mind.
|
||||
- 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.
|
||||
- `integrations/`
|
||||
- code intended for consumption by external tools (e.g. the Nix User Repos).
|
||||
- `modules/`
|
||||
- config which is gated behind `enable` flags, in similar style to nixpkgs' `nixos/` directory.
|
||||
- if you depend on this repo for anything besides packages, it's most likely for something in this directory.
|
||||
- `overlays/`
|
||||
- predominantly a list of `callPackage` directives.
|
||||
- `pkgs/`
|
||||
- derivations for things not yet packaged in nixpkgs.
|
||||
- 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
|
||||
that are highly specific to my setup).
|
||||
- `scripts/`
|
||||
- scripts which aren't reachable on a deployed system, but may aid manual deployments.
|
||||
- `secrets/`
|
||||
- encrypted keys, API tokens, anything which one or more of my machines needs
|
||||
read access to but shouldn't be world-readable.
|
||||
- not much to see here.
|
||||
- `templates/`
|
||||
- used to instantiate short-lived environments.
|
||||
- used to auto-fill the boiler-plate portions of new packages.
|
||||
|
||||
|
||||
## Key Points of Interest
|
||||
|
||||
i.e. you might find value in using these in your own config:
|
||||
|
||||
- `modules/fs/`
|
||||
- use this to statically define leafs and nodes anywhere in the filesystem,
|
||||
not just inside `/nix/store`.
|
||||
- e.g. specify that `/var/www` should be:
|
||||
- owned by a specific user/group
|
||||
- set to a specific mode
|
||||
- symlinked to some other path
|
||||
- populated with some statically-defined data
|
||||
- populated according to some script
|
||||
- created as a dependency of some service (e.g. `nginx`)
|
||||
- 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.
|
||||
namely, it avoids any custom activation scripts by leveraging `systemd-tmpfiles`.
|
||||
- `modules/persist/`
|
||||
- my implementation of impermanence, built atop the above `fs` module, with a few notable features:
|
||||
- no custom activation scripts or services (uses `systemd-tmpfiles` and `.mount` units)
|
||||
- "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.
|
||||
- persist to encrypted storage which is unlocked at login time.
|
||||
- `modules/programs/`
|
||||
- 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:
|
||||
- e.g. `/home/<user>/.mozilla/firefox` is persisted only for users who
|
||||
`sane.programs.firefox.enableFor.user."<user>" = true;`
|
||||
- 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.whitelistWayland = true; # allow it to render a wayland window`
|
||||
- `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.
|
||||
- `modules/users/`
|
||||
- convenience layer atop the above modules so that you can just write
|
||||
`fs.".config/git"` instead of `fs."/home/colin/.config/git"`
|
||||
- simplified `systemd.services` API
|
||||
|
||||
[home-manager]: https://github.com/nix-community/home-manager
|
||||
|
||||
|
||||
## Mirrors
|
||||
|
||||
this repo exists in a few known locations:
|
||||
- primary: <https://git.uninsane.org/colin/nix-files>
|
||||
- mirror: <https://github.com/nix-community/nur-combined/tree/master/repos/colinsane>
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
if you want to contact me for questions, or collaborate to split something useful into a shared repo, etc,
|
||||
you can reach me via any method listed [here](https://uninsane.org/about).
|
||||
patches, for this repo or any other i host, will be warmly welcomed in any manner you see fit:
|
||||
`git send-email`, DM'ing the patch over Matrix/Lemmy/ActivityPub/etc, even a literal PR where you
|
||||
link me to your own clone.
|
194
TODO.md
194
TODO.md
@@ -1,181 +1,21 @@
|
||||
## BUGS
|
||||
- alacritty Ctrl+N frequently fails to `cd` to the previous directory
|
||||
- bunpen dbus sandboxing can't be *nested* (likely a problem in xdg-dbus-proxy)
|
||||
- dissent has a memory leak (3G+ after 24hr)
|
||||
- set a max memory use in the systemd service, to force it to restart as it leaks?
|
||||
- `rmDbusServices` may break sandboxing
|
||||
- e.g. if the package ships a systemd unit which references $out, then make-sandboxed won't properly update that unit.
|
||||
- `rmDbusServicesInPlace` is not affected
|
||||
- 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)
|
||||
# features/tweaks
|
||||
- enable sshfs (deskto/lappy)
|
||||
- set firefox default search engine
|
||||
- iron out video drivers
|
||||
|
||||
## REFACTORING:
|
||||
- fold hosts/modules/ into toplevel modules/
|
||||
- add import checks to my Python nix-shell scripts
|
||||
- consolidate ~/dev and ~/ref
|
||||
- ~/dev becomes a link to ~/ref/cat/mine
|
||||
- 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
|
||||
- user secrets could just use `gocryptfs`, like with ~/private?
|
||||
- can gocryptfs support nested filesystems, each with different perms (for desko, moby, etc)?
|
||||
|
||||
### upstreaming
|
||||
- upstream blueprint-compiler cross fixes -> nixpkgs
|
||||
- upstream cargo cross fixes -> nixpkgs
|
||||
- upstream `gps-share` package -> nixpkgs
|
||||
|
||||
#### 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>
|
||||
- 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)
|
||||
# cleanup
|
||||
- remove helpers from outputs section (use `let .. in`)
|
||||
|
||||
|
||||
## IMPROVEMENTS:
|
||||
- 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...
|
||||
# speed up cross compiling
|
||||
https://nixos.wiki/wiki/Cross_Compiling
|
||||
https://nixos.wiki/wiki/NixOS_on_ARM
|
||||
overlays = [{ ... }: {
|
||||
nixpkgs.crossSystem.system = "aarch64-linux";
|
||||
}];
|
||||
|
||||
### security/resilience
|
||||
- /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)
|
||||
- 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
|
||||
- sandbox `nix`
|
||||
- enforce that all `environment.packages` has a sandbox profile (or explicitly opts out)
|
||||
- enforce granular dbus sandboxing (bunpen-dbus-*)
|
||||
- make gnome-keyring-daemon less monolithic
|
||||
- no reason every application with _a_ secret needs to see _all_ secrets
|
||||
- check out oo7-daemon?
|
||||
- also unix-pass based provider: <https://github.com/mdellweg/pass_secret_service>
|
||||
- make dconf stuff less monolithic
|
||||
- i.e. per-app dconf profiles for those which need it. possible static config.
|
||||
- flatpak/spectrum has some stuff to proxy dconf per-app
|
||||
- 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.
|
||||
|
||||
### user experience
|
||||
- setup a real calendar system, for recurring events
|
||||
- rofi: sort items case-insensitively
|
||||
- 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
|
||||
- watch `forkstat`: it does way too much
|
||||
- cleanup nwg-panel so that it's not invoking swaync every second
|
||||
- nwg-panel: doesn't know that virtual-desktop 10/TV exists
|
||||
- install apps:
|
||||
- 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/>
|
||||
- offline Wikipedia (or, add to `wike`)
|
||||
- some type of games manager/launcher
|
||||
- Gnome Highscore (retro games)?: <https://gitlab.gnome.org/World/highscore>
|
||||
- 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
|
||||
- i.e. mock joysticks, for use with SuperTux and SuperTuxKart
|
||||
- game: Hedgewars
|
||||
- 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>
|
||||
- UnCiv (Civ V clone; nixpkgs `unciv`; doesn't cross-compile): <https://github.com/yairm210/UnCiv>
|
||||
- Simon Tatham's Puzzle Collection (not in nixpkgs) <https://git.tartarus.org/?p=simon/puzzles.git>
|
||||
- Shootin Stars (Godot; not in nixpkgs) <https://gitlab.com/greenbeast/shootin-stars>
|
||||
- numberlink (generic name for Flow Free). not packaged in Nix
|
||||
- Neverball (https://neverball.org/screenshots.php). nix: as `neverball`
|
||||
- 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: 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 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
|
||||
- SwayNC/nwg-panel: add option to change audio output
|
||||
- Newsflash: sync OPML on start, same way i do with gpodder
|
||||
- better podcasting client?
|
||||
- hardware upgrade (OnePlus)?
|
||||
|
||||
#### non-moby
|
||||
- RSS: integrate a paywall bypass
|
||||
- e.g. self-hosted [ladder](https://github.com/everywall/ladder) (like 12ft.io)
|
||||
- RSS: have podcasts get downloaded straight into ~/Videos/...
|
||||
- and strip the ads out using Whisper transcription + asking a LLM where the ad breaks are
|
||||
- neovim: integrate ollama
|
||||
- neovim: better docsets (e.g. c++, glib)
|
||||
- firefox: persist history
|
||||
- just not cookies or tabs
|
||||
- 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
|
||||
- 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)
|
||||
- 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
|
||||
- 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
|
||||
|
||||
## NEW FEATURES:
|
||||
- migrate Kodi box to nix
|
||||
- migrate MAME cabinet to nix
|
||||
- boot it from PXE from servo?
|
||||
- enable IPv6
|
||||
# better secrets management? read:
|
||||
- decrypted at activation time: https://github.com/Mic92/sops-nix
|
||||
less promising:
|
||||
- https://christine.website/blog/nixos-encrypted-secrets-2021-01-20
|
||||
- git-crypt (https://github.com/bobbbay/dotfiles.git)
|
||||
|
25
configuration.nix
Normal file
25
configuration.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
# Edit this configuration file to define what should be installed on
|
||||
# your system. Help is available in the configuration.nix(5) man page
|
||||
# and in the NixOS manual (accessible by running ‘nixos-help’).
|
||||
|
||||
# USEFUL COMMANDS:
|
||||
# nix show-config
|
||||
# nix eval --raw <expr> => print an expression. e.g. nixpkgs.raspberrypifw prints store path to the package
|
||||
# nix-option ## query options -- including their SET VALUE; similar to search: https://search.nixos.org/options
|
||||
# nixos-rebuild switch --upgrade ## pull changes from the nixos channel (e.g. security updates) and rebuild
|
||||
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
|
||||
# enable flake support.
|
||||
# the real config root lives in flake.nix
|
||||
nix = {
|
||||
#package = pkgs.nixFlakes;
|
||||
package = pkgs.nixUnstable;
|
||||
extraOptions = ''
|
||||
experimental-features = nix-command flakes
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{ ... }@args:
|
||||
let
|
||||
sane-nix-files = import ./pkgs/by-name/sane-nix-files/package.nix { };
|
||||
in
|
||||
import "${sane-nix-files}/impure.nix" args
|
@@ -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
|
@@ -1,13 +0,0 @@
|
||||
to ship `pkgs.foo` on some host, either:
|
||||
- add it as an entry in `suggestedPrograms` to the appropriate category in `hosts/common/programs/assorted.nix`, or
|
||||
- `sane.programs.foo.enableFor.user.colin = true` in `hosts/by-name/myhost/default.nix`
|
||||
|
||||
if the program needs customization (persistence, configs, secrets):
|
||||
- add a file for it at `hosts/common/programs/<foo>.nix`
|
||||
- set the options, `sane.programs.foo.{fs,persist}`
|
||||
|
||||
if it's unclear what fs paths a program uses:
|
||||
- run one of these commands, launch the program, run it again, and `diff`:
|
||||
- `du -x --apparent-size ~`
|
||||
- `find ~ -xdev`
|
||||
- or, inspect the whole tmpfs root with `ncdu -x /`
|
BIN
doc/hello.gif
BIN
doc/hello.gif
Binary file not shown.
Before Width: | Height: | Size: 127 KiB |
@@ -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!
|
@@ -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`
|
81
flake.lock
generated
Normal file
81
flake.lock
generated
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"nodes": {
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1654113405,
|
||||
"narHash": "sha256-VpK+0QaWG2JRgB00lw77N9TjkE3ec0iMYIX1TzGpxa4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "ac2287df5a2d6f0a44bbcbd11701dbbf6ec43675",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-22.05",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"mobile-nixos": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1654281294,
|
||||
"narHash": "sha256-hT2/u0jUOD4TFU6YyYt+5Gt+hjIeerLTyZG7ru79aDU=",
|
||||
"owner": "nixos",
|
||||
"repo": "mobile-nixos",
|
||||
"rev": "d798b0b34240b18a08c22f5c0ee1f59a3ce43c01",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "mobile-nixos",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1654275867,
|
||||
"narHash": "sha256-pt14ZE4jVPGvfB2NynGsl34pgXfOqum5YJNpDK4+b9E=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7a20c208aacf4964c19186dcad51f89165dc7ed0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-22.05",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nurpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1654367137,
|
||||
"narHash": "sha256-xufB/+qvk/7rh7qrwZbzru1kTu8nsmNWBNQkYbdS84Q=",
|
||||
"owner": "nix-community",
|
||||
"repo": "NUR",
|
||||
"rev": "86ff2d098bce1d623232f4886027a1d61317b195",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "NUR",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"home-manager": "home-manager",
|
||||
"mobile-nixos": "mobile-nixos",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nurpkgs": "nurpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
116
flake.nix
Normal file
116
flake.nix
Normal file
@@ -0,0 +1,116 @@
|
||||
# docs:
|
||||
# https://nixos.wiki/wiki/Flakes
|
||||
# https://serokell.io/blog/practical-nix-flakes
|
||||
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-22.05";
|
||||
# pkgs-telegram.url = "nixpkgs/33775ec9a2173a08e46edf9f46c9febadbf743e8";# 2022/04/18; telegram 3.7.3. fails: nix log /nix/store/y5kv47hnv55qknb6cnmpcyraicay79fx-telegram-desktop-3.7.3.drv: g++: fatal error: cannot execute '/nix/store/njk5sbd21305bhr7gwibxbbvgbx5lxvn-gcc-9.3.0/libexec/gcc/aarch64-unknown-linux-gnu/9.3.0/cc1plus': execv: No such file or directory
|
||||
mobile-nixos = {
|
||||
url = "github:nixos/mobile-nixos";
|
||||
flake = false;
|
||||
};
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager/release-22.05";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
nurpkgs.url = "github:nix-community/NUR";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, mobile-nixos, home-manager, nurpkgs }: {
|
||||
machines.uninsane = self.decl-bootable-machine { name = "uninsane"; system = "aarch64-linux"; };
|
||||
machines.desko = self.decl-bootable-machine { name = "desko"; system = "x86_64-linux"; };
|
||||
machines.lappy = self.decl-bootable-machine { name = "lappy"; system = "x86_64-linux"; };
|
||||
|
||||
machines.moby =
|
||||
let machine = self.decl-machine {
|
||||
name = "moby";
|
||||
system = "aarch64-linux";
|
||||
extraModules = [
|
||||
(import "${mobile-nixos}/lib/configuration.nix" {
|
||||
device = "pine64-pinephone";
|
||||
})
|
||||
];
|
||||
};
|
||||
in {
|
||||
nixosConfiguration = machine;
|
||||
img = machine.config.mobile.outputs.u-boot.disk-image;
|
||||
};
|
||||
|
||||
nixosConfigurations = builtins.mapAttrs (name: value: value.nixosConfiguration) self.machines;
|
||||
imgs = builtins.mapAttrs (name: value: value.img) self.machines;
|
||||
|
||||
decl-machine = { name, system, extraModules ? [], basePkgs ? nixpkgs }: let
|
||||
patchedPkgs = basePkgs.legacyPackages.${system}.applyPatches {
|
||||
name = "nixpkgs-patched-uninsane";
|
||||
src = basePkgs;
|
||||
patches = [
|
||||
# for mobile: allow phoc to scale to non-integer values
|
||||
./nixpatches/01-phosh-float-scale.patch
|
||||
# for raspberry pi: allow building u-boot for rpi 4{,00}
|
||||
./nixpatches/02-rpi4-uboot.patch
|
||||
./nixpatches/03-whalebird-4.6.0.patch
|
||||
./nixpatches/04-dart-2.7.0.patch
|
||||
];
|
||||
};
|
||||
nixosSystem = import (patchedPkgs + "/nixos/lib/eval-config.nix");
|
||||
in (nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit home-manager; inherit nurpkgs; secrets = import ./secrets/default.nix; };
|
||||
modules = [
|
||||
./configuration.nix
|
||||
./machines/${name}
|
||||
(import ./helpers/set-hostname.nix name)
|
||||
(self.overlaysModule system)
|
||||
] ++ extraModules;
|
||||
});
|
||||
|
||||
# this produces a EFI-bootable .img file (GPT with / and /boot).
|
||||
# after building this, steps are:
|
||||
# run `btrfs-convert --uuid copy <device>`
|
||||
# boot, checkout this flake into /etc/nixos AND UPDATE THE UUIDS IT REFERENCES.
|
||||
# then `nixos-rebuild ...`
|
||||
decl-img = { name, system, extraModules ? [] }: (
|
||||
(self.decl-machine { inherit name; inherit system; extraModules = extraModules ++ [./image.nix]; })
|
||||
.config.system.build.raw
|
||||
);
|
||||
|
||||
decl-bootable-machine = { name, system }: {
|
||||
nixosConfiguration = self.decl-machine { inherit name; inherit system; };
|
||||
img = self.decl-img { inherit name; inherit system; };
|
||||
};
|
||||
|
||||
overlaysModule = system: { config, pkgs, ...}: {
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
nixpkgs.overlays = [
|
||||
#mobile-nixos.overlay
|
||||
nurpkgs.overlay
|
||||
(next: prev: {
|
||||
#### customized packages
|
||||
# nixos-unstable pleroma is too far out-of-date for our db
|
||||
pleroma = prev.callPackage ./pkgs/pleroma { };
|
||||
# jackett doesn't allow customization of the bind address: this will probably always be here.
|
||||
jackett = next.callPackage ./pkgs/jackett { pkgs = prev; };
|
||||
# fix abrupt HDD poweroffs as during reboot. patching systemd requires rebuilding nearly every package.
|
||||
# systemd = import ./pkgs/systemd { pkgs = prev; };
|
||||
|
||||
# patch rpi uboot with something that fixes USB HDD boot
|
||||
ubootRaspberryPi4_64bit = next.callPackage ./pkgs/ubootRaspberryPi4_64bit { pkgs = prev; };
|
||||
|
||||
#### TEMPORARY NIXOS-UNSTABLE PACKAGES
|
||||
|
||||
# stable telegram doesn't build, so explicitly use the stable one.
|
||||
# TODO: apply this specifically to the moby build?
|
||||
# tdesktop = pkgs-telegram.legacyPackages.${system}.tdesktop;
|
||||
tdesktop = nixpkgs.legacyPackages.${system}.tdesktop;
|
||||
|
||||
#### TEMPORARY: PACKAGES WAITING TO BE UPSTREAMED
|
||||
whalebird = prev.callPackage ./pkgs/whalebird { };
|
||||
kaiteki = prev.callPackage ./pkgs/kaiteki { };
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
13
helpers/gui/gnome.nix
Normal file
13
helpers/gui/gnome.nix
Normal file
@@ -0,0 +1,13 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# start gnome/gdm on boot
|
||||
services.xserver.enable = true;
|
||||
services.xserver.desktopManager.gnome.enable = true;
|
||||
services.xserver.displayManager.gdm.enable = true;
|
||||
|
||||
# gnome does networking stuff with networkmanager
|
||||
networking.useDHCP = false;
|
||||
networking.networkmanager.enable = true;
|
||||
networking.wireless.enable = lib.mkForce false;
|
||||
}
|
16
helpers/gui/i3.nix
Normal file
16
helpers/gui/i3.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.pathsToLink = [ "/libexec" ]; # patch for i3blocks to work
|
||||
services.xserver.enable = true;
|
||||
services.xserver.displayManager.defaultSession = "none+i3";
|
||||
services.xserver.windowManager.i3 = {
|
||||
enable = true;
|
||||
extraPackages = with pkgs; [
|
||||
dmenu
|
||||
i3status
|
||||
i3lock
|
||||
i3blocks
|
||||
];
|
||||
};
|
||||
}
|
||||
|
21
helpers/gui/phosh.nix
Normal file
21
helpers/gui/phosh.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{ ... }:
|
||||
{
|
||||
# docs: https://github.com/NixOS/nixpkgs/blob/nixos-22.05/nixos/modules/services/x11/desktop-managers/phosh.nix
|
||||
services.xserver.desktopManager.phosh = {
|
||||
enable = true;
|
||||
user = "colin";
|
||||
group = "users";
|
||||
phocConfig = {
|
||||
xwayland = "true";
|
||||
# find default outputs by catting /etc/phosh/phoc.ini
|
||||
outputs.DSI-1 = {
|
||||
scale = 1.5;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.variables = {
|
||||
# Qt apps won't always start unless this env var is set
|
||||
QT_QPA_PLATFORM = "wayland";
|
||||
};
|
||||
}
|
14
helpers/gui/plasma-mobile.nix
Normal file
14
helpers/gui/plasma-mobile.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# start plasma-mobile on boot
|
||||
services.xserver.enable = true;
|
||||
services.xserver.desktopManager.plasma5.mobile.enable = true;
|
||||
services.xserver.desktopManager.plasma5.mobile.installRecommendedSoftware = false; # not all plasma5-mobile packages build for aarch64
|
||||
services.xserver.displayManager.sddm.enable = true;
|
||||
|
||||
# Plasma does networking stuff with networkmanager, but nix configures the defaults itself
|
||||
# networking.useDHCP = false;
|
||||
# networking.networkmanager.enable = true;
|
||||
# networking.wireless.enable = lib.mkForce false;
|
||||
}
|
31
helpers/gui/sway.nix
Normal file
31
helpers/gui/sway.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
# docs: https://nixos.wiki/wiki/Sway
|
||||
{
|
||||
programs.sway = {
|
||||
# we configure sway with home-manager, but this enable gets us e.g. opengl and fonts
|
||||
enable = true;
|
||||
};
|
||||
|
||||
# TODO: should be able to use SDDM to get interactive login
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
settings = rec {
|
||||
initial_session = {
|
||||
command = "${pkgs.sway}/bin/sway";
|
||||
user = "colin";
|
||||
};
|
||||
default_session = initial_session;
|
||||
};
|
||||
};
|
||||
|
||||
# unlike other DEs, sway configures no audio stack
|
||||
# administer with pw-cli, pw-mon, pw-top commands
|
||||
services.pipewire = {
|
||||
enable = true;
|
||||
alsa.enable = true;
|
||||
alsa.support32Bit = true; # ??
|
||||
pulse.enable = true;
|
||||
};
|
||||
}
|
||||
|
65
helpers/hardware-x86_64.nix
Normal file
65
helpers/hardware-x86_64.nix
Normal file
@@ -0,0 +1,65 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
boot.initrd.availableKernelModules = [
|
||||
"xhci_pci" "ahci" "sd_mod" "sdhci_pci" # nixos-generate-config defaults
|
||||
"usb_storage" # rpi needed this to boot from usb storage, i think.
|
||||
# "usbhid" "hid-generic" # hopefully these will fix USB HID auto-sleep ?
|
||||
];
|
||||
boot.initrd.kernelModules = [ ];
|
||||
boot.initrd.supportedFilesystems = [ "ext4" "btrfs" "ext2" "ext3" "vfat" ];
|
||||
# find more of these with sensors-detect
|
||||
boot.kernelModules = [
|
||||
"coretemp"
|
||||
"kvm-intel"
|
||||
"kvm-amd" # desktop
|
||||
"amdgpu" # desktop
|
||||
];
|
||||
boot.extraModulePackages = [ ];
|
||||
boot.kernelParams = [ "boot.shell_on_fail" ];
|
||||
boot.consoleLogLevel = 7;
|
||||
|
||||
# Use the systemd-boot EFI boot loader.
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.systemd-boot.configurationLimit = 40; # keep this many generations
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
# enable cross compilation
|
||||
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
|
||||
# nixpkgs.crossSystem.system = "aarch64-linux";
|
||||
|
||||
powerManagement.cpuFreqGovernor = "powersave";
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
hardware.cpu.amd.updateMicrocode = true; # desktop
|
||||
hardware.cpu.intel.updateMicrocode = true; # laptop
|
||||
services.fwupd.enable = true;
|
||||
# powertop will default to putting USB devices -- including HID -- to sleep after TWO SECONDS
|
||||
powerManagement.powertop.enable = false;
|
||||
|
||||
hardware.opengl.extraPackages = [
|
||||
# laptop
|
||||
pkgs.intel-compute-runtime
|
||||
pkgs.intel-media-driver # new
|
||||
pkgs.libvdpau-va-gl # new
|
||||
pkgs.vaapiIntel
|
||||
# desktop
|
||||
pkgs.rocm-opencl-icd
|
||||
pkgs.rocm-opencl-runtime
|
||||
];
|
||||
hardware.opengl.driSupport = true;
|
||||
# For 32 bit applications
|
||||
hardware.opengl.driSupport32Bit = true;
|
||||
|
||||
# TODO colin: does this *do* anything?
|
||||
swapDevices = [ ];
|
||||
|
||||
# services.snapper.configs = {
|
||||
# root = {
|
||||
# subvolume = "/";
|
||||
# extraConfig = {
|
||||
# ALLOW_USERS = "colin";
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# services.snapper.snapshotInterval = "daily";
|
||||
}
|
532
helpers/home-manager-gen-colin.nix
Normal file
532
helpers/home-manager-gen-colin.nix
Normal file
@@ -0,0 +1,532 @@
|
||||
# docs:
|
||||
# https://rycee.gitlab.io/home-manager/
|
||||
# https://rycee.gitlab.io/home-manager/options.html
|
||||
# man home-configuration.nix
|
||||
#
|
||||
|
||||
# system is e.g. x86_64-linux
|
||||
# gui is "gnome", or null
|
||||
{ lib, pkgs, system, gui, extraPackages ? [] }: {
|
||||
home.stateVersion = "21.11";
|
||||
home.username = "colin";
|
||||
home.homeDirectory = "/home/colin";
|
||||
programs.home-manager.enable = true; # this lets home-manager manage dot-files in user dirs, i think
|
||||
|
||||
# XDG defines things like ~/Desktop, ~/Downloads, etc.
|
||||
# these clutter the home, so i mostly don't use them.
|
||||
xdg.userDirs = {
|
||||
enable = true;
|
||||
createDirectories = false; # on headless systems, most xdg dirs are noise
|
||||
desktop = "$HOME/.xdg/Desktop";
|
||||
documents = "$HOME/src";
|
||||
download = "$HOME/tmp";
|
||||
music = "$HOME/Music";
|
||||
pictures = "$HOME/Pictures";
|
||||
publicShare = "$HOME/.xdg/Public";
|
||||
templates = "$HOME/.xdg/Templates";
|
||||
videos = "$HOME/Videos";
|
||||
};
|
||||
|
||||
programs.zsh = {
|
||||
enable = true;
|
||||
enableSyntaxHighlighting = true;
|
||||
enableVteIntegration = true;
|
||||
dotDir = ".config/zsh";
|
||||
|
||||
initExtraBeforeCompInit = ''
|
||||
# p10k instant prompt
|
||||
# run p10k configure to configure, but it can't write out its file :-(
|
||||
POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true
|
||||
'';
|
||||
|
||||
# prezto = oh-my-zsh fork; controls prompt, auto-completion, etc.
|
||||
# see: https://github.com/sorin-ionescu/prezto
|
||||
prezto = {
|
||||
enable = true;
|
||||
pmodules = [
|
||||
"environment"
|
||||
"terminal"
|
||||
"editor"
|
||||
"history"
|
||||
"directory"
|
||||
"spectrum"
|
||||
"utility"
|
||||
"completion"
|
||||
"prompt"
|
||||
"git"
|
||||
];
|
||||
prompt = {
|
||||
theme = "powerlevel10k";
|
||||
};
|
||||
};
|
||||
};
|
||||
programs.kitty.enable = true;
|
||||
programs.git = {
|
||||
enable = true;
|
||||
userName = "colin";
|
||||
userEmail = "colin@uninsane.org";
|
||||
};
|
||||
|
||||
programs.vim = {
|
||||
enable = true;
|
||||
extraConfig = ''
|
||||
" wtf vim project: NOBODY LIKES MOUSE FOR VISUAL MODE
|
||||
set mouse-=a
|
||||
" copy/paste to system clipboard
|
||||
set clipboard=unnamedplus
|
||||
" <tab> completion menu settings
|
||||
set wildmenu
|
||||
set wildmode=longest,list,full
|
||||
" highlight all matching searches (using / and ?)
|
||||
set hlsearch
|
||||
" allow backspace to delete empty lines in insert mode
|
||||
set backspace=indent,eol,start
|
||||
" built-in syntax highlighting
|
||||
syntax enable
|
||||
" show line/col number in bottom right
|
||||
set ruler
|
||||
" highlight trailing space & related syntax errors (does this work?)
|
||||
let c_space_errors=1
|
||||
let python_space_errors=1
|
||||
'';
|
||||
};
|
||||
|
||||
# obtain these by running `dconf dump /` after manually customizing gnome
|
||||
# TODO: fix "is not of type `GVariant value'"
|
||||
# dconf.settings = lib.mkIf (gui == "gnome") {
|
||||
# gnome = {
|
||||
# # control alt-tab behavior
|
||||
# "org/gnome/desktop/wm/keybindings" = {
|
||||
# switch-applications = [ "<Super>Tab" ];
|
||||
# switch-applications-backward=[];
|
||||
# switch-windows=["<Alt>Tab"];
|
||||
# switch-windows-backward=["<Super><Alt>Tab"];
|
||||
# };
|
||||
# # idle power savings
|
||||
# "org/gnome/settings-deamon/plugins/power" = {
|
||||
# idle-brigthness = 50;
|
||||
# sleep-inactive-ac-type = "nothing";
|
||||
# sleep-inactive-battery-timeout = 5400; # seconds
|
||||
# };
|
||||
# "org/gnome/shell" = {
|
||||
# favorite-apps = [
|
||||
# "org.gnome.Nautilus.desktop"
|
||||
# "firefox.desktop"
|
||||
# "kitty.desktop"
|
||||
# # "org.gnome.Terminal.desktop"
|
||||
# ];
|
||||
# };
|
||||
# "org/gnome/desktop/session" = {
|
||||
# # how long until considering a session idle (triggers e.g. screen blanking)
|
||||
# idle-delay = 900;
|
||||
# };
|
||||
# "org/gnome/desktop/interface" = {
|
||||
# text-scaling-factor = 1.25;
|
||||
# };
|
||||
# "org/gnome/desktop/media-handling" = {
|
||||
# # don't auto-mount inserted media
|
||||
# automount = false;
|
||||
# automount-open = false;
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
|
||||
# home.pointerCursor = {
|
||||
# package = pkgs.vanilla-dmz;
|
||||
# name = "Vanilla-DMZ";
|
||||
# };
|
||||
|
||||
# taken from https://github.com/srid/nix-config/blob/705a70c094da53aa50cf560179b973529617eb31/nix/home/i3.nix
|
||||
xsession.windowManager.i3 = lib.mkIf (gui == "i3") (
|
||||
let
|
||||
mod = "Mod4";
|
||||
in {
|
||||
enable = true;
|
||||
config = {
|
||||
modifier = mod;
|
||||
|
||||
fonts = {
|
||||
names = [ "DejaVu Sans Mono" ];
|
||||
style = "Bold Semi-Condensed";
|
||||
size = 11.0;
|
||||
};
|
||||
|
||||
# terminal = "kitty";
|
||||
# terminal = "${pkgs.kitty}/bin/kitty";
|
||||
|
||||
keybindings = {
|
||||
"${mod}+Return" = "exec ${pkgs.kitty}/bin/kitty";
|
||||
"${mod}+p" = "exec ${pkgs.dmenu}/bin/dmenu_run";
|
||||
"${mod}+x" = "exec sh -c '${pkgs.maim}/bin/maim -s | xclip -selection clipboard -t image/png'";
|
||||
"${mod}+Shift+x" = "exec sh -c '${pkgs.i3lock}/bin/i3lock -c 222222 & sleep 5 && xset dpms force of'";
|
||||
|
||||
# Focus
|
||||
"${mod}+j" = "focus left";
|
||||
"${mod}+k" = "focus down";
|
||||
"${mod}+l" = "focus up";
|
||||
"${mod}+semicolon" = "focus right";
|
||||
|
||||
# Move
|
||||
"${mod}+Shift+j" = "move left";
|
||||
"${mod}+Shift+k" = "move down";
|
||||
"${mod}+Shift+l" = "move up";
|
||||
"${mod}+Shift+semicolon" = "move right";
|
||||
|
||||
# multi monitor setup
|
||||
# "${mod}+m" = "move workspace to output DP-2";
|
||||
# "${mod}+Shift+m" = "move workspace to output DP-5";
|
||||
};
|
||||
|
||||
# bars = [
|
||||
# {
|
||||
# position = "bottom";
|
||||
# statusCommand = "${pkgs.i3status-rust}/bin/i3status-rs ${./i3status-rust.toml}";
|
||||
# }
|
||||
# ];
|
||||
};
|
||||
});
|
||||
|
||||
wayland.windowManager.sway = lib.mkIf (gui == "sway") {
|
||||
enable = true;
|
||||
wrapperFeatures.gtk = true;
|
||||
config = rec {
|
||||
terminal = "${pkgs.kitty}/bin/kitty";
|
||||
window.border = 3; # pixel boundary between windows
|
||||
|
||||
# defaults; required for keybindings decl.
|
||||
modifier = "Mod1";
|
||||
# list of launchers: https://www.reddit.com/r/swaywm/comments/v39hxa/your_favorite_launcher/
|
||||
# menu = "${pkgs.dmenu}/bin/dmenu_path";
|
||||
menu = "${pkgs.fuzzel}/bin/fuzzel";
|
||||
# menu = "${pkgs.albert}/bin/albert";
|
||||
left = "h";
|
||||
down = "j";
|
||||
up = "k";
|
||||
right = "l";
|
||||
keybindings = {
|
||||
"${modifier}+Return" = "exec ${terminal}";
|
||||
"${modifier}+Shift+q" = "kill";
|
||||
"${modifier}+d" = "exec ${menu}";
|
||||
|
||||
"${modifier}+${left}" = "focus left";
|
||||
"${modifier}+${down}" = "focus down";
|
||||
"${modifier}+${up}" = "focus up";
|
||||
"${modifier}+${right}" = "focus right";
|
||||
|
||||
"${modifier}+Left" = "focus left";
|
||||
"${modifier}+Down" = "focus down";
|
||||
"${modifier}+Up" = "focus up";
|
||||
"${modifier}+Right" = "focus right";
|
||||
|
||||
"${modifier}+Shift+${left}" = "move left";
|
||||
"${modifier}+Shift+${down}" = "move down";
|
||||
"${modifier}+Shift+${up}" = "move up";
|
||||
"${modifier}+Shift+${right}" = "move right";
|
||||
|
||||
"${modifier}+Shift+Left" = "move left";
|
||||
"${modifier}+Shift+Down" = "move down";
|
||||
"${modifier}+Shift+Up" = "move up";
|
||||
"${modifier}+Shift+Right" = "move right";
|
||||
|
||||
"${modifier}+b" = "splith";
|
||||
"${modifier}+v" = "splitv";
|
||||
"${modifier}+f" = "fullscreen toggle";
|
||||
"${modifier}+a" = "focus parent";
|
||||
|
||||
"${modifier}+s" = "layout stacking";
|
||||
"${modifier}+w" = "layout tabbed";
|
||||
"${modifier}+e" = "layout toggle split";
|
||||
|
||||
"${modifier}+Shift+space" = "floating toggle";
|
||||
"${modifier}+space" = "focus mode_toggle";
|
||||
|
||||
"${modifier}+1" = "workspace number 1";
|
||||
"${modifier}+2" = "workspace number 2";
|
||||
"${modifier}+3" = "workspace number 3";
|
||||
"${modifier}+4" = "workspace number 4";
|
||||
"${modifier}+5" = "workspace number 5";
|
||||
"${modifier}+6" = "workspace number 6";
|
||||
"${modifier}+7" = "workspace number 7";
|
||||
"${modifier}+8" = "workspace number 8";
|
||||
"${modifier}+9" = "workspace number 9";
|
||||
|
||||
"${modifier}+Shift+1" =
|
||||
"move container to workspace number 1";
|
||||
"${modifier}+Shift+2" =
|
||||
"move container to workspace number 2";
|
||||
"${modifier}+Shift+3" =
|
||||
"move container to workspace number 3";
|
||||
"${modifier}+Shift+4" =
|
||||
"move container to workspace number 4";
|
||||
"${modifier}+Shift+5" =
|
||||
"move container to workspace number 5";
|
||||
"${modifier}+Shift+6" =
|
||||
"move container to workspace number 6";
|
||||
"${modifier}+Shift+7" =
|
||||
"move container to workspace number 7";
|
||||
"${modifier}+Shift+8" =
|
||||
"move container to workspace number 8";
|
||||
"${modifier}+Shift+9" =
|
||||
"move container to workspace number 9";
|
||||
|
||||
"${modifier}+Shift+minus" = "move scratchpad";
|
||||
"${modifier}+minus" = "scratchpad show";
|
||||
|
||||
"${modifier}+Shift+c" = "reload";
|
||||
"${modifier}+Shift+e" =
|
||||
"exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'";
|
||||
|
||||
"${modifier}+r" = "mode resize";
|
||||
} // {
|
||||
# media keys
|
||||
XF86MonBrightnessDown = ''exec "${pkgs.brightnessctl}/bin/brightnessctl set 2%-"'';
|
||||
XF86MonBrightnessUp = ''exec "${pkgs.brightnessctl}/bin/brightnessctl set +2%"'';
|
||||
|
||||
XF86AudioRaiseVolume = "exec '${pkgs.pulsemixer}/bin/pulsemixer --change-volume +5'";
|
||||
XF86AudioLowerVolume = "exec '${pkgs.pulsemixer}/bin/pulsemixer --change-volume -5'";
|
||||
XF86AudioMute = "exec '${pkgs.pulsemixer}/bin/pulsemixer --toggle-mute'";
|
||||
|
||||
};
|
||||
|
||||
# mostly defaults:
|
||||
bars = [{
|
||||
mode = "dock";
|
||||
hiddenState = "hide";
|
||||
position = "top";
|
||||
command = "${pkgs.waybar}/bin/waybar";
|
||||
workspaceButtons = true;
|
||||
workspaceNumbers = true;
|
||||
statusCommand = "${pkgs.i3status}/bin/i3status";
|
||||
fonts = {
|
||||
names = [ "monospace" ];
|
||||
size = 8.0;
|
||||
};
|
||||
trayOutput = "primary";
|
||||
colors = {
|
||||
background = "#000000";
|
||||
statusline = "#ffffff";
|
||||
separator = "#666666";
|
||||
focusedWorkspace = {
|
||||
border = "#4c7899";
|
||||
background = "#285577";
|
||||
text = "#ffffff";
|
||||
};
|
||||
activeWorkspace = {
|
||||
border = "#333333";
|
||||
background = "#5f676a";
|
||||
text = "#ffffff";
|
||||
};
|
||||
inactiveWorkspace = {
|
||||
border = "#333333";
|
||||
background = "#222222";
|
||||
text = "#888888";
|
||||
};
|
||||
urgentWorkspace = {
|
||||
border = "#2f343a";
|
||||
background = "#900000";
|
||||
text = "#ffffff";
|
||||
};
|
||||
bindingMode = {
|
||||
border = "#2f343a";
|
||||
background = "#900000";
|
||||
text = "#ffffff";
|
||||
};
|
||||
};
|
||||
}];
|
||||
};
|
||||
};
|
||||
|
||||
programs.waybar = lib.mkIf (gui == "sway") {
|
||||
enable = true;
|
||||
# docs: https://github.com/Alexays/Waybar/wiki/Configuration
|
||||
settings = {
|
||||
mainBar = {
|
||||
layer = "top";
|
||||
height = 40;
|
||||
modules-left = ["sway/workspaces" "sway/mode"];
|
||||
modules-center = ["sway/window"];
|
||||
modules-right = ["custom/mediaplayer" "clock" "cpu" "network"];
|
||||
"sway/window" = {
|
||||
max-length = 50;
|
||||
};
|
||||
# include song artist/title. source: https://www.reddit.com/r/swaywm/comments/ni0vso/waybar_spotify_tracktitle/
|
||||
"custom/mediaplayer" = {
|
||||
exec = pkgs.writeShellScript "waybar-mediaplayer" ''
|
||||
player_status=$(${pkgs.playerctl}/bin/playerctl status 2> /dev/null)
|
||||
if [ "$player_status" = "Playing" ]; then
|
||||
echo "$(${pkgs.playerctl}/bin/playerctl metadata artist) - $(${pkgs.playerctl}/bin/playerctl metadata title)"
|
||||
elif [ "$player_status" = "Paused" ]; then
|
||||
echo " $(${pkgs.playerctl}/bin/playerctl metadata artist) - $(${pkgs.playerctl}/bin/playerctl metadata title)"
|
||||
fi
|
||||
'';
|
||||
interval = 2;
|
||||
format = "{} ";
|
||||
# return-type = "json";
|
||||
on-click = "${pkgs.playerctl}/bin/playerctl play-pause";
|
||||
on-scroll-up = "${pkgs.playerctl}/bin/playerctl next";
|
||||
on-scroll-down = "${pkgs.playerctl}/bin/playerctl previous";
|
||||
};
|
||||
network = {
|
||||
interval = 1;
|
||||
format-ethernet = "{ifname}: {ipaddr}/{cidr} up: {bandwidthUpBits} down: {bandwidthDownBits}";
|
||||
};
|
||||
cpu = {
|
||||
format = "{usage}% ";
|
||||
tooltip = false;
|
||||
};
|
||||
clock = {
|
||||
format-alt = "{:%a, %d. %b %H:%M}";
|
||||
};
|
||||
};
|
||||
};
|
||||
# style = ''
|
||||
# * {
|
||||
# border: none;
|
||||
# border-radius: 0;
|
||||
# font-family: Source Code Pro;
|
||||
# }
|
||||
# window#waybar {
|
||||
# background: #16191C;
|
||||
# color: #AAB2BF;
|
||||
# }
|
||||
# #workspaces button {
|
||||
# padding: 0 5px;
|
||||
# }
|
||||
# .custom-spotify {
|
||||
# padding: 0 10px;
|
||||
# margin: 0 4px;
|
||||
# background-color: #1DB954;
|
||||
# color: black;
|
||||
# }
|
||||
# '';
|
||||
};
|
||||
|
||||
|
||||
programs.firefox = lib.mkIf (gui != null) {
|
||||
enable = true;
|
||||
|
||||
profiles.default = {
|
||||
bookmarks = {
|
||||
fed_uninsane.url = "https://fed.uninsane.org/";
|
||||
delightful.url = "https://delightful.club/";
|
||||
crowdsupply.url = "https://www.crowdsupply.com/";
|
||||
linux_phone_apps.url = "https://linuxphoneapps.org/mobile-compatibility/5/";
|
||||
mempool.url = "https://jochen-hoenicke.de/queue";
|
||||
};
|
||||
};
|
||||
|
||||
# firefox profile support seems to be broken :shrug:
|
||||
# profiles.other = {
|
||||
# id = 2;
|
||||
# };
|
||||
|
||||
# NB: these must be manually enabled in the Firefox settings on first start
|
||||
# extensions can be found here: https://gitlab.com/rycee/nur-expressions/-/blob/master/pkgs/firefox-addons/addons.json
|
||||
extensions = [
|
||||
pkgs.nur.repos.rycee.firefox-addons.bypass-paywalls-clean
|
||||
pkgs.nur.repos.rycee.firefox-addons.metamask
|
||||
pkgs.nur.repos.rycee.firefox-addons.i-dont-care-about-cookies
|
||||
pkgs.nur.repos.rycee.firefox-addons.sidebery
|
||||
pkgs.nur.repos.rycee.firefox-addons.sponsorblock
|
||||
pkgs.nur.repos.rycee.firefox-addons.ublock-origin
|
||||
];
|
||||
};
|
||||
|
||||
home.shellAliases = {
|
||||
":q" = "exit";
|
||||
# common typos
|
||||
"cd.." = "cd ..";
|
||||
"cd../" = "cd ../";
|
||||
};
|
||||
|
||||
|
||||
home.packages = [
|
||||
pkgs.btrfs-progs
|
||||
pkgs.dig
|
||||
pkgs.cryptsetup
|
||||
pkgs.duplicity
|
||||
pkgs.fatresize
|
||||
pkgs.fd
|
||||
pkgs.file
|
||||
pkgs.gnumake
|
||||
pkgs.gptfdisk
|
||||
pkgs.hdparm
|
||||
pkgs.htop
|
||||
pkgs.iftop
|
||||
pkgs.inetutils # for telnet
|
||||
pkgs.iotop
|
||||
pkgs.iptables
|
||||
pkgs.jq
|
||||
pkgs.killall
|
||||
pkgs.lm_sensors # for sensors-detect
|
||||
pkgs.lsof
|
||||
pkgs.mix2nix
|
||||
pkgs.netcat
|
||||
pkgs.nixpkgs-review
|
||||
pkgs.nixUnstable # TODO: still needed on 22.05?
|
||||
# pkgs.nixos-generators
|
||||
# pkgs.nettools
|
||||
pkgs.nmap
|
||||
pkgs.obsidian
|
||||
pkgs.parted
|
||||
pkgs.pciutils
|
||||
# pkgs.ponymix
|
||||
pkgs.powertop
|
||||
pkgs.pulsemixer
|
||||
pkgs.python3
|
||||
pkgs.ripgrep
|
||||
pkgs.smartmontools
|
||||
pkgs.snapper
|
||||
pkgs.socat
|
||||
pkgs.sudo
|
||||
pkgs.usbutils
|
||||
pkgs.wget
|
||||
pkgs.wireguard-tools
|
||||
pkgs.youtube-dl
|
||||
pkgs.zola
|
||||
]
|
||||
++ (if gui != null then
|
||||
[
|
||||
# GUI only
|
||||
pkgs.chromium
|
||||
pkgs.clinfo
|
||||
pkgs.element-desktop # broken on phosh
|
||||
pkgs.evince # works on phosh
|
||||
pkgs.font-manager
|
||||
pkgs.gimp # broken on phosh
|
||||
pkgs.gnome.dconf-editor
|
||||
pkgs.gnome.file-roller
|
||||
pkgs.gnome.gnome-maps # works on phosh
|
||||
pkgs.gnome.nautilus
|
||||
pkgs.gnome-podcasts
|
||||
pkgs.gnome.gnome-terminal # works on phosh
|
||||
pkgs.inkscape
|
||||
pkgs.kaiteki # Pleroma client
|
||||
pkgs.libreoffice-fresh # XXX colin: maybe don't want this on mobile
|
||||
pkgs.mesa-demos
|
||||
pkgs.playerctl
|
||||
pkgs.tdesktop # broken on phosh
|
||||
pkgs.vlc # works on phosh
|
||||
pkgs.whalebird # pleroma client. input is broken on phosh
|
||||
pkgs.xterm # broken on phosh
|
||||
] else [])
|
||||
++ (if gui == "sway" then
|
||||
[
|
||||
# TODO: move this to helpers/gui/sway.nix?
|
||||
pkgs.swaylock
|
||||
pkgs.swayidle
|
||||
pkgs.wl-clipboard
|
||||
pkgs.mako # notification daemon
|
||||
# pkgs.dmenu # todo: use wofi?
|
||||
# user stuff
|
||||
# pkgs.pavucontrol
|
||||
] else [])
|
||||
++ (if gui != null && system == "x86_64-linux" then
|
||||
[
|
||||
# x86_64 only
|
||||
pkgs.signal-desktop
|
||||
pkgs.spotify
|
||||
pkgs.discord
|
||||
] else [])
|
||||
++ extraPackages;
|
||||
}
|
4
helpers/set-hostname.nix
Normal file
4
helpers/set-hostname.nix
Normal file
@@ -0,0 +1,4 @@
|
||||
hostName: { ... }:
|
||||
{
|
||||
networking.hostName = hostName;
|
||||
}
|
17
helpers/universal/default.nix
Normal file
17
helpers/universal/default.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./fs.nix
|
||||
./home-manager.nix
|
||||
./nix-cache.nix
|
||||
./users.nix
|
||||
];
|
||||
|
||||
time.timeZone = "America/Los_Angeles";
|
||||
|
||||
environment.variables = {
|
||||
EDITOR = "vim";
|
||||
};
|
||||
}
|
||||
|
25
helpers/universal/fs.nix
Normal file
25
helpers/universal/fs.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
fileSystems."/mnt/media-uninsane" = {
|
||||
# device = "sshfs#colin@uninsane.org:/opt/uninsane/media";
|
||||
device = "colin@uninsane.org:/opt/uninsane/media";
|
||||
fsType = "fuse.sshfs";
|
||||
options = [
|
||||
"x-systemd.automount"
|
||||
"_netdev"
|
||||
"user"
|
||||
"idmap=user"
|
||||
"transform_symlinks"
|
||||
"identityfile=/home/colin/.ssh/id_ed25519"
|
||||
"allow_other"
|
||||
"default_permissions"
|
||||
"uid=1000"
|
||||
"gid=1000"
|
||||
];
|
||||
};
|
||||
environment.systemPackages = [
|
||||
pkgs.sshfs-fuse
|
||||
];
|
||||
}
|
||||
|
9
helpers/universal/home-manager.nix
Normal file
9
helpers/universal/home-manager.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{ home-manager, config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
home-manager.nixosModule
|
||||
];
|
||||
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
}
|
16
helpers/universal/nix-cache.nix
Normal file
16
helpers/universal/nix-cache.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
# use our own binary cache
|
||||
nix.settings = {
|
||||
substituters = [
|
||||
"https://nixcache.uninsane.org"
|
||||
"https://nix-community.cachix.org"
|
||||
"https://cache.nixos.org/"
|
||||
];
|
||||
trusted-public-keys = [
|
||||
"nixcache.uninsane.org:r3WILM6+QrkmsLgqVQcEdibFD7Q/4gyzD9dGT33GP70="
|
||||
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
|
||||
];
|
||||
};
|
||||
}
|
53
helpers/universal/users.nix
Normal file
53
helpers/universal/users.nix
Normal file
@@ -0,0 +1,53 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
# installer docs: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/installation-device.nix
|
||||
{
|
||||
# Users are exactly these specified here;
|
||||
# old ones will be deleted (from /etc/passwd, etc) upon upgrade.
|
||||
users.mutableUsers = false;
|
||||
|
||||
# docs: https://nixpkgs-manual-sphinx-markedown-example.netlify.app/generated/options-db.xml.html#users-users
|
||||
users.users.colin = {
|
||||
# sets group to "users" (?)
|
||||
isNormalUser = true;
|
||||
home = "/home/colin";
|
||||
uid = 1000;
|
||||
# XXX colin: this is what the installer has, but is it necessary?
|
||||
# group = "users";
|
||||
extraGroups = [
|
||||
"wheel"
|
||||
"nixbuild"
|
||||
"networkmanager"
|
||||
# phosh/mobile. XXX colin: unsure if necessary
|
||||
"video"
|
||||
"feedbackd"
|
||||
"dialout" # required for modem access
|
||||
];
|
||||
initialPassword = lib.mkDefault "";
|
||||
shell = pkgs.zsh;
|
||||
# shell = pkgs.bashInteractive;
|
||||
# XXX colin: create ssh key for THIS user by logging in and running:
|
||||
# ssh-keygen -t ed25519
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGSDe/y0e9PSeUwYlMPjzhW0UhNsGAGsW3lCG3apxrD5 colin@colin.desktop"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG+MZ/l5d8g5hbxMB9ed1uyvhV85jwNrSVNVxb5ujQjw colin@lappy"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPU5GlsSfbaarMvDA20bxpSZGWviEzXGD8gtrIowc1pX colin@desko"
|
||||
# TODO: should probably only let this authenticate to my server
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGCLCA9KbjXaXNNMJJvqbPO5KQQ64JCdG8sg88AfdKzi colin@moby"
|
||||
];
|
||||
};
|
||||
|
||||
security.sudo = {
|
||||
enable = true;
|
||||
wheelNeedsPassword = false;
|
||||
};
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
permitRootLogin = "no";
|
||||
passwordAuthentication = false;
|
||||
};
|
||||
|
||||
# TODO colin: move this somewhere else!
|
||||
programs.vim.defaultEditor = true;
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
## directory structure
|
||||
- by-name/<hostname>: configuration which is evaluated _only_ for the given hostname
|
||||
- common/: configuration which applies to all hosts
|
||||
- modules/: nixpkgs-style modules which may be used by multiple hosts, but configured separately per host.
|
||||
- ideally no module here has effect unless `enable`d
|
||||
- however, `enable` may default to true
|
||||
- and in practice some of these modules surely aren't fully "disableable"
|
@@ -1,16 +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;
|
||||
}
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -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!
|
||||
}
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
./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;
|
||||
|
||||
# don't enable wifi by default: it messes with connectivity.
|
||||
# systemd.services.iwd.enable = false;
|
||||
# networking.wireless.enable = false;
|
||||
# systemd.services.wpa_supplicant.enable = false;
|
||||
# sane.programs.wpa_supplicant.enableFor.user.colin = lib.mkForce false;
|
||||
# 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;
|
||||
|
||||
sane.roles.build-machine.enable = true;
|
||||
sane.roles.client = 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.ovpn.addrV4 = "172.26.55.21";
|
||||
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:20c1:a73c";
|
||||
sane.services.rsync-net.enable = true;
|
||||
|
||||
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.steam.enableFor.user.colin = true;
|
||||
|
||||
sane.programs.nwg-panel.config = {
|
||||
battery = false;
|
||||
brightness = false;
|
||||
};
|
||||
|
||||
sane.programs.mpv.config.defaultProfile = "high-quality";
|
||||
|
||||
# needed to use libimobiledevice/ifuse, for iphone sync
|
||||
services.usbmuxd.enable = true;
|
||||
|
||||
hardware.amdgpu.opencl.enable = true; # desktop (AMD's opencl implementation AKA "ROCM"); probably required for ollama
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
# 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
|
||||
fileSystems."/tmp".options = [ "size=128G" ];
|
||||
|
||||
fileSystems."/nix" = {
|
||||
device = "/dev/disk/by-uuid/dddddddd-eeee-5555-cccc-000020250527";
|
||||
fsType = "btrfs";
|
||||
options = [
|
||||
"compress=zstd"
|
||||
"defaults"
|
||||
];
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-uuid/2025-0527";
|
||||
fsType = "vfat";
|
||||
};
|
||||
}
|
@@ -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;
|
||||
};
|
||||
}
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
./fs.nix
|
||||
];
|
||||
|
||||
sane.roles.client = true;
|
||||
sane.roles.pc = true;
|
||||
sane.services.wg-home.enable = true;
|
||||
sane.ovpn.addrV4 = "172.23.119.72";
|
||||
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:0332:aa96/128";
|
||||
|
||||
# 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.stepmania.enableFor.user.colin = true; #< TODO: fix build
|
||||
sane.programs.sway.enableFor.user.colin = true;
|
||||
|
||||
sops.secrets.colin-passwd.neededForUsers = true;
|
||||
|
||||
sane.services.rsync-net.enable = true;
|
||||
|
||||
# starting 2024/09, under default settings (apparently 256 quantum), audio would crackle under load.
|
||||
# 1024 solves *most* crackles, but still noticable under heavier loads.
|
||||
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;
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
# Pinephone
|
||||
#
|
||||
# wikis, resources, ...:
|
||||
# - Linux Phone Apps: <https://linuxphoneapps.org/>
|
||||
# - massive mobile-friendly app database
|
||||
# - Mobian wiki: <https://wiki.mobian-project.org/doku.php?id=start>
|
||||
# - recommended apps, chatrooms
|
||||
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./fs.nix
|
||||
];
|
||||
|
||||
sane.hal.pine64-pinephone-pro.enable = true;
|
||||
sane.roles.client = true;
|
||||
sane.roles.handheld = true;
|
||||
sane.services.wg-home.enable = true;
|
||||
sane.ovpn.addrV4 = "172.24.87.255";
|
||||
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:18cd:a72b";
|
||||
|
||||
# XXX colin: phosh doesn't work well with passwordless login,
|
||||
# so set this more reliable default password should anything go wrong
|
||||
users.users.colin.initialPassword = "147147";
|
||||
|
||||
sops.secrets.colin-passwd.neededForUsers = true;
|
||||
|
||||
sane.services.rsync-net.enable = true;
|
||||
|
||||
sane.programs.sway.enableFor.user.colin = true;
|
||||
sane.programs.sway.config.mod = "Mod1"; #< alt key instead of Super
|
||||
|
||||
# enabled for easier debugging
|
||||
sane.programs.eg25-control.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.dino.config.autostart = true;
|
||||
sane.programs.signal-desktop.config.autostart = false;
|
||||
sane.programs.geary.config.autostart = false;
|
||||
|
||||
sane.programs.pipewire.config = {
|
||||
# tune so Dino doesn't drop audio
|
||||
# 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.
|
||||
# 2. Pipewire buffering into Dino.
|
||||
# the latter is fixed at 10ms by Dino, difficult to override via runtime config.
|
||||
# the former defaults low (e.g. 512 samples)
|
||||
# this default configuration causes the mic to regularly drop out entirely for a couple seconds at a time during a call,
|
||||
# presumably because the system can't keep up (pw-top shows incrementing counter in ERR column).
|
||||
# `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.
|
||||
# pipewire default config includes `clock.power-of-two-quantum = true`
|
||||
min-quantum = 2048;
|
||||
max-quantum = 8192;
|
||||
};
|
||||
|
||||
sane.programs.mpv.config.defaultProfile = "fast";
|
||||
|
||||
# TODO: switch to systemd-boot
|
||||
boot.loader.generic-extlinux-compatible.enable = true;
|
||||
boot.loader.systemd-boot.enable = false;
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
fileSystems."/nix" = {
|
||||
device = "/dev/disk/by-uuid/1f1271f8-53ce-4081-8a29-60a4a6b5d6f9";
|
||||
fsType = "btrfs";
|
||||
options = [
|
||||
"compress=zstd"
|
||||
"defaults"
|
||||
];
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-uuid/0299-F1E5";
|
||||
fsType = "vfat";
|
||||
};
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./fs.nix
|
||||
];
|
||||
|
||||
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
|
||||
|
||||
# auto-login at shell
|
||||
services.getty.autologinUser = "colin";
|
||||
# users.users.colin.initialPassword = "colin";
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
fileSystems."/nix" = {
|
||||
device = "/dev/disk/by-uuid/44445555-6666-7777-8888-999900001111";
|
||||
fsType = "ext4";
|
||||
};
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-uuid/2222-3333";
|
||||
fsType = "vfat";
|
||||
};
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./fs.nix
|
||||
./net
|
||||
./services
|
||||
./users
|
||||
];
|
||||
|
||||
# for administering services
|
||||
sane.programs.clightning-sane.enableFor.user.colin = true;
|
||||
# sane.programs.freshrss.enableFor.user.colin = true;
|
||||
# sane.programs.signaldctl.enableFor.user.colin = true;
|
||||
# sane.programs.matrix-synapse.enableFor.user.colin = true;
|
||||
|
||||
sane.roles.build-machine.enable = true;
|
||||
sane.programs.sane-deadlines.config.showOnLogin = false; # ~/knowledge doesn't always exist
|
||||
sane.programs.consoleUtils.suggestedPrograms = [
|
||||
"consoleMediaUtils" # notably, for go2tv / casting
|
||||
"pcConsoleUtils"
|
||||
"sane-scripts.stop-all-servo"
|
||||
];
|
||||
sane.services.dyn-dns.enable = true;
|
||||
sane.nixcache.remote-builders.desko = false;
|
||||
sane.nixcache.remote-builders.servo = false;
|
||||
sane.services.rsync-net.enable = true;
|
||||
|
||||
# automatically log in at the virtual consoles.
|
||||
# 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";
|
||||
|
||||
# both transmission and ipfs try to set different net defaults.
|
||||
# we just use the most aggressive of the two here:
|
||||
boot.kernel.sysctl = {
|
||||
"net.core.rmem_max" = 4194304; # 4MB
|
||||
};
|
||||
}
|
||||
|
@@ -1,106 +0,0 @@
|
||||
{ lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
# hostId: not used for anything except zfs guardrail?
|
||||
# [hex(ord(x)) for x in 'serv']
|
||||
# networking.hostId = "73657276";
|
||||
|
||||
sane.persist.stores."ext" = {
|
||||
origin = "/mnt/pool/persist";
|
||||
storeDescription = "external HDD storage";
|
||||
defaultMethod = "bind"; #< TODO: change to "symlink"?
|
||||
};
|
||||
|
||||
# increase /tmp space (defaults to 50% of RAM) for building large nix things.
|
||||
# even the stock `nixpkgs.linux` consumes > 16 GB of tmp
|
||||
fileSystems."/tmp".options = [ "size=32G" ];
|
||||
|
||||
fileSystems."/nix" = {
|
||||
device = "/dev/disk/by-uuid/55555555-eeee-ffff-bbbb-000020250820";
|
||||
fsType = "btrfs";
|
||||
options = [
|
||||
"compress=zstd"
|
||||
"defaults"
|
||||
];
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-uuid/2025-0820";
|
||||
fsType = "vfat";
|
||||
};
|
||||
|
||||
fileSystems."/mnt/pool" = {
|
||||
# all btrfs devices of the same RAID volume use the same UUID.
|
||||
device = "UUID=40fc6e1d-ba41-44de-bbf3-1aa02c3441df";
|
||||
fsType = "btrfs";
|
||||
options = [
|
||||
# "compress=zstd" #< not much point in compressing... mostly videos and music; media.
|
||||
"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"
|
||||
];
|
||||
};
|
||||
|
||||
# TODO: move this elsewhere and automate the ACLs!
|
||||
# FIRST TIME SETUP FOR MEDIA DIRECTORY:
|
||||
# - set the group sticky 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)
|
||||
# - 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`
|
||||
# - alternatively, `d:g:media:rwx` to grant `media` group even when file has a different owner, but that's a bit complex
|
||||
sane.persist.sys.byStore.ext = [{
|
||||
path = "/var/media";
|
||||
user = "colin";
|
||||
group = "media";
|
||||
mode = "0775";
|
||||
}];
|
||||
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)
|
||||
sane.fs."/var/media/archive/temp/README.md".file.text = ''
|
||||
this directory is for media i wish to remove from my library,
|
||||
but keep for a short time in case i reverse my decision.
|
||||
treat it like a system trash can.
|
||||
'';
|
||||
sane.fs."/var/media/Books".dir = {};
|
||||
sane.fs."/var/media/Books/Audiobooks".dir = {};
|
||||
sane.fs."/var/media/Books/Books".dir = {};
|
||||
sane.fs."/var/media/Books/Visual".dir = {};
|
||||
sane.fs."/var/media/collections".dir = {};
|
||||
sane.fs."/var/media/freeleech".dir = {};
|
||||
sane.fs."/var/media/Music".dir = {};
|
||||
sane.fs."/var/media/Pictures".dir = {};
|
||||
sane.fs."/var/media/Videos".dir = {};
|
||||
sane.fs."/var/media/Videos/Film".dir = {};
|
||||
sane.fs."/var/media/Videos/Shows".dir = {};
|
||||
sane.fs."/var/media/Videos/Talks".dir = {};
|
||||
|
||||
systemd.services.dedupe-media = {
|
||||
description = "transparently de-duplicate /var/media entries by using block-level hardlinks";
|
||||
script = ''
|
||||
${lib.getExe' pkgs.util-linux "hardlink"} /var/media --reflink=always --ignore-time --verbose
|
||||
'';
|
||||
};
|
||||
systemd.timers.dedupe-media = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
timerConfig = {
|
||||
OnStartupSec = "23min";
|
||||
OnUnitActiveSec = "720min";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -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"
|
||||
];
|
||||
}
|
@@ -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" ];
|
||||
};
|
||||
}
|
@@ -1,143 +0,0 @@
|
||||
# TURN/STUN NAT traversal service
|
||||
# commonly used to establish realtime calls with prosody, or possibly matrix/synapse
|
||||
#
|
||||
# - <https://github.com/coturn/coturn/>
|
||||
# - `man turnserver`
|
||||
# - config docs: <https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf>
|
||||
#
|
||||
# N.B. during operation it's NORMAL to see "error 401".
|
||||
# during session creation:
|
||||
# - client sends Allocate request
|
||||
# - server replies error 401, providing a realm and nonce
|
||||
# - client uses realm + nonce + shared secret to construct an auth key & call Allocate again
|
||||
# - server replies Allocate Success Response
|
||||
# - source: <https://stackoverflow.com/a/66643135>
|
||||
#
|
||||
# N.B. this safest implementation routes all traffic THROUGH A VPN
|
||||
# - that adds a lot of latency, but in practice turns out to be inconsequential.
|
||||
# i guess ICE allows clients to prefer the other party's lower-latency server, in practice?
|
||||
# - still, this is the "safe" implementation because STUN works with IP addresses instead of domain names:
|
||||
# 1. client A queries the STUN server to determine its own IP address/port.
|
||||
# 2. client A tells client B which IP address/port client A is visible on.
|
||||
# 3. client B contacts that IP address/port
|
||||
# this only works so long as the IP address/port which STUN server sees client A on is publicly routable.
|
||||
# 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.
|
||||
# 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:
|
||||
# - 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
|
||||
# TURN port range (inclusive).
|
||||
# default coturn behavior is to use the upper quarter of all ports. i.e. 49152 - 65535.
|
||||
# i believe TURN allocations expire after either 5 or 10 minutes of inactivity.
|
||||
turnPortLow = 49152; # 49152 = 0xc000
|
||||
turnPortHigh = turnPortLow + 256;
|
||||
turnPortRange = lib.range turnPortLow turnPortHigh;
|
||||
in
|
||||
{
|
||||
# the port definitions are only needed if running in the root net namespace
|
||||
# sane.ports.ports = lib.mkMerge ([
|
||||
# {
|
||||
# "3478" = {
|
||||
# # this is the "control" port.
|
||||
# # i.e. no client data is forwarded through it, but it's where clients request tunnels.
|
||||
# protocol = [ "tcp" "udp" ];
|
||||
# # visibleTo.lan = true;
|
||||
# # visibleTo.wan = true;
|
||||
# visibleTo.ovpns = true; # forward traffic from the VPN to the root NS
|
||||
# description = "colin-stun-turn";
|
||||
# };
|
||||
# "5349" = {
|
||||
# # the other port 3478 also supports TLS/DTLS, but presumably clients wanting TLS will default 5349
|
||||
# protocol = [ "tcp" ];
|
||||
# # visibleTo.lan = true;
|
||||
# # visibleTo.wan = true;
|
||||
# visibleTo.ovpns = true;
|
||||
# description = "colin-stun-turn-over-tls";
|
||||
# };
|
||||
# }
|
||||
# ] ++ (builtins.map
|
||||
# (port: {
|
||||
# "${builtins.toString port}" = let
|
||||
# count = port - turnPortLow + 1;
|
||||
# numPorts = turnPortHigh - turnPortLow + 1;
|
||||
# in {
|
||||
# protocol = [ "tcp" "udp" ];
|
||||
# # visibleTo.lan = true;
|
||||
# # visibleTo.wan = true;
|
||||
# visibleTo.ovpns = true;
|
||||
# description = "colin-turn-${builtins.toString count}-of-${builtins.toString numPorts}";
|
||||
# };
|
||||
# })
|
||||
# turnPortRange
|
||||
# ));
|
||||
|
||||
services.nginx.virtualHosts."turn.uninsane.org" = {
|
||||
# allow ACME to procure a cert via nginx for this domain
|
||||
enableACME = true;
|
||||
};
|
||||
sane.dns.zones."uninsane.org".inet = {
|
||||
# CNAME."turn" = "servo.wan";
|
||||
# CNAME."turn" = "ovpns";
|
||||
# CNAME."turn" = "native";
|
||||
# XXX: SRV records have to point to something with a A/AAAA record; no CNAMEs
|
||||
A."turn" = "%AOVPNS%";
|
||||
# A."turn" = "%AWAN%";
|
||||
|
||||
SRV."_stun._udp" = "5 50 3478 turn";
|
||||
SRV."_stun._tcp" = "5 50 3478 turn";
|
||||
SRV."_stuns._tcp" = "5 50 5349 turn";
|
||||
SRV."_turn._udp" = "5 50 3478 turn";
|
||||
SRV."_turn._tcp" = "5 50 3478 turn";
|
||||
SRV."_turns._tcp" = "5 50 5349 turn";
|
||||
};
|
||||
|
||||
# provide access to certs
|
||||
users.users.turnserver.extraGroups = [ "nginx" ];
|
||||
|
||||
services.coturn.enable = true;
|
||||
services.coturn.realm = "turn.uninsane.org";
|
||||
services.coturn.cert = "/var/lib/acme/turn.uninsane.org/fullchain.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.static-auth-secret-file = "/run/secrets/coturn_shared_secret";
|
||||
services.coturn.lt-cred-mech = true; #< XXX: use-auth-secret overrides lt-cred-mech
|
||||
|
||||
services.coturn.min-port = turnPortLow;
|
||||
services.coturn.max-port = turnPortHigh;
|
||||
# services.coturn.secure-stun = true;
|
||||
services.coturn.extraConfig = lib.concatStringsSep "\n" [
|
||||
"verbose"
|
||||
# "Verbose" #< even MORE verbosity than "verbose" (it's TOO MUCH verbosity really)
|
||||
"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=${config.sane.netns.ovpns.wg.address.ipv4}" "external-ip=${config.sane.netns.ovpns.wg.address.ipv4}"
|
||||
|
||||
# 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 *
|
||||
# "external-ip=97.113.128.229/10.78.79.51"
|
||||
# "external-ip=97.113.128.229"
|
||||
# "mobility" # "mobility with ICE (MICE) specs support" (?)
|
||||
];
|
||||
systemd.services.coturn.serviceConfig.NetworkNamespacePath = "/run/netns/ovpns";
|
||||
}
|
@@ -1,131 +0,0 @@
|
||||
# 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:
|
||||
# - 8333: for node-to-node communications
|
||||
# - 8332: rpc (client-to-node)
|
||||
#
|
||||
# rpc setup:
|
||||
# - generate a password
|
||||
# - use: <https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py>
|
||||
# (rpcauth.py is not included in the `'.#bitcoin'` package result)
|
||||
# - `wget https://raw.githubusercontent.com/bitcoin/bitcoin/master/share/rpcauth/rpcauth.py`
|
||||
# - `python ./rpcauth.py colin`
|
||||
# - copy the hash here. it's SHA-256, so safe to be public.
|
||||
# - add "rpcuser=colin" and "rpcpassword=<output>" to secrets/servo/bitcoin.conf (i.e. ~/.bitcoin/bitcoin.conf)
|
||||
# - bitcoin.conf docs: <https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md>
|
||||
# - validate with `bitcoin-cli -netinfo`
|
||||
{ config, lib, pkgs, sane-lib, ... }:
|
||||
let
|
||||
# bitcoind = config.sane.programs.bitcoind.packageUnwrapped;
|
||||
bitcoind = pkgs.bitcoind;
|
||||
# wrapper to run bitcoind with the tor onion address as externalip (computed at runtime)
|
||||
_bitcoindWithExternalIp = pkgs.writeShellScriptBin "bitcoind" ''
|
||||
set -xeu
|
||||
externalip="$(cat /var/lib/tor/onion/bitcoind/hostname)"
|
||||
exec ${lib.getExe' bitcoind "bitcoind"} "-externalip=$externalip" "$@"
|
||||
'';
|
||||
# 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:
|
||||
bitcoindWithExternalIp = pkgs.symlinkJoin {
|
||||
name = "bitcoind-with-external-ip";
|
||||
paths = [ _bitcoindWithExternalIp bitcoind ];
|
||||
};
|
||||
in
|
||||
{
|
||||
sane.persist.sys.byStore.ext = [
|
||||
{ user = "bitcoind-mainnet"; group = "bitcoind-mainnet"; path = "/var/lib/bitcoind-mainnet"; method = "bind"; }
|
||||
];
|
||||
|
||||
# sane.ports.ports."8333" = {
|
||||
# # this allows other nodes and clients to download blocks from me.
|
||||
# protocol = [ "tcp" ];
|
||||
# visibleTo.wan = true;
|
||||
# description = "colin-bitcoin";
|
||||
# };
|
||||
|
||||
services.tor.relay.onionServices.bitcoind = {
|
||||
version = 3;
|
||||
map = [{
|
||||
# by default tor will route public tor port P to 127.0.0.1:P.
|
||||
# so if this port is the same as clightning would natively use, then no further config is needed here.
|
||||
# see: <https://2019.www.torproject.org/docs/tor-manual.html.en#HiddenServicePort>
|
||||
port = 8333;
|
||||
# target.port; target.addr; #< set if tor port != clightning port
|
||||
}];
|
||||
# allow "tor" group (i.e. bitcoind-mainnet) to read /var/lib/tor/onion/bitcoind/hostname
|
||||
settings.HiddenServiceDirGroupReadable = true;
|
||||
};
|
||||
|
||||
services.bitcoind.mainnet = {
|
||||
enable = true;
|
||||
package = bitcoindWithExternalIp;
|
||||
rpc.users.colin = {
|
||||
# see docs at top of file for how to generate this
|
||||
passwordHMAC = "30002c05d82daa210550e17a182db3f3$6071444151281e1aa8a2729f75e3e2d224e9d7cac3974810dab60e7c28ffaae4";
|
||||
};
|
||||
extraConfig = ''
|
||||
# checkblocks: default 6: how many blocks to verify on start
|
||||
checkblocks=3
|
||||
# don't load the wallet, and disable wallet RPC calls
|
||||
disablewallet=1
|
||||
# proxy all outbound traffic through Tor
|
||||
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" ];
|
||||
|
||||
systemd.services.bitcoind-mainnet = {
|
||||
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" ];
|
||||
};
|
||||
|
||||
sops.secrets."bitcoin.conf" = {
|
||||
mode = "0600";
|
||||
owner = "colin";
|
||||
group = "users";
|
||||
};
|
||||
|
||||
sane.programs.bitcoin-cli.enableFor.user.colin = true; # for debugging/administration: `bitcoin-cli`
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
# clightning is an implementation of Bitcoin's Lightning Network.
|
||||
# as such, this assumes that `services.bitcoin` is enabled.
|
||||
# docs:
|
||||
# - tor clightning config: <https://docs.corelightning.org/docs/tor>
|
||||
# - `lightning-cli` and subcommands: <https://docs.corelightning.org/reference/lightning-cli>
|
||||
# - `man lightningd-config`
|
||||
#
|
||||
# management/setup/use:
|
||||
# - guide: <https://github.com/ElementsProject/lightning>
|
||||
#
|
||||
# debugging:
|
||||
# - `lightning-cli getlog debug`
|
||||
# - `lightning-cli listpays` -> show payments this node sent
|
||||
# - `lightning-cli listinvoices` -> show payments this node received
|
||||
#
|
||||
# first, acquire peers:
|
||||
# - `lightning-cli connect id@host`
|
||||
# where `id` is the node's pubkey, and `host` is perhaps an ip:port tuple, or a hash.onion:port tuple.
|
||||
# for testing, choose any node listed on <https://1ml.com>
|
||||
# - `lightning-cli listpeers`
|
||||
# should show the new peer, with `connected: true`
|
||||
#
|
||||
# then, fund the clightning wallet
|
||||
# - `lightning-cli newaddr`
|
||||
#
|
||||
# then, open channels
|
||||
# - `lightning-cli connect ...`
|
||||
# - `lightning-cli fundchannel <node_id> <amount_in_satoshis>`
|
||||
#
|
||||
# who to federate with?
|
||||
# - a lot of the larger nodes allow hands-free channel creation
|
||||
# - either inbound or outbound, sometimes paid
|
||||
# - find nodes on:
|
||||
# - <https://terminal.lightning.engineering/>
|
||||
# - <https://1ml.com>
|
||||
# - tor nodes: <https://1ml.com/node?order=capacity&iponionservice=true>
|
||||
# - <https://lightningnetwork.plus>
|
||||
# - <https://mempool.space/lightning>
|
||||
# - <https://amboss.space>
|
||||
# - a few tor-capable nodes which allow channel creation:
|
||||
# - <https://c-otto.de/>
|
||||
# - <https://cyberdyne.sh/>
|
||||
# - <https://yalls.org/about/>
|
||||
# - <https://coincept.com/>
|
||||
# - more resources: <https://www.lopp.net/lightning-information.html>
|
||||
# - node routability: https://hashxp.org/lightning/node/<id>
|
||||
# - especially, acquire inbound liquidity via lightningnetwork.plus's swap feature
|
||||
# - most of the opportunities are gated behind a minimum connection or capacity requirement
|
||||
#
|
||||
# tune payment parameters
|
||||
# - `lightning-cli setchannel <id> [feebase] [feeppm] [htlcmin] [htlcmax] [enforcedelay] [ignorefeelimits]`
|
||||
# - e.g. `lightning-cli setchannel all 0 10`
|
||||
# - it's suggested that feebase=0 simplifies routing.
|
||||
#
|
||||
# teardown:
|
||||
# - `lightning-cli withdraw <bc1... dest addr> <amount in satoshis> [feerate]`
|
||||
#
|
||||
# sanity:
|
||||
# - `lightning-cli listfunds`
|
||||
#
|
||||
# to receive a payment (do as `clightning` user):
|
||||
# - `lightning-cli invoice <amount in millisatoshi> <label> <description>`
|
||||
# - specify amount as `any` if undetermined
|
||||
# - then give the resulting bolt11 URI to the payer
|
||||
# to send a payment:
|
||||
# - `lightning-cli pay <bolt11 URI>`
|
||||
# - or `lightning-cli pay <bolt11 URI> [amount_msat] [label] [riskfactor] [maxfeepercent] ...`
|
||||
# - amount_msat must be "null" if the bolt11 URI specifies a value
|
||||
# - riskfactor defaults to 10
|
||||
# - maxfeepercent defaults to 0.5
|
||||
# - label is a human-friendly label for my records
|
||||
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
# 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"; }
|
||||
];
|
||||
|
||||
# see bitcoin.nix for how to generate this
|
||||
services.bitcoind.mainnet.rpc.users.clightning.passwordHMAC =
|
||||
"befcb82d9821049164db5217beb85439$2c31ac7db3124612e43893ae13b9527dbe464ab2d992e814602e7cb07dc28985";
|
||||
|
||||
sane.services.clightning.enable = true;
|
||||
sane.services.clightning.proxy = "127.0.0.1:9050"; # proxy outgoing traffic through tor
|
||||
# sane.services.clightning.publicAddress = "statictor:127.0.0.1:9051";
|
||||
sane.services.clightning.getPublicAddressCmd = "cat /var/lib/tor/onion/clightning/hostname";
|
||||
|
||||
services.tor.relay.onionServices.clightning = {
|
||||
version = 3;
|
||||
map = [{
|
||||
# by default tor will route public tor port P to 127.0.0.1:P.
|
||||
# so if this port is the same as clightning would natively use, then no further config is needed here.
|
||||
# see: <https://2019.www.torproject.org/docs/tor-manual.html.en#HiddenServicePort>
|
||||
port = 9735;
|
||||
# target.port; target.addr; #< set if tor port != clightning port
|
||||
}];
|
||||
# allow "tor" group (i.e. clightning) to read /var/lib/tor/onion/clightning/hostname
|
||||
settings.HiddenServiceDirGroupReadable = true;
|
||||
};
|
||||
|
||||
# must be in "tor" group to read /var/lib/tor/onion/*/hostname
|
||||
users.users.clightning.extraGroups = [ "tor" ];
|
||||
|
||||
systemd.services.clightning.after = [ "tor.service" ];
|
||||
systemd.services.clightning.requires = [ "tor.service" ];
|
||||
|
||||
# lightning-config contains fields from here:
|
||||
# - <https://docs.corelightning.org/docs/configuration>
|
||||
# secret config includes:
|
||||
# - bitcoin-rpcpassword
|
||||
# - alias=nodename
|
||||
# - rgb=rrggbb
|
||||
# - fee-base=<millisatoshi>
|
||||
# - fee-per-satoshi=<ppm>
|
||||
# - feature configs (i.e. experimental-xyz options)
|
||||
sane.services.clightning.extraConfig = ''
|
||||
# log levels: "io", "trace", "debug", "info", "unusual", "broken"
|
||||
# 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:
|
||||
# - config example: <https://github.com/fort-nix/nix-bitcoin/pull/462/files#diff-b357d832705b8ce8df1f41934d613f79adb77c4cd5cd9e9eb12a163fca3e16c6>
|
||||
# XXX: peerswap crashes clightning on launch. stacktrace is useless.
|
||||
# plugin={lib.getExe' pkgs.peerswap "peerswap"}
|
||||
# peerswap-db-path=/var/lib/clightning/peerswap/swaps
|
||||
# peerswap-policy-path=...
|
||||
'';
|
||||
sane.services.clightning.extraConfigFiles = [ config.sops.secrets."lightning-config".path ];
|
||||
sops.secrets."lightning-config" = {
|
||||
mode = "0640";
|
||||
owner = "clightning";
|
||||
group = "clightning";
|
||||
};
|
||||
|
||||
sane.programs.lightning-cli.enableFor.user.colin = true; # for debugging/admin:
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./bitcoin.nix
|
||||
./clightning.nix
|
||||
./i2p.nix
|
||||
./monero.nix
|
||||
./tor.nix
|
||||
];
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
{ 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;
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
# 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 = [
|
||||
# /var/lib/monero/lmdb is what consumes most of the space
|
||||
{ user = "monero"; group = "monero"; path = "/var/lib/monero"; method = "bind"; }
|
||||
];
|
||||
|
||||
services.monero.enable = true;
|
||||
services.monero.limits.upload = 5000; # in kB/s
|
||||
services.monero.extraConfig = ''
|
||||
# see: monero doc/ANONYMITY_NETWORKS.md
|
||||
#
|
||||
# "If any anonymity network is enabled, transactions being broadcast that lack a valid 'context'
|
||||
# (i.e. the transaction did not come from a P2P connection) will only be sent to peers on anonymity networks."
|
||||
#
|
||||
# i think this means that setting tx-proxy here ensures any transactions sent locally to my node (via RPC)
|
||||
# will be sent over an anonymity network.
|
||||
tx-proxy=i2p,127.0.0.1:9000
|
||||
tx-proxy=tor,127.0.0.1:9050
|
||||
'';
|
||||
|
||||
# monero ports: <https://monero.stackexchange.com/questions/604/what-ports-does-monero-use-rpc-p2p-etc>
|
||||
# - 18080 = "P2P" monero node <-> monero node connections
|
||||
# - 18081 = "RPC" monero client -> monero node connections
|
||||
sane.ports.ports."18080" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.wan = true;
|
||||
description = "colin-monero-p2p";
|
||||
};
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
# tor settings: <https://2019.www.torproject.org/docs/tor-manual.html.en>
|
||||
{ lib, ... }:
|
||||
{
|
||||
sane.persist.sys.byStore.ephemeral = [
|
||||
# N.B.: tor hidden service hostnames aren't deterministic, so if you need them
|
||||
# to be preserved across reboots then persist /var/lib/tor/onion in "private" store.
|
||||
{ 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.client.enable configures a torsocks proxy, accessible *only* to localhost.
|
||||
# at 127.0.0.1:9050
|
||||
services.tor.enable = true;
|
||||
services.tor.client.enable = true;
|
||||
|
||||
# in order for services to read /var/lib/tor/onion/*/hostname, they must be able to traverse /var/lib/tor,
|
||||
# and /var/lib/tor must have g+x.
|
||||
# DataDirectoryGroupReadable causes tor to use g+rx, technically more than we need, but all the files are 600 so it's fine.
|
||||
services.tor.settings.DataDirectoryGroupReadable = true;
|
||||
# StateDirectoryMode defaults to 0700, and thereby prevents the onion hostnames from being group readable
|
||||
systemd.services.tor.serviceConfig.StateDirectoryMode = lib.mkForce "0710";
|
||||
users.users.tor.homeMode = "0710"; # home mode defaults to 0700, causing readability problems, enforced by nixos "users" activation script
|
||||
|
||||
services.tor.settings.SafeLogging = false; # show actual .onion names in the syslog, else debugging is impossible
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./bitmagnet.nix
|
||||
./coturn.nix
|
||||
./cryptocurrencies
|
||||
./email
|
||||
./ejabberd.nix
|
||||
./freshrss.nix
|
||||
./export
|
||||
./hickory-dns.nix
|
||||
./gerbera.nix
|
||||
./gitea.nix
|
||||
./goaccess.nix
|
||||
./ipfs.nix
|
||||
./jackett
|
||||
./jellyfin
|
||||
./kiwix-serve.nix
|
||||
./komga.nix
|
||||
./lemmy.nix
|
||||
./matrix
|
||||
./minidlna.nix
|
||||
./mumble.nix
|
||||
./navidrome.nix
|
||||
./nginx
|
||||
./nixos-prebuild.nix
|
||||
./ntfy
|
||||
./pict-rs.nix
|
||||
./pleroma.nix
|
||||
./postgresql
|
||||
./prosody
|
||||
./slskd.nix
|
||||
./transmission
|
||||
./wikipedia.nix
|
||||
];
|
||||
}
|
@@ -1,470 +0,0 @@
|
||||
# docs:
|
||||
# - <https://docs.ejabberd.im/admin/configuration/basic>
|
||||
# example configs:
|
||||
# - <https://github.com/vkleen/machines/blob/138a2586ce185d7cf201d4e1fe898c83c4af52eb/hosts/europium/ejabberd.nix>
|
||||
# - <https://github.com/Mic92/stockholm/blob/675ef0088624c9de1cb531f318446316884a9d3d/tv/3modules/ejabberd/default.nix>
|
||||
# - <https://github.com/buffet/tararice/blob/master/programs/ejabberd.nix>
|
||||
# - enables STUN and TURN
|
||||
# - only over UDP 3478, not firewall-forwarding any TURN port range
|
||||
# - uses stun_disco module (but with no options)
|
||||
# - <https://github.com/leo60228/dotfiles/blob/39b3abba3009bdc31413d4757ca2f882a33eec8b/files/ejabberd.yml>
|
||||
# - <https://github.com/Mic92/dotfiles/blob/ddf0f4821f554f7667fc803344657367c55fb9e6/nixos/eve/modules/ejabberd.nix>
|
||||
# - <nixpkgs:nixos/tests/xmpp/ejabberd.nix>
|
||||
# - 2013: <https://github.com/processone/ejabberd/blob/master/ejabberd.yml.example>
|
||||
#
|
||||
# compliance tests:
|
||||
# - <https://compliance.conversations.im/server/uninsane.org/#xep0352>
|
||||
#
|
||||
# administration:
|
||||
# - `sudo -u ejabberd ejabberdctl help`
|
||||
#
|
||||
# federation/support matrix:
|
||||
# - avatars
|
||||
# - nixnet.services + dino: works in MUCs but not DMs (as of 2023 H1)
|
||||
# - movim.eu + dino: works in DMs, MUCs untested (as of 2023/08/29)
|
||||
# - calls
|
||||
# - local + dino: audio, video, works in DMs (as of 2023/08/29)
|
||||
# - movim.eu + dino: audio, video, works in DMs, no matter which side initiates (as of 2023/08/30)
|
||||
# - +native-cell-number@cheogram.com + dino: audio works in DMs, no matter which side initiates (as of 2023/09/01)
|
||||
# - can receive calls even if sender isn't in my roster
|
||||
# - this is presumably using JMP.chat's SIP servers, which then convert it to XMPP call
|
||||
#
|
||||
# bugs:
|
||||
# - 2023/09/01: will randomly stop federating. `systemctl restart ejabberd` fixes, but takes 10 minutes.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
# TODO: this range could be larger, but right now that's costly because each element is its own UPnP forward
|
||||
# TURN port range (inclusive)
|
||||
turnPortLow = 49152;
|
||||
turnPortHigh = 49167;
|
||||
turnPortRange = lib.range turnPortLow turnPortHigh;
|
||||
in
|
||||
# XXX(2023/10/15): disabled in favor of Prosody.
|
||||
# everything configured below was fine: used ejabberd for several months.
|
||||
lib.mkIf false
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
{ user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; method = "bind"; }
|
||||
];
|
||||
sane.ports.ports = lib.mkMerge ([
|
||||
{
|
||||
"3478" = {
|
||||
protocol = [ "tcp" "udp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-stun-turn";
|
||||
};
|
||||
"5222" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-client-to-server";
|
||||
};
|
||||
"5223" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpps-client-to-server"; # XMPP over TLS
|
||||
};
|
||||
"5269" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
description = "colin-xmpp-server-to-server";
|
||||
};
|
||||
"5270" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
description = "colin-xmpps-server-to-server"; # XMPP over TLS
|
||||
};
|
||||
"5280" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-bosh";
|
||||
};
|
||||
"5281" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-bosh-https";
|
||||
};
|
||||
"5349" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-stun-turn-over-tls";
|
||||
};
|
||||
"5443" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-web-services"; # file uploads, websockets, admin
|
||||
};
|
||||
}
|
||||
] ++ (builtins.map
|
||||
(port: {
|
||||
"${builtins.toString port}" = let
|
||||
count = port - turnPortLow + 1;
|
||||
numPorts = turnPortHigh - turnPortLow + 1;
|
||||
in {
|
||||
protocol = [ "tcp" "udp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-turn-${builtins.toString count}-of-${builtins.toString numPorts}";
|
||||
};
|
||||
})
|
||||
turnPortRange
|
||||
));
|
||||
|
||||
# this ejabberd config uses builtin STUN/TURN server, so hack to ensure no other implementation fights for ports
|
||||
services.coturn.enable = false;
|
||||
|
||||
# provide access to certs
|
||||
# TODO: this should just be `acme`. then we also add nginx to the `acme` group.
|
||||
# why is /var/lib/acme/* owned by `nginx` group??
|
||||
users.users.ejabberd.extraGroups = [ "nginx" ];
|
||||
|
||||
security.acme.certs."uninsane.org".extraDomainNames = [
|
||||
"xmpp.uninsane.org"
|
||||
"muc.xmpp.uninsane.org"
|
||||
"pubsub.xmpp.uninsane.org"
|
||||
"upload.xmpp.uninsane.org"
|
||||
"vjid.xmpp.uninsane.org"
|
||||
];
|
||||
|
||||
# exists so the XMPP server's cert can obtain altNames for all its resources
|
||||
services.nginx.virtualHosts."xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
services.nginx.virtualHosts."muc.xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
services.nginx.virtualHosts."pubsub.xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
services.nginx.virtualHosts."upload.xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
services.nginx.virtualHosts."vjid.xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet = {
|
||||
# XXX: SRV records have to point to something with a A/AAAA record; no CNAMEs
|
||||
A."xmpp" = "%ANATIVE%";
|
||||
CNAME."muc.xmpp" = "xmpp";
|
||||
CNAME."pubsub.xmpp" = "xmpp";
|
||||
CNAME."upload.xmpp" = "xmpp";
|
||||
CNAME."vjid.xmpp" = "xmpp";
|
||||
|
||||
# _Service._Proto.Name TTL Class SRV Priority Weight Port Target
|
||||
# - <https://xmpp.org/extensions/xep-0368.html>
|
||||
# something's requesting the SRV records for muc.xmpp, so let's include it
|
||||
# nothing seems to request XMPP SRVs for the other records (except @)
|
||||
# lower numerical priority field tells clients to prefer this method
|
||||
SRV."_xmpps-client._tcp.muc.xmpp" = "3 50 5223 xmpp";
|
||||
SRV."_xmpps-server._tcp.muc.xmpp" = "3 50 5270 xmpp";
|
||||
SRV."_xmpp-client._tcp.muc.xmpp" = "5 50 5222 xmpp";
|
||||
SRV."_xmpp-server._tcp.muc.xmpp" = "5 50 5269 xmpp";
|
||||
|
||||
SRV."_xmpps-client._tcp" = "3 50 5223 xmpp";
|
||||
SRV."_xmpps-server._tcp" = "3 50 5270 xmpp";
|
||||
SRV."_xmpp-client._tcp" = "5 50 5222 xmpp";
|
||||
SRV."_xmpp-server._tcp" = "5 50 5269 xmpp";
|
||||
|
||||
SRV."_stun._udp" = "5 50 3478 xmpp";
|
||||
SRV."_stun._tcp" = "5 50 3478 xmpp";
|
||||
SRV."_stuns._tcp" = "5 50 5349 xmpp";
|
||||
SRV."_turn._udp" = "5 50 3478 xmpp";
|
||||
SRV."_turn._tcp" = "5 50 3478 xmpp";
|
||||
SRV."_turns._tcp" = "5 50 5349 xmpp";
|
||||
};
|
||||
|
||||
# TODO: allocate UIDs/GIDs ?
|
||||
services.ejabberd.enable = true;
|
||||
services.ejabberd.configFile = "/var/lib/ejabberd/ejabberd.yaml";
|
||||
systemd.services.ejabberd.preStart = let
|
||||
config-in = pkgs.writeText "ejabberd.yaml.in" (lib.generators.toYAML {} {
|
||||
hosts = [ "uninsane.org" ];
|
||||
# none | emergency | alert | critical | error | warning | notice | info | debug
|
||||
loglevel = "debug";
|
||||
acme.auto = false;
|
||||
certfiles = [ "/var/lib/acme/uninsane.org/full.pem" ];
|
||||
# ca_file = "${pkgs.cacert.unbundled}/etc/ssl/certs/";
|
||||
# ca_file = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
|
||||
pam_userinfotype = "jid";
|
||||
acl = {
|
||||
admin.user = [ "colin@uninsane.org" ];
|
||||
local.user_regexp = "";
|
||||
loopback.ip = [ "127.0.0.0/8" "::1/128" ];
|
||||
};
|
||||
|
||||
access_rules = {
|
||||
local.allow = "local";
|
||||
c2s_access.allow = "all";
|
||||
announce.allow = "admin";
|
||||
configure.allow = "admin";
|
||||
muc_create.allow = "local";
|
||||
pubsub_createnode_access.allow = "all";
|
||||
trusted_network.allow = "loopback";
|
||||
};
|
||||
|
||||
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shaper-rules>
|
||||
shaper_rules = {
|
||||
# setting this to above 1 may break outgoing messages
|
||||
# - maybe some servers rate limit? or just don't understand simultaneous connections?
|
||||
max_s2s_connections = 1;
|
||||
max_user_sessions = 10;
|
||||
max_user_offline_messages = 5000;
|
||||
c2s_shaper.fast = "all";
|
||||
s2s_shaper.med = "all";
|
||||
};
|
||||
|
||||
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shapers>
|
||||
# this limits the bytes/sec.
|
||||
# for example, burst: 3_000_000 and rate: 100_000 means:
|
||||
# - each client has a BW budget that accumulates 100kB/sec and is capped at 3 MB
|
||||
shaper.fast = 1000000;
|
||||
shaper.med = 500000;
|
||||
# shaper.fast.rate = 1000000;
|
||||
# shaper.fast.burst_size = 10000000;
|
||||
# shaper.med.rate = 500000;
|
||||
# shaper.med.burst_size = 5000000;
|
||||
|
||||
# see: <https://docs.ejabberd.im/admin/configuration/listen/>
|
||||
# s2s_use_starttls = true;
|
||||
s2s_use_starttls = "optional";
|
||||
# lessens 504: remote-server-timeout errors
|
||||
# see: <https://github.com/processone/ejabberd/issues/3105#issuecomment-562182967>
|
||||
negotiation_timeout = 60;
|
||||
|
||||
listen = [
|
||||
{
|
||||
port = 5222;
|
||||
module = "ejabberd_c2s";
|
||||
shaper = "c2s_shaper";
|
||||
starttls = true;
|
||||
access = "c2s_access";
|
||||
}
|
||||
{
|
||||
port = 5223;
|
||||
module = "ejabberd_c2s";
|
||||
shaper = "c2s_shaper";
|
||||
tls = true;
|
||||
access = "c2s_access";
|
||||
}
|
||||
{
|
||||
port = 5269;
|
||||
module = "ejabberd_s2s_in";
|
||||
shaper = "s2s_shaper";
|
||||
}
|
||||
{
|
||||
port = 5270;
|
||||
module = "ejabberd_s2s_in";
|
||||
shaper = "s2s_shaper";
|
||||
tls = true;
|
||||
}
|
||||
{
|
||||
port = 5443;
|
||||
module = "ejabberd_http";
|
||||
tls = true;
|
||||
request_handlers = {
|
||||
"/admin" = "ejabberd_web_admin"; # TODO: ensure this actually works
|
||||
"/api" = "mod_http_api"; # ejabberd API endpoint (to control server)
|
||||
"/bosh" = "mod_bosh";
|
||||
"/upload" = "mod_http_upload";
|
||||
"/ws" = "ejabberd_http_ws";
|
||||
# "/.well-known/host-meta" = "mod_host_meta";
|
||||
# "/.well-known/host-meta.json" = "mod_host_meta";
|
||||
};
|
||||
}
|
||||
{
|
||||
# STUN+TURN TCP
|
||||
# note that the full port range should be forwarded ("not NAT'd")
|
||||
# `use_turn=true` enables both TURN *and* STUN
|
||||
port = 3478;
|
||||
module = "ejabberd_stun";
|
||||
transport = "tcp";
|
||||
use_turn = true;
|
||||
turn_min_port = turnPortLow;
|
||||
turn_max_port = turnPortHigh;
|
||||
turn_ipv4_address = "%ANATIVE%";
|
||||
}
|
||||
{
|
||||
# STUN+TURN UDP
|
||||
port = 3478;
|
||||
module = "ejabberd_stun";
|
||||
transport = "udp";
|
||||
use_turn = true;
|
||||
turn_min_port = turnPortLow;
|
||||
turn_max_port = turnPortHigh;
|
||||
turn_ipv4_address = "%ANATIVE%";
|
||||
}
|
||||
{
|
||||
# STUN+TURN TLS over TCP
|
||||
port = 5349;
|
||||
module = "ejabberd_stun";
|
||||
transport = "tcp";
|
||||
tls = true;
|
||||
certfile = "/var/lib/acme/uninsane.org/full.pem";
|
||||
use_turn = true;
|
||||
turn_min_port = turnPortLow;
|
||||
turn_max_port = turnPortHigh;
|
||||
turn_ipv4_address = "%ANATIVE%";
|
||||
}
|
||||
];
|
||||
|
||||
# TODO: enable mod_fail2ban
|
||||
# TODO(low): look into mod_http_fileserver for serving macros?
|
||||
modules = {
|
||||
# mod_adhoc = {};
|
||||
# mod_announce = {
|
||||
# access = "admin";
|
||||
# };
|
||||
# allows users to set avatars in vCard
|
||||
# - <https://docs.ejabberd.im/admin/configuration/modules/#mod-avatar>
|
||||
mod_avatar = {};
|
||||
mod_caps = {}; # for mod_pubsub
|
||||
mod_carboncopy = {}; # allows multiple clients to receive a user's message
|
||||
# queues messages when recipient is offline, including PEP and presence messages.
|
||||
# compliance test suggests this be enabled
|
||||
mod_client_state = {};
|
||||
|
||||
# mod_conversejs: TODO: enable once on 21.12
|
||||
# allows clients like Dino to discover where to upload files
|
||||
mod_disco.server_info = [
|
||||
{
|
||||
modules = "all";
|
||||
name = "abuse-addresses";
|
||||
urls = [
|
||||
"mailto:admin.xmpp@uninsane.org"
|
||||
"xmpp:colin@uninsane.org"
|
||||
];
|
||||
}
|
||||
{
|
||||
modules = "all";
|
||||
name = "admin-addresses";
|
||||
urls = [
|
||||
"mailto:admin.xmpp@uninsane.org"
|
||||
"xmpp:colin@uninsane.org"
|
||||
];
|
||||
}
|
||||
];
|
||||
mod_http_upload = {
|
||||
host = "upload.xmpp.uninsane.org";
|
||||
hosts = [ "upload.xmpp.uninsane.org" ];
|
||||
put_url = "https://@HOST@:5443/upload";
|
||||
dir_mode = "0750";
|
||||
file_mode = "0750";
|
||||
rm_on_unregister = false;
|
||||
};
|
||||
# allow discoverability of BOSH and websocket endpoints
|
||||
# TODO: enable once on ejabberd 22.05 (presently 21.04)
|
||||
# mod_host_meta = {};
|
||||
mod_jidprep = {}; # probably not needed: lets clients normalize jids
|
||||
mod_last = {}; # allow other users to know when i was last online
|
||||
mod_mam = {
|
||||
# Mnesia is limited to 2GB, better to use an SQL backend
|
||||
# For small servers SQLite is a good fit and is very easy
|
||||
# to configure. Uncomment this when you have SQL configured:
|
||||
# db_type: sql
|
||||
assume_mam_usage = true;
|
||||
default = "always";
|
||||
};
|
||||
mod_muc = {
|
||||
access = [ "allow" ];
|
||||
access_admin = { allow = "admin"; };
|
||||
access_create = "muc_create";
|
||||
access_persistent = "muc_create";
|
||||
access_mam = [ "allow" ];
|
||||
history_size = 100; # messages to show new participants
|
||||
host = "muc.xmpp.uninsane.org";
|
||||
hosts = [ "muc.xmpp.uninsane.org" ];
|
||||
default_room_options = {
|
||||
anonymous = false;
|
||||
lang = "en";
|
||||
persistent = true;
|
||||
mam = true;
|
||||
};
|
||||
};
|
||||
mod_muc_admin = {};
|
||||
mod_offline = {
|
||||
# store messages for a user when they're offline (TODO: understand multi-client workflow?)
|
||||
access_max_user_messages = "max_user_offline_messages";
|
||||
store_groupchat = true;
|
||||
};
|
||||
mod_ping = {};
|
||||
mod_privacy = {}; # deprecated, but required for `ejabberctl export_piefxis`
|
||||
mod_private = {}; # allow local clients to persist arbitrary data on my server
|
||||
# push notifications to services integrated with e.g. Apple/Android.
|
||||
# default is for a maximum amount of PII to be withheld, since these push notifs
|
||||
# generally traverse 3rd party services. can opt to include message body, etc, though.
|
||||
mod_push = {};
|
||||
# i don't fully understand what this does, but it seems aimed at making push notifs more reliable.
|
||||
mod_push_keepalive = {};
|
||||
mod_roster = {
|
||||
versioning = true;
|
||||
};
|
||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-s2s-dialback>
|
||||
# s2s dialback to verify inbound messages
|
||||
# unclear to what degree the XMPP network requires this
|
||||
mod_s2s_dialback = {};
|
||||
mod_shared_roster = {}; # creates groups for @all, @online, and anything manually administered?
|
||||
mod_stream_mgmt = {
|
||||
# resend undelivered messages if the origin client is offline
|
||||
resend_on_timeout = "if_offline";
|
||||
};
|
||||
# fallback for when DNS-based STUN discovery is unsupported.
|
||||
# - see: <https://xmpp.org/extensions/xep-0215.html>
|
||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-stun-disco>
|
||||
# people say to just keep this defaulted (i guess ejabberd knows to return its `host` option of uninsane.org?)
|
||||
mod_stun_disco = {};
|
||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-vcard>
|
||||
mod_vcard = {
|
||||
allow_return_all = true; # all users are discoverable (?)
|
||||
host = "vjid.xmpp.uninsane.org";
|
||||
hosts = [ "vjid.xmpp.uninsane.org" ];
|
||||
search = true;
|
||||
};
|
||||
mod_vcard_xupdate = {}; # needed for avatars
|
||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-pubsub>
|
||||
mod_pubsub = {
|
||||
#^ needed for avatars
|
||||
access_createnode = "pubsub_createnode_access";
|
||||
host = "pubsub.xmpp.uninsane.org";
|
||||
hosts = [ "pubsub.xmpp.uninsane.org" ];
|
||||
ignore_pep_from_offline = false;
|
||||
last_item_cache = true;
|
||||
plugins = [
|
||||
"pep"
|
||||
"flat"
|
||||
];
|
||||
force_node_config = {
|
||||
# ensure client bookmarks are private
|
||||
"storage:bookmarks:" = {
|
||||
"access_model" = "whitelist";
|
||||
};
|
||||
"urn:xmpp:avatar:data" = {
|
||||
"access_model" = "open";
|
||||
};
|
||||
"urn:xmpp:avatar:metadata" = {
|
||||
"access_model" = "open";
|
||||
};
|
||||
};
|
||||
};
|
||||
mod_version = {};
|
||||
};
|
||||
});
|
||||
in ''
|
||||
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
|
||||
# TODO: factor this out into `sane-woop` helper?
|
||||
rm -f /var/lib/ejabberd/ejabberd.yaml.new
|
||||
${lib.getExe pkgs.gnused} "s/%ANATIVE%/$ip/g" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new
|
||||
mv /var/lib/ejabberd/ejabberd.yaml{.new,}
|
||||
'';
|
||||
|
||||
sane.services.dyn-dns.restartOnChange = [ "ejabberd.service" ];
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
# nix configs to reference:
|
||||
# - <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver>
|
||||
# - <https://github.com/nix-community/nur-combined/-/tree/master/repos/eh5/machines/srv-m/mail-rspamd.nix>
|
||||
# - postfix / dovecot / rspamd / stalwart-jmap / sogo
|
||||
#
|
||||
# rspamd:
|
||||
# - nixos: <https://nixos.wiki/wiki/Rspamd>
|
||||
# - guide: <https://rspamd.com/doc/quickstart.html>
|
||||
# - non-nixos example: <https://dataswamp.org/~solene/2021-07-13-smtpd-rspamd.html>
|
||||
#
|
||||
#
|
||||
# my rough understanding of the pieces:
|
||||
# - postfix handles SMTP protocol with the rest of the world.
|
||||
# - dovecot implements IMAP protocol.
|
||||
# - client auth (i.e. validate that user@uninsane.org is who they claim)
|
||||
# - "folders" (INBOX, JUNK) are internal to dovecot?
|
||||
# or where do folders live, on-disk?
|
||||
#
|
||||
# - non-local clients (i.e. me) interact with BOTH postfix and dovecot, but primarily dovecot:
|
||||
# - mail reading is done via IMAP (so, dovecot)
|
||||
# - mail sending is done via SMTP/submission port (so, postfix)
|
||||
# - but postfix delegates authorization of that outgoing mail to dovecot, on the server side
|
||||
#
|
||||
# - local clients (i.e. sendmail) interact only with postfix
|
||||
#
|
||||
# debugging: general connectivity issues
|
||||
# - test that inbound port 25 is unblocked:
|
||||
# - `curl https://canyouseeme.org/ --data 'port=25&IP=$MX_IP' | grep 'see your service'`
|
||||
# - and retry with port 465, 587
|
||||
# - 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
|
||||
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./dovecot.nix
|
||||
./postfix.nix
|
||||
];
|
||||
|
||||
|
||||
#### SPAM FILTERING
|
||||
# services.rspamd.enable = true;
|
||||
# services.rspamd.postfix.enable = true;
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
# dovecot config options: <https://doc.dovecot.org/configuration_manual/>
|
||||
#
|
||||
# sieve docs:
|
||||
# - sieve language examples: <https://doc.dovecot.org/configuration_manual/sieve/examples/>
|
||||
# - sieve protocol/language: <https://proton.me/support/sieve-advanced-custom-filters>
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
sane.ports.ports."143" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-imap-imap.uninsane.org";
|
||||
};
|
||||
sane.ports.ports."993" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-imaps-imap.uninsane.org";
|
||||
};
|
||||
|
||||
# exists only to manage certs for dovecot
|
||||
services.nginx.virtualHosts."imap.uninsane.org" = {
|
||||
enableACME = true;
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet = {
|
||||
CNAME."imap" = "native";
|
||||
};
|
||||
|
||||
sops.secrets."dovecot_passwd" = {
|
||||
owner = config.users.users.dovecot2.name;
|
||||
# TODO: debug why mail can't be sent without this being world-readable
|
||||
mode = "0444";
|
||||
};
|
||||
|
||||
# inspired by https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/
|
||||
services.dovecot2.enable = true;
|
||||
# services.dovecot2.enableLmtp = true;
|
||||
services.dovecot2.sslServerCert = "/var/lib/acme/imap.uninsane.org/fullchain.pem";
|
||||
services.dovecot2.sslServerKey = "/var/lib/acme/imap.uninsane.org/key.pem";
|
||||
services.dovecot2.enablePAM = false;
|
||||
|
||||
# sieve scripts require me to set a user for... idk why?
|
||||
services.dovecot2.mailUser = "colin";
|
||||
services.dovecot2.mailGroup = "users";
|
||||
users.users.colin.isSystemUser = lib.mkForce false;
|
||||
|
||||
services.dovecot2.extraConfig =
|
||||
let
|
||||
passwdFile = config.sops.secrets.dovecot_passwd.path;
|
||||
in
|
||||
''
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = ${passwdFile}
|
||||
}
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = ${passwdFile}
|
||||
}
|
||||
|
||||
# allow postfix to query our auth db
|
||||
service auth {
|
||||
unix_listener auth {
|
||||
mode = 0660
|
||||
user = postfix
|
||||
group = postfix
|
||||
}
|
||||
}
|
||||
auth_mechanisms = plain login
|
||||
|
||||
# accept incoming messaging from postfix
|
||||
# service lmtp {
|
||||
# unix_listener dovecot-lmtp {
|
||||
# mode = 0600
|
||||
# user = postfix
|
||||
# group = postfix
|
||||
# }
|
||||
# }
|
||||
|
||||
# plugin {
|
||||
# sieve_plugins = sieve_imapsieve
|
||||
# }
|
||||
|
||||
# mail_debug = yes
|
||||
# auth_debug = yes
|
||||
# verbose_ssl = yes
|
||||
'';
|
||||
|
||||
services.dovecot2.mailboxes = {
|
||||
# special-purpose mailboxes: "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash"
|
||||
# RFC6154 describes these special mailboxes: https://www.ietf.org/rfc/rfc6154.html
|
||||
# how these boxes are treated is 100% up to the client and server to decide.
|
||||
# client behavior:
|
||||
# iOS
|
||||
# - Drafts: ?
|
||||
# - Sent: works
|
||||
# - Trash: works
|
||||
# - Junk: works ("mark" -> "move to Junk")
|
||||
# aerc
|
||||
# - Drafts: works
|
||||
# - Sent: works
|
||||
# - Trash: no; deleted messages are actually deleted
|
||||
# use `:move trash` instead
|
||||
# - Junk: ?
|
||||
# Sent mailbox: all sent messages are copied to it. unclear if this happens server-side or client-side.
|
||||
Drafts = { specialUse = "Drafts"; auto = "create"; };
|
||||
Sent = { specialUse = "Sent"; auto = "create"; };
|
||||
Trash = { specialUse = "Trash"; auto = "create"; };
|
||||
Junk = { specialUse = "Junk"; auto = "create"; };
|
||||
};
|
||||
|
||||
services.dovecot2.mailPlugins = {
|
||||
perProtocol = {
|
||||
# imap.enable = [
|
||||
# "imap_sieve"
|
||||
# ];
|
||||
lda.enable = [
|
||||
"sieve"
|
||||
];
|
||||
# lmtp.enable = [
|
||||
# "sieve"
|
||||
# ];
|
||||
};
|
||||
};
|
||||
environment.systemPackages = [
|
||||
# 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 (?)
|
||||
];
|
||||
services.dovecot2.sieve = {
|
||||
extensions = [ "fileinto" ];
|
||||
# if any messages fail to pass (or lack) DKIM, move them to Junk
|
||||
# XXX the key name ("after") is only used to order sieve execution/ordering
|
||||
scripts.after = builtins.toFile "ensuredkim.sieve" ''
|
||||
require "fileinto";
|
||||
|
||||
if not header :contains "Authentication-Results" "dkim=pass" {
|
||||
fileinto "Junk";
|
||||
stop;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.dovecot.serviceConfig.RestartSec = lib.mkForce "15s"; # nixos defaults this to 1s
|
||||
}
|
@@ -1,225 +0,0 @@
|
||||
# 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, ... }:
|
||||
|
||||
let
|
||||
submissionOptions = {
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||
smtpd_sasl_security_options = "noanonymous";
|
||||
smtpd_sasl_local_domain = "uninsane.org";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
# reuse the virtual map so that sender mapping matches recipient mapping
|
||||
smtpd_sender_login_maps = "hash:/var/lib/postfix/conf/virtual";
|
||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,permit_sasl_authenticated,reject";
|
||||
};
|
||||
in
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
# TODO: mode? could be more granular
|
||||
{ user = "opendkim"; group = "opendkim"; path = "/var/lib/opendkim"; method = "bind"; } #< TODO: migrate to secrets
|
||||
{ user = "root"; group = "root"; path = "/var/spool/mail"; method = "bind"; }
|
||||
# *probably* don't need these dirs:
|
||||
# "/var/lib/dhparams" # https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/dhparams.nix
|
||||
# "/var/lib/dovecot"
|
||||
# "/var/lib/postfix"
|
||||
];
|
||||
|
||||
# XXX(2023/10/20): opening these ports in the firewall has the OPPOSITE effect as intended.
|
||||
# these ports are only routable so long as they AREN'T opened.
|
||||
# probably some cursed interaction with network namespaces introduced after 2023/10/10.
|
||||
# sane.ports.ports."25" = {
|
||||
# protocol = [ "tcp" ];
|
||||
# # XXX visibleTo.lan effectively means "open firewall, but don't configure any NAT/forwarding"
|
||||
# visibleTo.lan = true;
|
||||
# description = "colin-smtp-mx.uninsane.org";
|
||||
# };
|
||||
# sane.ports.ports."465" = {
|
||||
# protocol = [ "tcp" ];
|
||||
# visibleTo.lan = true;
|
||||
# description = "colin-smtps-mx.uninsane.org";
|
||||
# };
|
||||
# sane.ports.ports."587" = {
|
||||
# protocol = [ "tcp" ];
|
||||
# visibleTo.lan = true;
|
||||
# description = "colin-smtps-submission-mx.uninsane.org";
|
||||
# };
|
||||
|
||||
# exists only to manage certs for Postfix
|
||||
services.nginx.virtualHosts."mx.uninsane.org" = {
|
||||
enableACME = true;
|
||||
};
|
||||
|
||||
|
||||
sane.dns.zones."uninsane.org".inet = {
|
||||
MX."@" = "10 mx.uninsane.org.";
|
||||
A."mx" = "%AOVPNS%"; #< XXX: RFC's specify that the MX record CANNOT BE A CNAME. TODO: use "%AOVPNS%?
|
||||
|
||||
# Sender Policy Framework:
|
||||
# +mx => mail passes if it originated from the MX
|
||||
# +a => mail passes if it originated from the A address of this domain
|
||||
# +ip4:.. => mail passes if it originated from this IP
|
||||
# -all => mail fails if none of these conditions were met
|
||||
TXT."@" = "v=spf1 a mx -all";
|
||||
|
||||
# DKIM public key:
|
||||
TXT."mx._domainkey" =
|
||||
"v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkSyMufc2KrRx3j17e/LyB+3eYSBRuEFT8PUka8EDX04QzCwDPdkwgnj3GNDvnB5Ktb05Cf2SJ/S1OLqNsINxJRWtkVfZd/C339KNh9wrukMKRKNELL9HLUw0bczOI4gKKFqyrRE9qm+4csCMAR79Te9FCjGV/jVnrkLdPT0GtFwIDAQAB"
|
||||
;
|
||||
|
||||
# DMARC fields <https://datatracker.ietf.org/doc/html/rfc7489>:
|
||||
# p=none|quarantine|reject: what to do with failures
|
||||
# sp = p but for subdomains
|
||||
# rua = where to send aggregrate reports
|
||||
# ruf = where to send individual failure reports
|
||||
# fo=0|1|d|s controls WHEN to send failure reports
|
||||
# (1=on bad alignment; d=on DKIM failure; s=on SPF failure);
|
||||
# Additionally:
|
||||
# adkim=r|s (is DKIM relaxed [default] or strict)
|
||||
# aspf=r|s (is SPF relaxed [default] or strict)
|
||||
# pct = sampling ratio for punishing failures (default 100 for 100%)
|
||||
# rf = report format
|
||||
# ri = report interval
|
||||
TXT."_dmarc" =
|
||||
"v=DMARC1;p=quarantine;sp=reject;rua=mailto:admin+mail@uninsane.org;ruf=mailto:admin+mail@uninsane.org;fo=1:d:s"
|
||||
;
|
||||
};
|
||||
|
||||
services.postfix.enable = true;
|
||||
services.postfix.hostname = "mx.uninsane.org";
|
||||
services.postfix.origin = "uninsane.org";
|
||||
services.postfix.destination = [ "localhost" "uninsane.org" ];
|
||||
services.postfix.config.smtpd_tls_chain_files = [
|
||||
"/var/lib/acme/mx.uninsane.org/key.pem"
|
||||
"/var/lib/acme/mx.uninsane.org/fullchain.pem"
|
||||
];
|
||||
|
||||
# see: `man 5 virtual`
|
||||
services.postfix.virtual = ''
|
||||
notify.matrix@uninsane.org matrix-synapse
|
||||
@uninsane.org colin
|
||||
'';
|
||||
|
||||
services.postfix.config = {
|
||||
# smtpd_milters = local:/run/opendkim/opendkim.sock
|
||||
# milter docs: http://www.postfix.org/MILTER_README.html
|
||||
# mail filters for receiving email and from authorized SMTP clients (i.e. via submission)
|
||||
# smtpd_milters = inet:$IP:8891
|
||||
# opendkim.sock will add a Authentication-Results header, with `dkim=pass|fail|...` value to received messages
|
||||
smtpd_milters = "unix:/run/opendkim/opendkim.sock";
|
||||
# mail filters for sendmail
|
||||
non_smtpd_milters = "$smtpd_milters";
|
||||
|
||||
# what to do when a milter exits unexpectedly:
|
||||
milter_default_action = "accept";
|
||||
|
||||
inet_protocols = "ipv4";
|
||||
smtp_tls_security_level = "may";
|
||||
|
||||
# hand received mail over to dovecot so that it can run sieves & such
|
||||
mailbox_command = ''${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT"'';
|
||||
|
||||
# hand received mail over to dovecot
|
||||
# virtual_alias_maps = [
|
||||
# "hash:/etc/postfix/virtual"
|
||||
# ];
|
||||
# mydestination = "";
|
||||
# virtual_mailbox_domains = [ "localhost" "uninsane.org" ];
|
||||
# # virtual_mailbox_maps = "hash:/etc/postfix/virtual";
|
||||
# virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||
|
||||
# anti-spam options: <https://www.postfix.org/SMTPD_ACCESS_README.html>
|
||||
# reject_unknown_sender_domain: causes postfix to `dig <sender> MX` and make sure that exists.
|
||||
# but may cause problems receiving mail from google & others who load-balance?
|
||||
# - <https://unix.stackexchange.com/questions/592131/how-to-reject-email-from-unknown-domains-with-postfix-on-centos>
|
||||
# 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.submissionOptions = submissionOptions;
|
||||
services.postfix.enableSubmissions = true;
|
||||
services.postfix.submissionsOptions = submissionOptions;
|
||||
|
||||
systemd.services.postfix.unitConfig.RequiresMountsFor = [
|
||||
"/var/spool/mail" # spooky errors when postfix is run w/o this: `warning: connect #1 to subsystem private/proxymap: Connection refused`
|
||||
"/var/lib/opendkim"
|
||||
];
|
||||
|
||||
# run these behind the OVPN static VPN
|
||||
sane.netns.ovpns.services = [ "opendkim" "postfix" ];
|
||||
|
||||
|
||||
#### OPENDKIM
|
||||
|
||||
services.opendkim.enable = true;
|
||||
# services.opendkim.domains = "csl:uninsane.org";
|
||||
services.opendkim.domains = "uninsane.org";
|
||||
|
||||
# we use a custom (inet) socket, because the default perms
|
||||
# of the unix socket don't allow postfix to connect.
|
||||
# this sits on the machine-local 10.0.1 interface because it's the closest
|
||||
# thing to a loopback interface shared by postfix and opendkim netns.
|
||||
# services.opendkim.socket = "inet:8891@185.157.162.190";
|
||||
# services.opendkim.socket = "local:/run/opendkim.sock";
|
||||
# selectors can be used to disambiguate sender machines.
|
||||
# keeping this the same as the hostname seems simplest
|
||||
services.opendkim.selector = "mx";
|
||||
|
||||
systemd.services.opendkim.serviceConfig = {
|
||||
# /run/opendkim/opendkim.sock needs to be rw by postfix
|
||||
UMask = lib.mkForce "0011";
|
||||
};
|
||||
|
||||
|
||||
#### OUTGOING MESSAGE REWRITING:
|
||||
# - `man 5 header_checks`
|
||||
# - <https://www.postfix.org/header_checks.5.html>
|
||||
# - populates `/var/lib/postfix/conf/header_checks`
|
||||
# XXX(2024-08-06): registration gating via email matches is AWFUL:
|
||||
# 1. bypassed if the service offers localization.
|
||||
# 2. if i try to forward the registration request, it may match the filter again and get sent back to my inbox.
|
||||
# 3. header checks are possibly under-used in the ecosystem, and may break postfix config.
|
||||
# services.postfix.enableHeaderChecks = true;
|
||||
# services.postfix.headerChecks = [
|
||||
# # intercept gitea registration confirmations and manually screen them
|
||||
# {
|
||||
# # headerChecks are somehow ignorant of alias rules: have to redirect to a real user
|
||||
# action = "REDIRECT colin@uninsane.org";
|
||||
# pattern = "/^Subject: Please activate your account/";
|
||||
# }
|
||||
# # intercept Matrix registration confirmations
|
||||
# {
|
||||
# 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/";
|
||||
# # }
|
||||
# ];
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
./nfs.nix
|
||||
./sftpgo
|
||||
];
|
||||
|
||||
users.groups.export = {};
|
||||
|
||||
fileSystems."/var/export/media" = {
|
||||
# everything in here could be considered publicly readable (based on the viewer's legal jurisdiction)
|
||||
device = "/var/media";
|
||||
options = [ "rbind" "nofail" ];
|
||||
};
|
||||
# fileSystems."/var/export/playground" = {
|
||||
# device = config.fileSystems."/mnt/persist/ext".device;
|
||||
# fsType = "btrfs";
|
||||
# options = [
|
||||
# "subvol=export-playground"
|
||||
# "compress=zstd"
|
||||
# "defaults"
|
||||
# ];
|
||||
# };
|
||||
# N.B.: the backing directory should be manually created here **as a btrfs subvolume** and with a quota.
|
||||
# - `sudo btrfs subvolume create /mnt/persist/ext/persist/var/export/playground`
|
||||
# - `sudo btrfs quota enable /mnt/persist/ext/persist/var/export/playground`
|
||||
# - `sudo btrfs quota rescan -sw /mnt/persist/ext/persist/var/export/playground`
|
||||
# to adjust the limits (which apply at the block layer, i.e. post-compression):
|
||||
# - `sudo btrfs qgroup limit 20G /mnt/persist/ext/persist/var/export/playground`
|
||||
# to query the quota/status:
|
||||
# - `sudo btrfs qgroup show -re /var/export/playground`
|
||||
sane.persist.sys.byStore.ext = [
|
||||
{ user = "root"; group = "export"; mode = "0775"; path = "/var/export/playground"; method = "bind"; }
|
||||
];
|
||||
|
||||
sane.fs."/var/export/README.md" = {
|
||||
file.text = ''
|
||||
- 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
|
||||
*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" = {
|
||||
file.text = ''
|
||||
this directory is intentionally read+write by anyone with access.
|
||||
- share files
|
||||
- write poetry
|
||||
- be a friendly troll
|
||||
'';
|
||||
};
|
||||
|
||||
sane.fs."/var/export/.public_for_test/test" = {
|
||||
file.text = ''
|
||||
automated tests read this file to probe connectivity
|
||||
'';
|
||||
};
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
# docs:
|
||||
# - <https://nixos.wiki/wiki/NFS>
|
||||
# - <https://wiki.gentoo.org/wiki/Nfs-utils>
|
||||
# system files:
|
||||
# - /etc/exports
|
||||
# system services:
|
||||
# - nfs-server.service
|
||||
# - nfs-idmapd.service
|
||||
# - nfs-mountd.service
|
||||
# - nfsdcld.service
|
||||
# - rpc-statd.service
|
||||
# - rpcbind.service
|
||||
#
|
||||
# TODO: force files to be 755, or 750.
|
||||
# - could maybe be done with some mount option?
|
||||
|
||||
{ config, lib, ... }:
|
||||
lib.mkIf false #< TODO: remove nfs altogether! it's not exactly the most secure
|
||||
{
|
||||
services.nfs.server.enable = true;
|
||||
|
||||
# see which ports NFS uses with:
|
||||
# - `rpcinfo -p`
|
||||
sane.ports.ports."111" = {
|
||||
protocol = [ "tcp" "udp" ];
|
||||
visibleTo.lan = true;
|
||||
description = "NFS server portmapper";
|
||||
};
|
||||
sane.ports.ports."2049" = {
|
||||
protocol = [ "tcp" "udp" ];
|
||||
visibleTo.lan = true;
|
||||
description = "NFS server";
|
||||
};
|
||||
sane.ports.ports."4000" = {
|
||||
protocol = [ "udp" ];
|
||||
visibleTo.lan = true;
|
||||
description = "NFS server status daemon";
|
||||
};
|
||||
sane.ports.ports."4001" = {
|
||||
protocol = [ "tcp" "udp" ];
|
||||
visibleTo.lan = true;
|
||||
description = "NFS server lock daemon";
|
||||
};
|
||||
sane.ports.ports."4002" = {
|
||||
protocol = [ "tcp" "udp" ];
|
||||
visibleTo.lan = true;
|
||||
description = "NFS server mount daemon";
|
||||
};
|
||||
|
||||
# NFS4 allows these to float, but NFS3 mandates specific ports, so fix them for backwards compat.
|
||||
services.nfs.server.lockdPort = 4001;
|
||||
services.nfs.server.mountdPort = 4002;
|
||||
services.nfs.server.statdPort = 4000;
|
||||
|
||||
services.nfs.extraConfig = ''
|
||||
[nfsd]
|
||||
# XXX: NFS over UDP REQUIRES SPECIAL CONFIG TO AVOID DATA LOSS.
|
||||
# see `man 5 nfs`: "Using NFS over UDP on high-speed links".
|
||||
# it's actually just a general property of UDP over IPv4 (IPv6 fixes it).
|
||||
# both the client and the server should configure a shorter-than-default IPv4 fragment reassembly window to mitigate.
|
||||
# OTOH, tunneling NFS over Wireguard also bypasses this weakness, because a mis-assembled packet would not have a valid signature.
|
||||
udp=y
|
||||
|
||||
[exports]
|
||||
# all export paths are relative to rootdir.
|
||||
# for NFSv4, the export with fsid=0 behaves as `/` publicly,
|
||||
# but NFSv3 implements no such feature.
|
||||
# using `rootdir` instead of relying on `fsid=0` allows consistent export paths regardless of NFS proto version
|
||||
rootdir=/var/export
|
||||
'';
|
||||
|
||||
# format:
|
||||
# fspoint visibility(options)
|
||||
# options:
|
||||
# - see: <https://wiki.gentoo.org/wiki/Nfs-utils#Exports>
|
||||
# - see [man 5 exports](https://linux.die.net/man/5/exports)
|
||||
# - insecure: require clients use src port > 1024
|
||||
# - rw, ro (default)
|
||||
# - async, sync (default)
|
||||
# - no_subtree_check (default), subtree_check: verify not just that files requested by the client live
|
||||
# in the expected fs, but also that they live under whatever subdirectory of that fs is exported.
|
||||
# - no_root_squash, root_squash (default): map requests from uid 0 to user `nobody`.
|
||||
# - crossmnt: reveal filesystems that are mounted under this endpoint
|
||||
# - fsid: must be zero for the root export
|
||||
# - fsid=root is alias for fsid=0
|
||||
# - mountpoint[=/path]: only export the directory if it's a mountpoint. used to avoid exporting failed mounts.
|
||||
# - all_squash: rewrite all client requests such that they come from anonuid/anongid
|
||||
# - any files a user creates are owned by local anonuid/anongid.
|
||||
# - users can read any local file which anonuid/anongid would be able to read.
|
||||
# - users can't chown to/away from anonuid/anongid.
|
||||
# - users can chmod files they own, to anything (making them unreadable to non-`nfsuser` export users, like FTP).
|
||||
# - `stat` remains unchanged, returning the real UIDs/GIDs to the client.
|
||||
# - thus programs which check `uid` or `gid` before trying an operation may incorrectly conclude they can't perform some op.
|
||||
#
|
||||
# 10.0.0.0/8 to export both to LAN (readonly, unencrypted) and wg vpn (read-write, encrypted)
|
||||
services.nfs.server.exports =
|
||||
let
|
||||
fmtExport = { export, baseOpts, extraLanOpts ? [], extraVpnOpts ? [] }:
|
||||
let
|
||||
always = [ "subtree_check" ];
|
||||
lanOpts = always ++ baseOpts ++ extraLanOpts;
|
||||
vpnOpts = always ++ baseOpts ++ extraVpnOpts;
|
||||
in "${export} 10.78.79.0/22(${lib.concatStringsSep "," lanOpts}) 10.0.10.0/24(${lib.concatStringsSep "," vpnOpts})";
|
||||
in lib.concatStringsSep "\n" [
|
||||
(fmtExport {
|
||||
export = "/";
|
||||
baseOpts = [ "crossmnt" "fsid=root" ];
|
||||
extraLanOpts = [ "ro" ];
|
||||
extraVpnOpts = [ "rw" "no_root_squash" ];
|
||||
})
|
||||
(fmtExport {
|
||||
# provide /media as an explicit export. NFSv4 can transparently mount a subdir of an export, but NFSv3 can only mount paths which are exports.
|
||||
export = "/media";
|
||||
baseOpts = [ "crossmnt" ]; # TODO: is crossmnt needed here?
|
||||
extraLanOpts = [ "ro" ];
|
||||
extraVpnOpts = [ "rw" "no_root_squash" ];
|
||||
})
|
||||
(fmtExport {
|
||||
export = "/playground";
|
||||
baseOpts = [
|
||||
"mountpoint"
|
||||
"all_squash"
|
||||
"rw"
|
||||
"anonuid=${builtins.toString config.users.users.nfsuser.uid}"
|
||||
"anongid=${builtins.toString config.users.groups.export.gid}"
|
||||
];
|
||||
})
|
||||
];
|
||||
|
||||
users.users.nfsuser = {
|
||||
description = "virtual user for anonymous NFS operations";
|
||||
group = "export";
|
||||
isSystemUser = true;
|
||||
};
|
||||
}
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i python3 -p python3 -p python3.pkgs.passlib
|
||||
# vim: set filetype=python :
|
||||
#
|
||||
# available environment variables:
|
||||
# - SFTPGO_AUTHD_USERNAME
|
||||
# - SFTPGO_AUTHD_USER
|
||||
# - SFTPGO_AUTHD_IP
|
||||
# - SFTPGO_AUTHD_PROTOCOL = { "DAV", "FTP", "HTTP", "SSH" }
|
||||
# - SFTPGO_AUTHD_PASSWORD
|
||||
# - SFTPGO_AUTHD_PUBLIC_KEY
|
||||
# - SFTPGO_AUTHD_KEYBOARD_INTERACTIVE
|
||||
# - SFTPGO_AUTHD_TLS_CERT
|
||||
#
|
||||
# user permissions:
|
||||
# - see <repo:drakkan/sftpgo:internal/dataprovider/user.go>
|
||||
# - "*" = grant all permissions
|
||||
# - read-only perms:
|
||||
# - "list" = list files and directories
|
||||
# - "download"
|
||||
# - rw perms:
|
||||
# - "upload"
|
||||
# - "overwrite" = allow uploads to replace existing files
|
||||
# - "delete" = delete files and directories
|
||||
# - "delete_files"
|
||||
# - "delete_dirs"
|
||||
# - "rename" = rename files and directories
|
||||
# - "rename_files"
|
||||
# - "rename_dirs"
|
||||
# - "create_dirs"
|
||||
# - "create_symlinks"
|
||||
# - "chmod"
|
||||
# - "chown"
|
||||
# - "chtimes" = change atime/mtime (access and modification times)
|
||||
#
|
||||
# home_dir:
|
||||
# - 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.
|
||||
|
||||
import json
|
||||
import os
|
||||
import passlib.hosts
|
||||
|
||||
from hmac import compare_digest
|
||||
|
||||
authFail = dict(username="")
|
||||
|
||||
PERM_DENY = []
|
||||
PERM_LIST = [ "list" ]
|
||||
PERM_RO = [ "list", "download" ]
|
||||
PERM_RW = [
|
||||
# read-only:
|
||||
"list",
|
||||
"download",
|
||||
# write:
|
||||
"upload",
|
||||
"overwrite",
|
||||
"delete",
|
||||
"rename",
|
||||
"create_dirs",
|
||||
"create_symlinks",
|
||||
# intentionally omitted:
|
||||
# "chmod",
|
||||
# "chown",
|
||||
# "chtimes",
|
||||
]
|
||||
|
||||
TRUSTED_CREDS = [
|
||||
# /etc/shadow style creds.
|
||||
# mkpasswd -m sha-512
|
||||
# $<method>$<salt>$<hash>
|
||||
"$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:
|
||||
return dict(
|
||||
status = 1,
|
||||
username = username,
|
||||
expiration_date = 0,
|
||||
home_dir = "/var/export",
|
||||
# uid/gid 0 means to inherit sftpgo uid.
|
||||
# - i.e. users can't read files which Linux user `sftpgo` can't read
|
||||
# - uploaded files belong to Linux user `sftpgo`
|
||||
# other uid/gid values aren't possible for localfs backend, unless i let sftpgo use `sudo`.
|
||||
uid = 0,
|
||||
gid = 0,
|
||||
# uid = 65534,
|
||||
# gid = 65534,
|
||||
max_sessions = 0,
|
||||
# quota_*: 0 means to not use SFTP's quota system
|
||||
quota_size = 0,
|
||||
quota_files = 0,
|
||||
permissions = permissions,
|
||||
upload_bandwidth = 0,
|
||||
download_bandwidth = 0,
|
||||
filters = dict(
|
||||
allowed_ip = [],
|
||||
denied_ip = [],
|
||||
),
|
||||
public_keys = [],
|
||||
# other fields:
|
||||
# ? groups
|
||||
# ? virtual_folders
|
||||
)
|
||||
|
||||
def isLan(ip: str) -> bool:
|
||||
return ip.startswith("10.78.76.") \
|
||||
or ip.startswith("10.78.77.") \
|
||||
or ip.startswith("10.78.78.") \
|
||||
or ip.startswith("10.78.79.")
|
||||
|
||||
def isWireguard(ip: str) -> bool:
|
||||
return ip.startswith("10.0.10.")
|
||||
|
||||
def isTrustedCred(password: str, credlist: list[str] = TRUSTED_CREDS) -> bool:
|
||||
for cred in credlist:
|
||||
if passlib.hosts.linux_context.verify(password, cred):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getAuthResponse(ip: str, username: str, password: str) -> dict:
|
||||
"""
|
||||
return a sftpgo auth response either denying the user or approving them
|
||||
with a set of permissions.
|
||||
"""
|
||||
if isTrustedCred(password) and username != "colin":
|
||||
# allow r/w access from those with a special token
|
||||
return mkAuthOk(username, permissions = {
|
||||
"/": 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):
|
||||
# allow any user from wireguard
|
||||
return mkAuthOk(username, permissions = {
|
||||
"/": PERM_RW,
|
||||
"/playground": PERM_RW,
|
||||
"/.public_for_test": PERM_RO,
|
||||
})
|
||||
if isLan(ip):
|
||||
if username == "anonymous":
|
||||
# allow anonymous users on the LAN
|
||||
return mkAuthOk("anonymous", permissions = {
|
||||
"/": PERM_RO,
|
||||
"/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
|
||||
|
||||
def main():
|
||||
ip = os.environ.get("SFTPGO_AUTHD_IP", "")
|
||||
username = os.environ.get("SFTPGO_AUTHD_USERNAME", "")
|
||||
password = os.environ.get("SFTPGO_AUTHD_PASSWORD", "")
|
||||
resp = getAuthResponse(ip, username, password)
|
||||
print(json.dumps(resp))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -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
|
@@ -1,64 +0,0 @@
|
||||
# import feeds with e.g.
|
||||
# ```console
|
||||
# $ nix build '.#nixpkgs.freshrss'
|
||||
# $ sudo -u freshrss -g freshrss FRESHRSS_DATA_PATH=/var/lib/freshrss ./result/cli/import-for-user.php --user admin --filename /home/colin/.config/newsflashFeeds.opml
|
||||
# ```
|
||||
#
|
||||
# export feeds with
|
||||
# ```console
|
||||
# $ sudo -u freshrss -g freshrss FRESHRSS_DATA_PATH=/var/lib/freshrss ./result/cli/export-opml-for-user.php --user admin
|
||||
# ```
|
||||
|
||||
{ config, lib, pkgs, sane-lib, ... }:
|
||||
lib.mkIf false #< 2024/07/04: i haven't actively used this for months
|
||||
{
|
||||
sops.secrets."freshrss_passwd" = {
|
||||
owner = config.users.users.freshrss.name;
|
||||
mode = "0400";
|
||||
};
|
||||
sane.persist.sys.byStore.plaintext = [
|
||||
{ user = "freshrss"; group = "freshrss"; path = "/var/lib/freshrss"; method = "bind"; }
|
||||
];
|
||||
|
||||
services.freshrss.enable = true;
|
||||
services.freshrss.baseUrl = "https://rss.uninsane.org";
|
||||
services.freshrss.virtualHost = "rss.uninsane.org";
|
||||
services.freshrss.passwordFile = config.sops.secrets.freshrss_passwd.path;
|
||||
|
||||
systemd.services.freshrss-import-feeds =
|
||||
let
|
||||
feeds = sane-lib.feeds;
|
||||
fresh = config.systemd.services.freshrss-config;
|
||||
all-feeds = config.sane.feeds;
|
||||
wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
|
||||
opml = pkgs.writeText "sane-freshrss.opml" (feeds.feedsToOpml wanted-feeds);
|
||||
in {
|
||||
inherit (fresh) wantedBy environment;
|
||||
serviceConfig = {
|
||||
inherit (fresh.serviceConfig) Type User Group StateDirectory WorkingDirectory
|
||||
# hardening options
|
||||
CapabilityBoundingSet DeviceAllow LockPersonality NoNewPrivileges PrivateDevices PrivateTmp PrivateUsers ProcSubset ProtectClock ProtectControlGroups ProtectHome ProtectHostname ProtectKernelLogs ProtectKernelModules ProtectKernelTunables ProtectProc ProtectSystem RemoveIPC RestrictNamespaces RestrictRealtime RestrictSUIDSGID SystemCallArchitectures SystemCallFilter UMask;
|
||||
};
|
||||
description = "import sane RSS feed list";
|
||||
after = [ "freshrss-config.service" ];
|
||||
script = ''
|
||||
# easiest way to preserve feeds: delete the user, recreate it, import feeds
|
||||
${pkgs.freshrss}/cli/delete-user.php --user colin || true
|
||||
${pkgs.freshrss}/cli/create-user.php --user colin --password "$(cat ${config.services.freshrss.passwordFile})" || true
|
||||
${pkgs.freshrss}/cli/import-for-user.php --user colin --filename ${opml}
|
||||
'';
|
||||
};
|
||||
|
||||
# the default ("*:0/5") is to run every 5 minutes.
|
||||
# `systemctl list-timers` to show
|
||||
systemd.services.freshrss-updater.startAt = lib.mkForce "*:3/30";
|
||||
|
||||
services.nginx.virtualHosts."rss.uninsane.org" = {
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
# the routing is handled by services.freshrss.virtualHost
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet.CNAME."rss" = "native";
|
||||
}
|
@@ -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" ];
|
||||
}
|
@@ -1,201 +0,0 @@
|
||||
# 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, ... }:
|
||||
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
{ user = "git"; group = "gitea"; mode = "0750"; path = "/var/lib/gitea"; method = "bind"; }
|
||||
];
|
||||
|
||||
sane.programs.gitea.enableFor.user.colin = true; # for admin, and monitoring
|
||||
|
||||
services.gitea.enable = true;
|
||||
services.gitea.user = "git"; # default is 'gitea'
|
||||
services.gitea.appName = "Perfectly Sane Git";
|
||||
# services.gitea.disableRegistration = true;
|
||||
|
||||
services.gitea.database.createDatabase = false; # can only createDatabase if user ("git") == dbname ("gitea")
|
||||
services.gitea.database.type = "postgres";
|
||||
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.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
|
||||
users.users.git = {
|
||||
description = "Gitea Service";
|
||||
home = "/var/lib/gitea";
|
||||
useDefaultShell = true;
|
||||
group = "gitea";
|
||||
isSystemUser = true;
|
||||
# sendmail access (not 100% sure if this is necessary)
|
||||
extraGroups = [ "postdrop" ];
|
||||
};
|
||||
|
||||
services.gitea.settings = {
|
||||
# options: "Trace", "Debug", "Info", "Warn", "Error", "Critical"
|
||||
log.LEVEL = "Warn";
|
||||
server = {
|
||||
# options: "home", "explore", "organizations", "login" or URL fragment (or full URL)
|
||||
LANDING_PAGE = "explore";
|
||||
DOMAIN = "git.uninsane.org";
|
||||
ROOT_URL = "https://git.uninsane.org/";
|
||||
};
|
||||
service = {
|
||||
# timeout for email approval. 5760 = 4 days. 10080 = 7 days
|
||||
ACTIVE_CODE_LIVE_MINUTES = 10080;
|
||||
# REGISTER_EMAIL_CONFIRM = false;
|
||||
# REGISTER_EMAIL_CONFIRM = true; #< override REGISTER_MANUAL_CONFIRM
|
||||
REGISTER_MANUAL_CONFIRM = true;
|
||||
# not sure what this notifies *on*...
|
||||
ENABLE_NOTIFY_MAIL = true;
|
||||
# defaults to image-based captcha.
|
||||
# also supports recaptcha (with custom URLs) or hCaptcha.
|
||||
ENABLE_CAPTCHA = true;
|
||||
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;
|
||||
};
|
||||
repository = {
|
||||
DEFAULT_BRANCH = "master";
|
||||
ENABLE_PUSH_CREATE_USER = true;
|
||||
ENABLE_PUSH_CREATE_ORG = true;
|
||||
};
|
||||
other = {
|
||||
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false;
|
||||
};
|
||||
ui = {
|
||||
# options: "gitea-auto" (adapt to system theme), "gitea-dark", "gitea-light"
|
||||
# DEFAULT_THEME = "gitea-auto";
|
||||
# cache frontend assets if true
|
||||
# USE_SERVICE_WORKER = true;
|
||||
};
|
||||
#"ui.meta" = ... to customize html author/description/etc
|
||||
mailer = {
|
||||
# alternative is to use nixos-level config:
|
||||
# services.gitea.mailerPasswordFile = ...
|
||||
ENABLED = true;
|
||||
FROM = "notify.git@uninsane.org";
|
||||
PROTOCOL = "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 = {
|
||||
# options: ANSIC, UnixDate, RubyDate, RFC822, RFC822Z, RFC850, RFC1123, RFC1123Z, RFC3339, RFC3339Nano, Kitchen, Stamp, StampMilli, StampMicro, StampNano
|
||||
# docs: https://pkg.go.dev/time#pkg-constants
|
||||
FORMAT = "RFC3339";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.gitea.wants = [ "postgresql.service" ];
|
||||
systemd.services.gitea.serviceConfig = {
|
||||
# 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.
|
||||
RestrictAddressFamilies = lib.mkForce "~";
|
||||
# add maildrop to allow sendmail to work
|
||||
ReadWritePaths = [
|
||||
"/var/lib/postfix/queue/maildrop"
|
||||
];
|
||||
# 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
|
||||
# TODO: enable publog?
|
||||
services.nginx.virtualHosts."git.uninsane.org" = let
|
||||
# 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
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
extraConfig = ''
|
||||
client_max_body_size 100m;
|
||||
'';
|
||||
|
||||
locations."/" = {
|
||||
proxyPass = proxyPassLight;
|
||||
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.
|
||||
# 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" = {
|
||||
proxyPass = proxyPassLight;
|
||||
recommendedProxySettings = true;
|
||||
extraConfig = ''
|
||||
proxy_hide_header Content-Type;
|
||||
default_type text/html;
|
||||
add_header Content-Type text/html;
|
||||
'';
|
||||
};
|
||||
locations."~ ^/colin/phone-case-cq/raw/.*.js" = {
|
||||
proxyPass = proxyPassLight;
|
||||
recommendedProxySettings = true;
|
||||
extraConfig = ''
|
||||
proxy_hide_header Content-Type;
|
||||
default_type text/html;
|
||||
add_header Content-Type text/javascript;
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet.CNAME."git" = "native";
|
||||
|
||||
sane.ports.ports."22" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.lan = true;
|
||||
visibleTo.doof = true;
|
||||
description = "colin-git@git.uninsane.org";
|
||||
};
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
{ lib, 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/>
|
||||
# log-format setting can be derived with this tool if custom:
|
||||
# - <https://github.com/stockrt/nginx2goaccess>
|
||||
# config options:
|
||||
# - <https://github.com/allinurl/goaccess/blob/master/config/goaccess.conf>
|
||||
|
||||
systemd.services.goaccess = {
|
||||
description = "GoAccess server monitoring";
|
||||
serviceConfig = {
|
||||
ExecStart = ''
|
||||
${lib.getExe pkgs.goaccess} \
|
||||
-f /var/log/nginx/public.log \
|
||||
--log-format=VCOMBINED \
|
||||
--real-time-html \
|
||||
--html-refresh=30 \
|
||||
--no-query-string \
|
||||
--anonymize-ip \
|
||||
--ignore-panel=HOSTS \
|
||||
--ws-url=wss://sink.uninsane.org:443/ws \
|
||||
--port=7890 \
|
||||
-o /var/lib/goaccess/index.html
|
||||
'';
|
||||
ExecReload = "${lib.getExe' pkgs.coreutils "kill"} -HUP $MAINPID";
|
||||
Type = "simple";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10s";
|
||||
|
||||
# hardening
|
||||
# TODO: run as `goaccess` user and add `goaccess` user to group `nginx`.
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = "yes";
|
||||
PrivateTmp = true;
|
||||
ProtectHome = "read-only";
|
||||
ProtectKernelModules = "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" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
# server statistics
|
||||
services.nginx.virtualHosts."sink.uninsane.org" = {
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
root = "/var/lib/goaccess";
|
||||
|
||||
locations."/ws" = {
|
||||
proxyPass = "http://127.0.0.1:7890";
|
||||
recommendedProxySettings = true;
|
||||
# XXX not sure how much of this is necessary
|
||||
extraConfig = ''
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 7d;
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet.CNAME."sink" = "native";
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -1,94 +0,0 @@
|
||||
# admin:
|
||||
# - view stats:
|
||||
# - sudo -u ipfs -g ipfs ipfs -c /var/lib/ipfs/ stats bw
|
||||
# - sudo -u ipfs -g ipfs ipfs -c /var/lib/ipfs/ stats dht
|
||||
# - sudo -u ipfs -g ipfs ipfs -c /var/lib/ipfs/ bitswap stat
|
||||
# - number of open peer connections:
|
||||
# - sudo -u ipfs -g ipfs ipfs -c /var/lib/ipfs/ swarm peers | wc -l
|
||||
|
||||
{ lib, ... }:
|
||||
|
||||
lib.mkIf false # i don't actively use ipfs anymore
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
# TODO: mode? could be more granular
|
||||
{ user = "261"; group = "261"; path = "/var/lib/ipfs"; method = "bind"; }
|
||||
];
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 4001 ];
|
||||
networking.firewall.allowedUDPPorts = [ 4001 ];
|
||||
|
||||
services.nginx.virtualHosts."ipfs.uninsane.org" = {
|
||||
# don't default to ssl upgrades, since this may be dnslink'd from a different domain.
|
||||
# ideally we'd disable ssl entirely, but some places assume it?
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:8080";
|
||||
recommendedProxySettings = true;
|
||||
extraConfig = ''
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Ipfs-Gateway-Prefix "";
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet.CNAME."ipfs" = "native";
|
||||
|
||||
# services.ipfs.enable = true;
|
||||
services.kubo.localDiscovery = true;
|
||||
services.kubo.settings = {
|
||||
Addresses = {
|
||||
Announce = [
|
||||
# "/dns4/ipfs.uninsane.org/tcp/4001"
|
||||
"/dns4/ipfs.uninsane.org/udp/4001/quic"
|
||||
];
|
||||
Swarm = [
|
||||
# "/dns4/ipfs.uninsane.org/tcp/4001"
|
||||
# "/ip4/0.0.0.0/tcp/4001"
|
||||
"/dns4/ipfs.uninsane.org/udp/4001/quic"
|
||||
"/ip4/0.0.0.0/udp/4001/quic"
|
||||
];
|
||||
};
|
||||
Gateway = {
|
||||
# the gateway can only be used to serve content already replicated on this host
|
||||
NoFetch = true;
|
||||
};
|
||||
Swarm = {
|
||||
ConnMgr = {
|
||||
# maintain between LowWater and HighWater peer connections
|
||||
# taken from: https://github.com/ipfs/ipfs-desktop/pull/2055
|
||||
# defaults are 600-900: https://github.com/ipfs/kubo/blob/master/docs/config.md#swarmconnmgr
|
||||
LowWater = 20;
|
||||
HighWater = 40;
|
||||
# default is 20s. i guess more grace period = less churn
|
||||
GracePeriod = "1m";
|
||||
};
|
||||
ResourceMgr = {
|
||||
# docs: https://github.com/libp2p/go-libp2p-resource-manager#resource-scopes
|
||||
Enabled = true;
|
||||
Limits = {
|
||||
System = {
|
||||
Conns = 196;
|
||||
ConnsInbound = 128;
|
||||
ConnsOutbound = 128;
|
||||
FD = 512;
|
||||
Memory = 1073741824; # 1GiB
|
||||
Streams = 1536;
|
||||
StreamsInbound = 1024;
|
||||
StreamsOutbound = 1024;
|
||||
};
|
||||
};
|
||||
};
|
||||
Transports = {
|
||||
Network = {
|
||||
# disable TCP, force QUIC, for lighter resources
|
||||
TCP = false;
|
||||
QUIC = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@@ -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";
|
||||
}
|
||||
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -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>
|
@@ -1,42 +0,0 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config = lib.mkIf (config.sane.maxBuildCost >= 3) {
|
||||
sane.services.kiwix-serve = {
|
||||
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" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
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";
|
||||
};
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
svc-cfg = config.services.komga;
|
||||
inherit (svc-cfg) user group port stateDir;
|
||||
in
|
||||
lib.mkIf false #< 2024/09/30: disabled because i haven't used this for several months
|
||||
{
|
||||
sane.persist.sys.byStore.plaintext = [
|
||||
{ inherit user group; mode = "0700"; path = stateDir; method = "bind"; }
|
||||
];
|
||||
|
||||
services.komga.enable = true;
|
||||
services.komga.port = 11319; # chosen at random
|
||||
|
||||
services.nginx.virtualHosts."komga.uninsane.org" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
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";
|
||||
}
|
@@ -1,179 +0,0 @@
|
||||
# docs:
|
||||
# - <repo:LemmyNet/lemmy:docker/federation/nginx.conf>
|
||||
# - <repo:LemmyNet/lemmy:docker/nginx.conf>
|
||||
# - <repo:LemmyNet/lemmy-ansible:templates/nginx.conf>
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
uiPort = 1234; # default ui port is 1234
|
||||
backendPort = 8536; # default backend port is 8536
|
||||
#^ i guess the "backend" port is used for federation?
|
||||
pict-rs = pkgs.pict-rs;
|
||||
# pict-rs configuration is applied in this order:
|
||||
# - via toml
|
||||
# - via env vars (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.
|
||||
# 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>
|
||||
toml = pkgs.formats.toml { };
|
||||
tomlConfig = toml.generate "pict-rs.toml" pictrsConfig;
|
||||
pictrsConfig = {
|
||||
media.process_timeout = 120;
|
||||
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" ];
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,175 +0,0 @@
|
||||
# docs: <https://nixos.wiki/wiki/Matrix>
|
||||
# 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>
|
||||
#
|
||||
# ENABLING PUSH NOTIFICATIONS (with UnifiedPush/ntfy):
|
||||
# - Matrix "pushers" API spec: <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset>
|
||||
# - first, view notification settings:
|
||||
# - obtain your client's auth token. e.g. Element -> profile -> help/about -> access token.
|
||||
# - `curl --header 'Authorization: Bearer <your_access_token>' localhost:8008/_matrix/client/v3/pushers | jq .`
|
||||
# - enable a new notification destination:
|
||||
# - `curl --header "Authorization: Bearer <your_access_token>" --data '{ "app_display_name": "<topic>", "app_id": "ntfy.uninsane.org", "data": { "url": "https://ntfy.uninsane.org/_matrix/push/v1/notify", "format": "event_id_only" }, "device_display_name": "<topic>", "kind": "http", "lang": "en-US", "profile_tag": "", "pushkey": "<topic>" }' localhost:8008/_matrix/client/v3/pushers/set`
|
||||
# - delete a notification destination by setting `kind` to `null` (otherwise, request is identical to above)
|
||||
#
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
ntfy = config.services.ntfy-sh.enable;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./discord-puppet.nix
|
||||
./irc.nix
|
||||
./signal.nix
|
||||
];
|
||||
|
||||
sane.persist.sys.byStore.private = [
|
||||
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/matrix-synapse"; method = "bind"; }
|
||||
];
|
||||
services.matrix-synapse.enable = true;
|
||||
services.matrix-synapse.log.root.level = "ERROR"; # accepts "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL" (?)
|
||||
services.matrix-synapse.settings = {
|
||||
server_name = "uninsane.org";
|
||||
|
||||
# services.matrix-synapse.enable_registration_captcha = true;
|
||||
# services.matrix-synapse.enable_registration_without_verification = true;
|
||||
# enable_registration = true;
|
||||
# services.matrix-synapse.registration_shared_secret = "<shared key goes here>";
|
||||
|
||||
# default for listeners is port = 8448, tls = true, x_forwarded = false.
|
||||
# we change this because the server is situated behind nginx.
|
||||
listeners = [
|
||||
{
|
||||
port = 8008;
|
||||
bind_addresses = [ "127.0.0.1" ];
|
||||
type = "http";
|
||||
tls = false;
|
||||
x_forwarded = true;
|
||||
resources = [
|
||||
{
|
||||
names = [ "client" "federation" ];
|
||||
compress = false;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
ip_range_whitelist = [
|
||||
# to communicate with ntfy.uninsane.org push notifs.
|
||||
# TODO: move this to some non-shared loopback device: we don't want Matrix spouting http requests to *anything* on this machine
|
||||
"10.78.79.51"
|
||||
];
|
||||
|
||||
x_forwarded = true; # because we proxy matrix behind nginx
|
||||
max_upload_size = "100M"; # default is "50M"
|
||||
|
||||
admin_contact = "admin.matrix@uninsane.org";
|
||||
registrations_require_3pid = [ "email" ];
|
||||
};
|
||||
|
||||
services.matrix-synapse.extraConfigFiles = [
|
||||
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.serviceConfig.RestartSec = 5;
|
||||
systemd.services.matrix-synapse.serviceConfig.RestartMaxDelaySec = 20;
|
||||
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"
|
||||
${lib.getExe pkgs.curl} \
|
||||
--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\" }" \
|
||||
localhost:8008/_matrix/client/v3/pushers/set
|
||||
|
||||
echo "registered push gateways:"
|
||||
${lib.getExe pkgs.curl} \
|
||||
--header "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
localhost:8008/_matrix/client/v3/pushers \
|
||||
| ${lib.getExe pkgs.jq} .
|
||||
'';
|
||||
|
||||
|
||||
# new users may be registered on the CLI:
|
||||
# register_new_matrix_user -c /nix/store/8n6kcka37jhmi4qpd2r03aj71pkyh21s-homeserver.yaml http://localhost:8008
|
||||
#
|
||||
# or provide an registration token then can use to register through the client.
|
||||
# docs: https://github.com/matrix-org/synapse/blob/develop/docs/usage/administration/admin_api/registration_tokens.md
|
||||
# first, grab your own user's access token (Help & About section in Element). then:
|
||||
# curl --header "Authorization: Bearer <my_token>" localhost:8008/_synapse/admin/v1/registration_tokens
|
||||
# create a token with unlimited uses:
|
||||
# curl -d '{}' --header "Authorization: Bearer <my_token>" localhost:8008/_synapse/admin/v1/registration_tokens/new
|
||||
# create a token with limited uses:
|
||||
# curl -d '{ "uses_allowed": 1 }' --header "Authorization: Bearer <my_token>" localhost:8008/_synapse/admin/v1/registration_tokens/new
|
||||
|
||||
# matrix chat server
|
||||
# TODO: was `publog`
|
||||
services.nginx.virtualHosts."matrix.uninsane.org" = {
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
|
||||
# TODO colin: replace this with something helpful to the viewer
|
||||
# locations."/".extraConfig = ''
|
||||
# return 404;
|
||||
# '';
|
||||
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:8008";
|
||||
recommendedProxySettings = true;
|
||||
extraConfig = ''
|
||||
# allow uploading large files (matrix enforces a separate limit, downstream)
|
||||
client_max_body_size 512m;
|
||||
'';
|
||||
};
|
||||
# redirect browsers to the web client.
|
||||
# i don't think native matrix clients ever fetch the root.
|
||||
# ideally this would be put behind some user-agent test though.
|
||||
locations."= /" = {
|
||||
return = "301 https://web.matrix.uninsane.org";
|
||||
};
|
||||
|
||||
# locations."/_matrix" = {
|
||||
# proxyPass = "http://127.0.0.1:8008";
|
||||
# };
|
||||
};
|
||||
|
||||
# matrix web client
|
||||
# docs: https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-element-web
|
||||
services.nginx.virtualHosts."web.matrix.uninsane.org" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
|
||||
root = pkgs.element-web.override {
|
||||
conf = {
|
||||
default_server_config."m.homeserver" = {
|
||||
"base_url" = "https://matrix.uninsane.org";
|
||||
"server_name" = "uninsane.org";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet = {
|
||||
CNAME."matrix" = "native";
|
||||
CNAME."web.matrix" = "native";
|
||||
};
|
||||
|
||||
|
||||
sops.secrets."matrix_synapse_secrets.yaml" = {
|
||||
owner = config.users.users.matrix-synapse.name;
|
||||
};
|
||||
sops.secrets."matrix_access_token" = {
|
||||
owner = config.users.users.matrix-synapse.name;
|
||||
};
|
||||
# provide access to ntfy-sh-topic secret
|
||||
users.users.matrix-synapse.extraGroups = lib.optionals ntfy [ "ntfy-sh" ];
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
{ lib, ... }:
|
||||
|
||||
# XXX mx-discord-puppet uses nodejs_14 which is EOL
|
||||
# - mx-discord-puppet is abandoned upstream _and_ in nixpkgs
|
||||
# - recommended to use mautrix-discord: <https://github.com/NixOS/nixpkgs/pull/200462>
|
||||
lib.mkIf false
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/mx-puppet-discord"; method = "bind"; }
|
||||
];
|
||||
|
||||
services.matrix-synapse.settings.app_service_config_files = [
|
||||
# auto-created by mx-puppet-discord service
|
||||
"/var/lib/mx-puppet-discord/discord-registration.yaml"
|
||||
];
|
||||
|
||||
services.mx-puppet-discord.enable = true;
|
||||
# schema/example: <https://gitlab.com/mx-puppet/discord/mx-puppet-discord/-/blob/main/sample.config.yaml>
|
||||
services.mx-puppet-discord.settings = {
|
||||
bridge = {
|
||||
# port = 8434
|
||||
bindAddress = "127.0.0.1";
|
||||
domain = "uninsane.org";
|
||||
homeserverUrl = "http://127.0.0.1:8008";
|
||||
# displayName = "mx-discord-puppet"; # matrix name for the bot
|
||||
# matrix "groups" were an earlier version of spaces.
|
||||
# maybe the puppet understands this, maybe not?
|
||||
enableGroupSync = false;
|
||||
};
|
||||
presence = {
|
||||
enabled = false;
|
||||
interval = 30000;
|
||||
};
|
||||
provisioning = {
|
||||
# allow these users to control the puppet
|
||||
whitelist = [ "@colin:uninsane\\.org" ];
|
||||
};
|
||||
relay = {
|
||||
whitelist = [ "@colin:uninsane\\.org" ];
|
||||
};
|
||||
selfService = {
|
||||
# who's allowed to use plumbed rooms (idk what that means)
|
||||
whitelist = [ "@colin:uninsane\\.org" ];
|
||||
};
|
||||
logging = {
|
||||
# silly, debug, verbose, info, warn, error
|
||||
console = "debug";
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: should use a dedicated user
|
||||
systemd.services.mx-puppet-discord.serviceConfig = {
|
||||
# fix up to not use /var/lib/private, but just /var/lib
|
||||
DynamicUser = lib.mkForce false;
|
||||
User = "matrix-synapse";
|
||||
Group = "matrix-synapse";
|
||||
};
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
diff --git a/src/irc/ConnectionInstance.ts b/src/irc/ConnectionInstance.ts
|
||||
index 688036ca..3373fa27 100644
|
||||
--- a/src/irc/ConnectionInstance.ts
|
||||
+++ b/src/irc/ConnectionInstance.ts
|
||||
@@ -149,7 +149,7 @@ export class ConnectionInstance {
|
||||
if (this.dead) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
- ircReason = ircReason || reason;
|
||||
+ ircReason = "bye"; // don't reveal through the IRC quit message that we're a bridge
|
||||
log.info(
|
||||
"disconnect()ing %s@%s - %s", this.nick, this.domain, reason
|
||||
);
|
@@ -1,50 +0,0 @@
|
||||
diff --git a/config.schema.yml b/config.schema.yml
|
||||
index 2e71c8d6..42ba8ba1 100644
|
||||
--- a/config.schema.yml
|
||||
+++ b/config.schema.yml
|
||||
@@ -433,7 +433,7 @@ properties:
|
||||
type: "boolean"
|
||||
realnameFormat:
|
||||
type: "string"
|
||||
- enum: ["mxid","reverse-mxid"]
|
||||
+ enum: ["mxid","reverse-mxid","localpart"]
|
||||
ipv6:
|
||||
type: "object"
|
||||
properties:
|
||||
diff --git a/src/irc/IdentGenerator.ts b/src/irc/IdentGenerator.ts
|
||||
index 7a2b5cf1..50f7815a 100644
|
||||
--- a/src/irc/IdentGenerator.ts
|
||||
+++ b/src/irc/IdentGenerator.ts
|
||||
@@ -74,6 +74,9 @@ export class IdentGenerator {
|
||||
else if (server.getRealNameFormat() === "reverse-mxid") {
|
||||
realname = IdentGenerator.sanitiseRealname(IdentGenerator.switchAroundMxid(matrixUser));
|
||||
}
|
||||
+ else if (server.getRealNameFormat() == "localpart") {
|
||||
+ realname = IdentGenerator.sanitiseRealname(matrixUser.localpart);
|
||||
+ }
|
||||
else {
|
||||
throw Error('Invalid value for realNameFormat');
|
||||
}
|
||||
diff --git a/src/irc/IrcServer.ts b/src/irc/IrcServer.ts
|
||||
index 2af73ab4..895b9783 100644
|
||||
--- a/src/irc/IrcServer.ts
|
||||
+++ b/src/irc/IrcServer.ts
|
||||
@@ -101,7 +101,7 @@ export interface IrcServerConfig {
|
||||
};
|
||||
lineLimit: number;
|
||||
userModes?: string;
|
||||
- realnameFormat?: "mxid"|"reverse-mxid";
|
||||
+ realnameFormat?: "mxid"|"reverse-mxid"|"localpart";
|
||||
pingTimeoutMs: number;
|
||||
pingRateMs: number;
|
||||
kickOn: {
|
||||
@@ -289,7 +289,7 @@ export class IrcServer {
|
||||
return this.config.ircClients.userModes || "";
|
||||
}
|
||||
|
||||
- public getRealNameFormat(): "mxid"|"reverse-mxid" {
|
||||
+ public getRealNameFormat(): "mxid"|"reverse-mxid"|"localpart" {
|
||||
return this.config.ircClients.realnameFormat || "mxid";
|
||||
}
|
||||
|
||||
|
@@ -1,194 +0,0 @@
|
||||
# config docs:
|
||||
# - <https://github.com/matrix-org/matrix-appservice-irc/blob/develop/config.sample.yaml>
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
ircServer = { name, additionalAddresses ? [], ssl ? true, sasl ? true, port ? if ssl then 6697 else 6667 }: let
|
||||
lowerName = lib.toLower name;
|
||||
in {
|
||||
# XXX sasl: appservice doesn't support NickServ identification (only SASL, or PASS if sasl = false)
|
||||
inherit additionalAddresses name port sasl ssl;
|
||||
botConfig = {
|
||||
# bot has no presence in IRC channel; only real Matrix users
|
||||
enabled = false;
|
||||
# this is the IRC username/nickname *of the bot* (not visible in channels): not of the end-user.
|
||||
# the irc username/nick of a mapped Matrix user is determined further down in `ircClients` section.
|
||||
# if `enabled` is false, then this name probably never shows up on the IRC side (?)
|
||||
nick = "uninsane";
|
||||
username = "uninsane";
|
||||
joinChannelsIfNoUsers = false;
|
||||
};
|
||||
dynamicChannels = {
|
||||
enabled = true;
|
||||
aliasTemplate = "#irc_${lowerName}_$CHANNEL";
|
||||
published = false; # false => irc rooms aren't listed in homeserver public rooms list
|
||||
federate = false; # false => Matrix users from other homeservers can't join IRC channels
|
||||
};
|
||||
ircClients = {
|
||||
nickTemplate = "$LOCALPARTsane"; # @colin:uninsane.org (Matrix) -> colinsane (IRC)
|
||||
realnameFormat = "reverse-mxid"; # @colin:uninsane.org (Matrix) -> org.uninsane:colin (IRC)
|
||||
# realnameFormat = "localpart"; # @colin:uninsane.org (Matrix) -> colin (IRC) -- but requires the mxid patch below
|
||||
# by default, Matrix will convert messages greater than (3) lines into a pastebin-like URL to send to IRC.
|
||||
lineLimit = 20;
|
||||
# Rizon in particular allows only 4 connections from one IP before a 30min ban.
|
||||
# that's effectively reduced to 2 during a netsplit, or maybe during a restart.
|
||||
# - https://wiki.rizon.net/index.php?title=Connection/Session_Limit_Exemptions
|
||||
# especially, misconfigurations elsewhere in this config may cause hundreds of connections
|
||||
# so this is a safeguard.
|
||||
maxClients = 2;
|
||||
# don't have the bridge disconnect me from IRC when idle.
|
||||
idleTimeout = 0;
|
||||
concurrentReconnectLimit = 2;
|
||||
reconnectIntervalMs = 60000;
|
||||
kickOn = {
|
||||
# remove Matrix user from room when...
|
||||
channelJoinFailure = false;
|
||||
ircConnectionFailure = false;
|
||||
userQuit = true;
|
||||
};
|
||||
};
|
||||
matrixClients = {
|
||||
userTemplate = "@irc_${lowerName}_$NICK"; # the :uninsane.org part is appended automatically
|
||||
};
|
||||
|
||||
# this will let this user message the appservice with `!join #<IRCChannel>` and the rest "Just Works"
|
||||
"@colin:uninsane.org" = "admin";
|
||||
|
||||
membershipLists = {
|
||||
enabled = true;
|
||||
global = {
|
||||
ircToMatrix = {
|
||||
initial = true;
|
||||
incremental = true;
|
||||
requireMatrixJoined = false;
|
||||
};
|
||||
matrixToIrc = {
|
||||
initial = true;
|
||||
incremental = true;
|
||||
};
|
||||
};
|
||||
ignoreIdleUsersOnStartup = {
|
||||
enabled = false; # false => always bridge users, even if idle
|
||||
};
|
||||
};
|
||||
# sync room description?
|
||||
bridgeInfoState = {
|
||||
enabled = true;
|
||||
initial = true;
|
||||
};
|
||||
|
||||
# for per-user IRC password:
|
||||
# - invite @irc_${lowerName}_NickServ:uninsane.org to a DM and type `help` => register
|
||||
# - invite the matrix-appservice-irc user to a DM and type `!help` => add PW to database
|
||||
# to validate that i'm authenticated on the IRC network, DM @irc_${lowerName}_NickServ:uninsane.org:
|
||||
# - send: `STATUS colinsane`
|
||||
# - response should be `3`: "user recognized as owner via password identification"
|
||||
# passwordEncryptionKeyPath = "/path/to/privkey"; # appservice will generate its own if unspecified
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
nixpkgs.overlays = [
|
||||
(next: prev: {
|
||||
matrix-appservice-irc = prev.matrix-appservice-irc.overrideAttrs (super: {
|
||||
patches = super.patches or [] ++ [
|
||||
./irc-no-reveal-bridge.patch
|
||||
# ./irc-no-reveal-mxid.patch
|
||||
];
|
||||
});
|
||||
})
|
||||
];
|
||||
|
||||
sane.persist.sys.byStore.private = [
|
||||
# TODO: mode?
|
||||
{ user = "matrix-appservice-irc"; group = "matrix-appservice-irc"; path = "/var/lib/matrix-appservice-irc"; method = "bind"; }
|
||||
];
|
||||
|
||||
# XXX: matrix-appservice-irc PreStart tries to chgrp the registration.yml to matrix-synapse,
|
||||
# which requires matrix-appservice-irc to be of that group
|
||||
users.users.matrix-appservice-irc.extraGroups = [ "matrix-synapse" ];
|
||||
# weird race conditions around registration.yml mean we want matrix-synapse to be of matrix-appservice-irc group too.
|
||||
users.users.matrix-synapse.extraGroups = [ "matrix-appservice-irc" ];
|
||||
|
||||
services.matrix-synapse.settings.app_service_config_files = [
|
||||
"/var/lib/matrix-appservice-irc/registration.yml" # auto-created by irc appservice
|
||||
];
|
||||
|
||||
services.matrix-appservice-irc.enable = true;
|
||||
services.matrix-appservice-irc.registrationUrl = "http://127.0.0.1:8009";
|
||||
services.matrix-appservice-irc.settings = {
|
||||
homeserver = {
|
||||
url = "http://127.0.0.1:8008";
|
||||
dropMatrixMessagesAfterSecs = 300;
|
||||
domain = "uninsane.org";
|
||||
enablePresence = true;
|
||||
bindPort = 9999;
|
||||
bindHost = "127.0.0.1";
|
||||
};
|
||||
|
||||
ircService = {
|
||||
logging.level = "warn"; # "error", "warn", "info", "debug"
|
||||
mediaProxy.publicUrl = "https://irc.matrix.uninsane.org/media";
|
||||
servers = {
|
||||
"irc.esper.net" = ircServer {
|
||||
name = "esper";
|
||||
sasl = false;
|
||||
# notable channels:
|
||||
# - #merveilles
|
||||
};
|
||||
"irc.libera.chat" = ircServer {
|
||||
name = "libera";
|
||||
sasl = false;
|
||||
# notable channels:
|
||||
# - #hare
|
||||
# - #mnt-reform
|
||||
};
|
||||
"irc.myanonamouse.net" = ircServer {
|
||||
name = "MyAnonamouse";
|
||||
additionalAddresses = [ "irc2.myanonamouse.net" ];
|
||||
sasl = false;
|
||||
};
|
||||
"irc.oftc.net" = ircServer {
|
||||
name = "oftc";
|
||||
sasl = false;
|
||||
# notable channels:
|
||||
# - #sxmo
|
||||
# - #sxmo-offtopic
|
||||
# supposedly also available at <irc://37lnq2veifl4kar7.onion:6667/> (unofficial)
|
||||
};
|
||||
"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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.matrix-appservice-irc.serviceConfig = {
|
||||
# XXX 2023/06/20: nixos specifies this + @aio and @memlock as forbidden
|
||||
# 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";
|
||||
};
|
||||
|
||||
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";
|
||||
};
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
# config options:
|
||||
# - <https://github.com/mautrix/signal/blob/master/mautrix_signal/example-config.yaml>
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
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 = [
|
||||
{ user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; method = "bind"; }
|
||||
{ user = "signald"; group = "signald"; path = "/var/lib/signald"; method = "bind"; }
|
||||
];
|
||||
|
||||
# allow synapse to read the registration file
|
||||
users.users.matrix-synapse.extraGroups = [ "mautrix-signal" ];
|
||||
|
||||
services.signald.enable = true;
|
||||
services.mautrix-signal.enable = true;
|
||||
services.mautrix-signal.environmentFile =
|
||||
config.sops.secrets.mautrix_signal_env.path;
|
||||
|
||||
services.mautrix-signal.settings.signal.socket_path = "/run/signald/signald.sock";
|
||||
services.mautrix-signal.settings.homeserver.domain = "uninsane.org";
|
||||
services.mautrix-signal.settings.bridge.permissions."@colin:uninsane.org" = "admin";
|
||||
services.matrix-synapse.settings.app_service_config_files = [
|
||||
# auto-created by mautrix-signal service
|
||||
"/var/lib/mautrix-signal/signal-registration.yaml"
|
||||
];
|
||||
|
||||
systemd.services.mautrix-signal.serviceConfig = {
|
||||
# allow communication to signald
|
||||
SupplementaryGroups = [ "signald" ];
|
||||
ReadWritePaths = [ "/run/signald" ];
|
||||
};
|
||||
|
||||
sops.secrets."mautrix_signal_env" = {
|
||||
mode = "0440";
|
||||
owner = config.users.users.mautrix-signal.name;
|
||||
group = config.users.users.matrix-synapse.name;
|
||||
};
|
||||
}
|
@@ -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" ];
|
||||
}
|
@@ -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";
|
||||
};
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
{ lib, ... }:
|
||||
|
||||
lib.mkIf false #< i don't actively use navidrome
|
||||
{
|
||||
sane.persist.sys.byStore.plaintext = [
|
||||
{ user = "navidrome"; group = "navidrome"; path = "/var/lib/navidrome"; method = "bind"; }
|
||||
];
|
||||
services.navidrome.enable = true;
|
||||
services.navidrome.settings = {
|
||||
# docs: https://www.navidrome.org/docs/usage/configuration-options/
|
||||
Address = "127.0.0.1";
|
||||
Port = 4533;
|
||||
MusicFolder = "/var/media/Music";
|
||||
CovertArtPriority = "*.jpg, *.JPG, *.png, *.PNG, embedded";
|
||||
AutoImportPlaylists = false;
|
||||
ScanSchedule = "@every 1h";
|
||||
};
|
||||
|
||||
systemd.services.navidrome.serviceConfig = {
|
||||
# fix to use a normal user so we can configure perms correctly
|
||||
DynamicUser = lib.mkForce false;
|
||||
User = "navidrome";
|
||||
Group = "navidrome";
|
||||
};
|
||||
|
||||
users.groups.navidrome = {};
|
||||
|
||||
users.users.navidrome = {
|
||||
group = "navidrome";
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
services.nginx.virtualHosts."music.uninsane.org" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
# inherit kTLS;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:4533";
|
||||
recommendedProxySettings = true;
|
||||
};
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet.CNAME."music" = "native";
|
||||
}
|
@@ -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
|
||||
'';
|
||||
}
|
@@ -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;";
|
||||
};
|
||||
}
|
@@ -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.";
|
||||
};
|
||||
}
|
@@ -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>
|
@@ -1,26 +0,0 @@
|
||||
{ lib, pkgs, ... }:
|
||||
|
||||
lib.optionalAttrs false # disabled until i can be sure it's not gonna OOM my server in the middle of the night
|
||||
{
|
||||
systemd.services.nixos-prebuild = {
|
||||
description = "build a nixos image with all updated deps";
|
||||
path = with pkgs; [ coreutils git nix ];
|
||||
script = ''
|
||||
working=$(mktemp -d nixos-prebuild.XXXXXX --tmpdir)
|
||||
pushd "$working"
|
||||
git clone https://git.uninsane.org/colin/nix-files.git \
|
||||
&& cd nix-files \
|
||||
&& nix flake update \
|
||||
|| true
|
||||
RC=$(nix run "$working/nix-files#check" -- -j1 --cores 5 --builders "")
|
||||
popd
|
||||
rm -rf "$working"
|
||||
exit "$RC"
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.nixos-prebuild = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
timerConfig.OnCalendar = "11,23:00:00";
|
||||
};
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
# ntfy: UnifiedPush notification delivery system
|
||||
# - used to get push notifications out of Matrix and onto a Phone (iOS, Android, or a custom client)
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
./ntfy-waiter.nix
|
||||
./ntfy-sh.nix
|
||||
];
|
||||
sops.secrets."ntfy-sh-topic" = lib.mkIf config.services.ntfy-sh.enable {
|
||||
mode = "0440";
|
||||
owner = config.users.users.ntfy-sh.name;
|
||||
group = config.users.users.ntfy-sh.name;
|
||||
};
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
# ntfy: UnifiedPush notification delivery system
|
||||
# - used to get push notifications out of Matrix and onto a Phone (iOS, Android, or a custom client)
|
||||
#
|
||||
# config options:
|
||||
# - <https://docs.ntfy.sh/config/#config-options>
|
||||
#
|
||||
# usage:
|
||||
# - ntfy sub https://ntfy.uninsane.org/TOPIC
|
||||
# - ntfy pub https://ntfy.uninsane.org/TOPIC "my message"
|
||||
# in production, TOPIC is a shared secret between the publisher (Matrix homeserver) and the subscriber (phone)
|
||||
#
|
||||
# administering:
|
||||
# - sudo -u ntfy-sh ntfy access
|
||||
#
|
||||
# debugging:
|
||||
# - make sure that the keepalives are good:
|
||||
# - on the subscriber machine, run `lsof -i4` to find the port being used
|
||||
# - `sudo tcpdump tcp port <p>`
|
||||
# - shouldn't be too spammy
|
||||
#
|
||||
# matrix integration:
|
||||
# - the user must manually point synapse to the ntfy endpoint:
|
||||
# - `curl --header "Authorization: <your_token>" --data '{ "app_display_name": "sane-nix moby", "app_id": "ntfy.uninsane.org", "data": { "url": "https://ntfy.uninsane.org/_matrix/push/v1/notify", "format": "event_id_only" }, "device_display_name": "sane-nix moby", "kind": "http", "lang": "en-US", "profile_tag": "", "pushkey": "https://ntfy.uninsane.org/TOPIC" }' localhost:8008/_matrix/client/v3/pushers/set`
|
||||
# where the token is grabbed from Element's help&about page when logged in
|
||||
# - to remove, send this `curl` with `"kind": null`
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
# subscribers need a non-443 public port to listen on as a way to easily differentiate this traffic
|
||||
# at the IP layer, to enable e.g. wake-on-lan.
|
||||
altPort = 2587;
|
||||
in
|
||||
lib.mkIf false #< 2024/09/30: disabled because i haven't used it in several months
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
# not 100% necessary to persist this, but ntfy does keep a 12hr (by default) cache
|
||||
# for pushing notifications to users who become offline.
|
||||
# ACLs also live here.
|
||||
{ user = "ntfy-sh"; group ="ntfy-sh"; path = "/var/lib/ntfy-sh"; method = "bind"; }
|
||||
];
|
||||
|
||||
services.ntfy-sh.enable = true;
|
||||
services.ntfy-sh.settings = {
|
||||
base-url = "https://ntfy.uninsane.org";
|
||||
behind-proxy = true; # not sure if needed
|
||||
# keepalive interval is a ntfy-specific keepalive thing, where it sends actual data down the wire.
|
||||
# it's not simple TCP keepalive.
|
||||
# defaults to 45s.
|
||||
# note that the client may still do its own TCP-level keepalives, typically every 30s
|
||||
keepalive-interval = "15m";
|
||||
log-level = "info"; # trace, debug, info (default), warn, error
|
||||
auth-default-access = "deny-all";
|
||||
};
|
||||
systemd.services.ntfy-sh.serviceConfig.DynamicUser = lib.mkForce false;
|
||||
systemd.services.ntfy-sh.preStart = ''
|
||||
# make this specific topic read-write by world
|
||||
# it would be better to use the token system, but that's extra complexity for e.g.
|
||||
# how do i plumb a secret into the Matrix notification pusher
|
||||
#
|
||||
# note that this will fail upon first run, i.e. before ntfy has created its db.
|
||||
# just restart the service.
|
||||
topic=$(cat ${config.sops.secrets.ntfy-sh-topic.path})
|
||||
${lib.getExe' pkgs.ntfy-sh "ntfy"} access everyone "$topic" read-write
|
||||
'';
|
||||
|
||||
|
||||
services.nginx.virtualHosts."ntfy.uninsane.org" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
listen = [
|
||||
{ addr = "0.0.0.0"; port = altPort; ssl = true; }
|
||||
{ addr = "0.0.0.0"; port = 443; ssl = true; }
|
||||
{ addr = "0.0.0.0"; port = 80; ssl = false; }
|
||||
];
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:2586";
|
||||
proxyWebsockets = true; #< support websocket upgrades. without that, `ntfy sub` hangs silently
|
||||
recommendedProxySettings = true; #< adds headers so ntfy logs include the real IP
|
||||
extraConfig = ''
|
||||
# absurdly long timeout (86400s=24h) so that we never hang up on clients.
|
||||
# make sure the client is smart enough to detect a broken proxy though!
|
||||
proxy_read_timeout 86400s;
|
||||
'';
|
||||
};
|
||||
};
|
||||
sane.dns.zones."uninsane.org".inet.CNAME."ntfy" = "native";
|
||||
|
||||
sane.ports.ports."${builtins.toString altPort}" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.lan = true;
|
||||
visibleTo.doof = true;
|
||||
description = "colin-ntfy.uninsane.org";
|
||||
};
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i python3 -p ntfy-sh -p python3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LISTEN_QUEUE = 3
|
||||
WAKE_MESSAGE = b'notification\n'
|
||||
|
||||
class Client:
|
||||
def __init__(self, sock, addr_info, live_after: float):
|
||||
self.live_after = live_after
|
||||
self.sock = sock
|
||||
self.addr_info = addr_info
|
||||
|
||||
def __cmp__(self, other: 'Client'):
|
||||
return cmp(self.addr_info, other.addr_info)
|
||||
|
||||
def try_notify(self, message: bytes) -> bool:
|
||||
"""
|
||||
returns true if we send a packet to notify client.
|
||||
fals otherwise (e.g. the socket is dead).
|
||||
"""
|
||||
ttl = self.live_after - time.time()
|
||||
if ttl > 0:
|
||||
logger.debug(f"sleeping {ttl:.2f}s until client {self.addr_info} is ready to receive notification")
|
||||
time.sleep(ttl)
|
||||
|
||||
try:
|
||||
self.sock.sendall(message)
|
||||
except Exception as e:
|
||||
logger.warning(f"failed to notify client {self.addr_info} {e}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"successfully notified {self.addr_info}: {message}")
|
||||
return True
|
||||
|
||||
class Adapter:
|
||||
def __init__(self, host: str, port: int, silence: int, topic: str):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.silence = silence
|
||||
self.topic = topic
|
||||
self.clients = set()
|
||||
|
||||
def log_clients(self):
|
||||
clients_str = '\n'.join(f' {c.addr_info}' for c in self.clients)
|
||||
logger.debug(f"clients alive ({len(self.clients)}):\n{clients_str}")
|
||||
|
||||
def add_client(self, client: Client):
|
||||
# it's a little bit risky to keep more than one client at the same IP address,
|
||||
# because it's possible a notification comes in and we ring the old connection,
|
||||
# even when the new connection says "don't ring yet".
|
||||
for c in set(self.clients):
|
||||
if c.addr_info[0] == client.addr_info[0]:
|
||||
logger.info(f"purging old client before adding new one at same address: {c.addr_info} -> {client.addr_info}")
|
||||
self.clients.remove(c)
|
||||
|
||||
logger.info(f"accepted client at {client.addr_info}")
|
||||
self.clients.add(client)
|
||||
|
||||
def listener_loop(self):
|
||||
logger.info(f"listening for connections on {self.host}:{self.port}")
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind((self.host, self.port))
|
||||
s.listen(LISTEN_QUEUE)
|
||||
while True:
|
||||
conn, addr_info = s.accept()
|
||||
self.add_client(Client(conn, addr_info, live_after = time.time() + self.silence))
|
||||
|
||||
def notify_clients(self, message: bytes = WAKE_MESSAGE):
|
||||
# notify every client, and drop any which have disconnected.
|
||||
# note that we notify based on age (oldest -> youngest)
|
||||
# because notifying young clients might entail sleeping until they're ready.
|
||||
clients = sorted(self.clients, key=lambda c: (c.live_after, c.addr_info))
|
||||
|
||||
dead_clients = [
|
||||
c for c in clients if not c.try_notify(message)
|
||||
]
|
||||
for c in dead_clients:
|
||||
self.clients.remove(c)
|
||||
|
||||
self.log_clients()
|
||||
|
||||
def notify_loop(self):
|
||||
logger.info("waiting for notification events")
|
||||
ntfy_proc = subprocess.Popen(
|
||||
[
|
||||
"ntfy",
|
||||
"sub",
|
||||
f"https://ntfy.uninsane.org/{self.topic}"
|
||||
],
|
||||
stdout=subprocess.PIPE
|
||||
)
|
||||
for line in iter(ntfy_proc.stdout.readline, b''):
|
||||
logger.debug(f"received notification: {line}")
|
||||
self.notify_clients()
|
||||
|
||||
def get_topic() -> str:
|
||||
return open('/run/secrets/ntfy-sh-topic', 'rt').read().strip()
|
||||
|
||||
def run_forever(callable):
|
||||
try:
|
||||
callable()
|
||||
except Exception as e:
|
||||
logger.error(f"{callable} failed: {e}")
|
||||
else:
|
||||
logger.error(f"{callable} unexpectedly returned")
|
||||
# sys.exit(1)
|
||||
os._exit(1) # sometimes `sys.exit()` doesn't actually exit...
|
||||
|
||||
def main():
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
parser = argparse.ArgumentParser(description="accept connections and notify the other end upon ntfy activity, with a guaranteed amount of silence")
|
||||
parser.add_argument('--verbose', action='store_true')
|
||||
parser.add_argument('--host', type=str, default='')
|
||||
parser.add_argument('--port', type=int)
|
||||
parser.add_argument('--silence', type=int, help="number of seconds to remain silent upon accepting a connection")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
adapter = Adapter(args.host, args.port, args.silence, get_topic())
|
||||
|
||||
listener_loop = threading.Thread(target=run_forever, name="listener_loop", args=(adapter.listener_loop,))
|
||||
notify_loop = threading.Thread(target=run_forever, name="notify_loop", args=(adapter.notify_loop,))
|
||||
|
||||
# TODO: this method of exiting seems to sometimes leave the listener behind (?)
|
||||
# preventing anyone else from re-binding the port.
|
||||
listener_loop.start()
|
||||
notify_loop.start()
|
||||
listener_loop.join()
|
||||
notify_loop.join()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@@ -1,72 +0,0 @@
|
||||
# service which adapts ntfy-sh into something suitable specifically for the Pinephone's
|
||||
# wake-on-lan (WoL) feature.
|
||||
# notably, it provides a mechanism by which the caller can be confident of an interval in which
|
||||
# zero traffic will occur on the TCP connection, thus allowing it to enter sleep w/o fear of hitting
|
||||
# race conditions in the Pinephone WoL feature.
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.sane.ntfy-waiter;
|
||||
portLow = 5550;
|
||||
portHigh = 5559;
|
||||
portRange = lib.range portLow portHigh;
|
||||
numPorts = portHigh - portLow + 1;
|
||||
mkService = port: let
|
||||
silence = port - portLow;
|
||||
flags = lib.optional cfg.verbose "--verbose";
|
||||
cli = [
|
||||
(lib.getExe cfg.package)
|
||||
"--port"
|
||||
"${builtins.toString port}"
|
||||
"--silence"
|
||||
"${builtins.toString silence}"
|
||||
] ++ flags;
|
||||
in {
|
||||
"ntfy-waiter-${builtins.toString silence}" = {
|
||||
# TODO: run not as root (e.g. as ntfy-sh)
|
||||
description = "wait for notification, with ${builtins.toString silence} seconds of guaranteed silence";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
ExecStart = lib.concatStringsSep " " cli;
|
||||
};
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "ntfy-sh.service" ];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options = with lib; {
|
||||
sane.ntfy-waiter.enable = mkOption {
|
||||
type = types.bool;
|
||||
default = config.services.ntfy-sh.enable;
|
||||
};
|
||||
sane.ntfy-waiter.verbose = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
sane.ntfy-waiter.package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.static-nix-shell.mkPython3 {
|
||||
pname = "ntfy-waiter";
|
||||
srcRoot = ./.;
|
||||
pkgs = [ "ntfy-sh" ];
|
||||
};
|
||||
description = ''
|
||||
exposed to provide an attr-path by which one may build the package for manual testing.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
sane.ports.ports = lib.mkMerge (lib.forEach portRange (port: {
|
||||
"${builtins.toString port}" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-notification-waiter-${builtins.toString (port - portLow + 1)}-of-${builtins.toString numPorts}";
|
||||
};
|
||||
}));
|
||||
systemd.services = lib.mkMerge (builtins.map mkService portRange);
|
||||
};
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
# pict-rs is an image database/store used by Lemmy.
|
||||
# i don't explicitly activate it here -- just adjust its defaults to be a bit friendlier
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.services.pict-rs;
|
||||
in
|
||||
{
|
||||
sane.persist.sys.byStore.plaintext = lib.mkIf cfg.enable [
|
||||
{ user = "pict-rs"; group = "pict-rs"; path = cfg.dataDir; method = "bind"; }
|
||||
];
|
||||
|
||||
systemd.services.pict-rs.serviceConfig = {
|
||||
# fix to use a normal user so we can configure perms correctly
|
||||
DynamicUser = lib.mkForce false;
|
||||
User = "pict-rs";
|
||||
Group = "pict-rs";
|
||||
};
|
||||
users.groups.pict-rs = {};
|
||||
users.users.pict-rs = {
|
||||
group = "pict-rs";
|
||||
isSystemUser = true;
|
||||
};
|
||||
}
|
@@ -1,222 +0,0 @@
|
||||
# docs:
|
||||
# - <https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/networking/pleroma.nix>
|
||||
# - <https://docs.pleroma.social/backend/configuration/cheatsheet/>
|
||||
# example config:
|
||||
# - <https://git.pleroma.social/pleroma/pleroma/-/blob/develop/config/config.exs>
|
||||
#
|
||||
# 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>
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
logLevel = "warning";
|
||||
# logLevel = "debug";
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (config.sane.maxBuildCost >= 2) {
|
||||
sane.persist.sys.byStore.private = [
|
||||
# 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.configs = [
|
||||
''
|
||||
import Config
|
||||
|
||||
config :pleroma, Pleroma.Web.Endpoint,
|
||||
url: [host: "fed.uninsane.org", scheme: "https", port: 443],
|
||||
http: [ip: {127, 0, 0, 1}, port: 4040]
|
||||
# secret_key_base: "{secrets.pleroma.secret_key_base}",
|
||||
# signing_salt: "{secrets.pleroma.signing_salt}"
|
||||
|
||||
config :pleroma, :instance,
|
||||
name: "Perfectly Sane",
|
||||
description: "Single-user Pleroma instance",
|
||||
email: "admin.pleroma@uninsane.org",
|
||||
notify_email: "notify.pleroma@uninsane.org",
|
||||
limit: 5000,
|
||||
registrations_open: true,
|
||||
account_approval_required: true,
|
||||
max_pinned_statuses: 5,
|
||||
external_user_synchronization: true
|
||||
|
||||
# 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
|
||||
config :pleroma, Pleroma.Emails.Mailer,
|
||||
enabled: true,
|
||||
adapter: Swoosh.Adapters.Sendmail,
|
||||
cmd_path: "${lib.getExe' pkgs.postfix "sendmail"}"
|
||||
|
||||
config :pleroma, Pleroma.User,
|
||||
restricted_nicknames: [ "admin", "uninsane", "root" ]
|
||||
|
||||
config :pleroma, :media_proxy,
|
||||
enabled: false,
|
||||
redirect_on_failure: true
|
||||
#base_url: "https://cache.pleroma.social"
|
||||
|
||||
# see for reference:
|
||||
# - `force_custom_plan`: <https://docs.pleroma.social/backend/configuration/postgresql/#disable-generic-query-plans>
|
||||
config :pleroma, Pleroma.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "pleroma",
|
||||
database: "pleroma",
|
||||
hostname: "localhost",
|
||||
pool_size: 10,
|
||||
prepare: :named,
|
||||
parameters: [
|
||||
plan_cache_mode: "force_custom_plan"
|
||||
]
|
||||
# XXX: prepare: :named is needed only for PG <= 12
|
||||
# prepare: :named,
|
||||
# password: "{secrets.pleroma.db_password}",
|
||||
|
||||
# Configure web push notifications
|
||||
config :web_push_encryption, :vapid_details,
|
||||
subject: "mailto:notify.pleroma@uninsane.org"
|
||||
# public_key: "{secrets.pleroma.vapid_public_key}",
|
||||
# private_key: "{secrets.pleroma.vapid_private_key}"
|
||||
|
||||
# config :joken, default_signer: "{secrets.pleroma.joken_default_signer}"
|
||||
|
||||
config :pleroma, :database, rum_enabled: false
|
||||
config :pleroma, :instance, static_dir: "/var/lib/pleroma/instance/static"
|
||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
|
||||
config :pleroma, configurable_from_database: false
|
||||
|
||||
# strip metadata from uploaded images
|
||||
config :pleroma, Pleroma.Upload, filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
|
||||
|
||||
# fix log spam: <https://git.pleroma.social/pleroma/pleroma/-/issues/1659>
|
||||
# specifically, remove LAN addresses from `reserved`
|
||||
config :pleroma, Pleroma.Web.Plugs.RemoteIp,
|
||||
enabled: true,
|
||||
reserved: ["127.0.0.0/8", "::1/128", "fc00::/7", "172.16.0.0/12"]
|
||||
|
||||
# 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)
|
||||
# Enable Strict-Transport-Security once SSL is working:
|
||||
config :pleroma, :http_security,
|
||||
sts: true
|
||||
|
||||
# docs: https://docs.pleroma.social/backend/configuration/cheatsheet/#logger
|
||||
config :logger,
|
||||
backends: [{ExSyslogger, :ex_syslogger}]
|
||||
|
||||
config :logger, :ex_syslogger,
|
||||
level: :${logLevel}
|
||||
|
||||
# policies => list of message rewriting facilities to be enabled
|
||||
# transparence => whether to publish these rules in node_info (and /about)
|
||||
config :pleroma, :mrf,
|
||||
policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy],
|
||||
transparency: true
|
||||
|
||||
# reject => { host, reason }
|
||||
config :pleroma, :mrf_simple,
|
||||
reject: [ {"threads.net", "megacorp"}, {"*.threads.net", "megacorp"} ]
|
||||
# reject: [ [host: "threads.net", reason: "megacorp"], [host: "*.threads.net", reason: "megacorp"] ]
|
||||
|
||||
# XXX colin: not sure if this actually _does_ anything
|
||||
# better to steal emoji from other instances?
|
||||
# - <https://docs.pleroma.social/backend/configuration/cheatsheet/#mrf_steal_emoji>
|
||||
config :pleroma, :emoji,
|
||||
shortcode_globs: ["/emoji/**/*.png"],
|
||||
groups: [
|
||||
"Cirno": "/emoji/cirno/*.png",
|
||||
"Kirby": "/emoji/kirby/*.png",
|
||||
"Bun": "/emoji/bun/*.png",
|
||||
"Yuru Camp": "/emoji/yuru_camp/*.png",
|
||||
]
|
||||
''
|
||||
];
|
||||
|
||||
systemd.services.pleroma.path = [
|
||||
# something inside pleroma invokes `sh` w/o specifying it by path, so this is needed to allow pleroma to start
|
||||
pkgs.bash
|
||||
# used by Pleroma to strip geo tags from uploads
|
||||
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
|
||||
# config.sane.programs.gawk.package
|
||||
# needed for email operations like password reset
|
||||
pkgs.postfix
|
||||
];
|
||||
|
||||
systemd.services.pleroma = {
|
||||
# postgres can be slow to service early requests, preventing pleroma from starting on the first try
|
||||
serviceConfig.Restart = "on-failure";
|
||||
serviceConfig.RestartSec = "10s";
|
||||
|
||||
# hardening (systemd-analyze security pleroma)
|
||||
# XXX(2024-07-28): this hasn't been rigorously tested:
|
||||
# possible that i've set something too strict and won't notice right away
|
||||
# make sure to test:
|
||||
# - image/media uploading
|
||||
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";
|
||||
serviceConfig.ProcSubset = "all"; #< needs /proc/sys/kernel/overflowuid for bwrap
|
||||
|
||||
serviceConfig.ProtectClock = true;
|
||||
serviceConfig.ProtectControlGroups = true;
|
||||
serviceConfig.ProtectHome = true;
|
||||
serviceConfig.ProtectKernelModules = true;
|
||||
serviceConfig.ProtectSystem = lib.mkForce "strict";
|
||||
serviceConfig.RemoveIPC = true;
|
||||
serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
|
||||
|
||||
serviceConfig.RestrictSUIDSGID = true;
|
||||
serviceConfig.SystemCallArchitectures = "native";
|
||||
serviceConfig.SystemCallFilter = [ "@system-service" "@mount" "@sandbox" ]; #< "sandbox" might not actually be necessary
|
||||
|
||||
serviceConfig.ProtectHostname = false; #< else brap can't mount /proc
|
||||
serviceConfig.ProtectKernelLogs = false; #< else breaks exiftool ("bwrap: Can't mount proc on /newroot/proc: Operation not permitted")
|
||||
serviceConfig.ProtectKernelTunables = false; #< else breaks exiftool
|
||||
serviceConfig.RestrictNamespaces = false; # media uploads require bwrap
|
||||
};
|
||||
|
||||
# this is required to allow pleroma to send email.
|
||||
# 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" ];
|
||||
|
||||
# Pleroma server and web interface
|
||||
# TODO: enable publog?
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
{ lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
GiB = n: MiB 1024*n;
|
||||
MiB = n: KiB 1024*n;
|
||||
KiB = n: 1024*n;
|
||||
in
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
{ user = "postgres"; group = "postgres"; mode = "0750"; path = "/var/lib/postgresql"; method = "bind"; }
|
||||
{ user = "postgres"; group = "postgres"; mode = "0750"; path = "/var/backup/postgresql"; method = "bind"; }
|
||||
];
|
||||
services.postgresql.enable = true;
|
||||
|
||||
# HOW TO UPDATE:
|
||||
# postgres version updates are manual and require intervention.
|
||||
# - `sane-stop-all-servo`
|
||||
# - `systemctl start postgresql`
|
||||
# - as `sudo su postgres`:
|
||||
# - `cd /var/log/postgresql`
|
||||
# - `pg_dumpall > state.sql`
|
||||
# - `echo placeholder > <new_version>` # to prevent state from being created earlier than we want
|
||||
# - then, atomically:
|
||||
# - update the `services.postgresql.package` here
|
||||
# - `dataDir` is atomically updated to match package; don't touch
|
||||
# - `nixos-rebuild --flake . switch ; sane-stop-all-servo`
|
||||
# - `sudo rm -rf /var/lib/postgresql/<new_version>`
|
||||
# - `systemctl start postgresql`
|
||||
# - as `sudo su postgres`:
|
||||
# - `cd /var/lib/postgreql`
|
||||
# - `psql -f state.sql`
|
||||
# (for a compressed dump: `gunzip --stdout state.sql.gz | psql`)
|
||||
# - restart dependent services (maybe test one at a time)
|
||||
|
||||
services.postgresql.package = pkgs.postgresql_16;
|
||||
|
||||
|
||||
# XXX colin: for a proper deploy, we'd want to include something for Pleroma here too.
|
||||
# services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
|
||||
# CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD '<password goes here>';
|
||||
# CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
|
||||
# TEMPLATE template0
|
||||
# ENCODING = "UTF8"
|
||||
# LC_COLLATE = "C"
|
||||
# LC_CTYPE = "C";
|
||||
# '';
|
||||
|
||||
services.postgresql.settings = {
|
||||
# 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>
|
||||
# DB Version: 16
|
||||
# OS Type: linux
|
||||
# DB Type: web
|
||||
# vvv artificially constrained because the server's resources are shared across maaany services
|
||||
# Total Memory (RAM): 12 GB
|
||||
# CPUs num: 12
|
||||
# Data Storage: ssd
|
||||
max_connections = 200;
|
||||
shared_buffers = "3GB";
|
||||
effective_cache_size = "9GB";
|
||||
maintenance_work_mem = "768MB";
|
||||
checkpoint_completion_target = 0.9;
|
||||
wal_buffers = "16MB";
|
||||
default_statistics_target = 100;
|
||||
random_page_cost = 1.1;
|
||||
effective_io_concurrency = 200;
|
||||
work_mem = "3932kB";
|
||||
min_wal_size = "1GB";
|
||||
max_wal_size = "4GB";
|
||||
max_worker_processes = 12;
|
||||
max_parallel_workers_per_gather = 4;
|
||||
max_parallel_workers = 12;
|
||||
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
|
||||
services.postgresqlBackup.enable = true;
|
||||
|
||||
# common admin operations:
|
||||
# sudo systemctl start postgresql
|
||||
# sudo -u postgres psql
|
||||
# > \l # lists all databases
|
||||
# > \du # lists all roles
|
||||
# > \c pleroma # connects to database by name
|
||||
# > \d # shows all tables
|
||||
# > \q # exits psql
|
||||
# dump/restore (-F t = tar):
|
||||
# sudo -u postgres pg_dump -F t pleroma > /backup/pleroma-db.tar
|
||||
# sudo -u postgres -g postgres pg_restore -d pleroma /backup/pleroma-db.tar
|
||||
}
|
@@ -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."
|
@@ -1,309 +0,0 @@
|
||||
# example configs:
|
||||
# - official: <https://prosody.im/doc/example_config>
|
||||
# - nixos: <https://github.com/kittywitch/nixfiles/blob/main/services/prosody.nix>
|
||||
# config options:
|
||||
# - <https://prosody.im/doc/configure>
|
||||
#
|
||||
# modules:
|
||||
# - main: <https://prosody.im/doc/modules>
|
||||
# - community: <https://modules.prosody.im/index.html>
|
||||
#
|
||||
# debugging:
|
||||
# - logging:
|
||||
# - enable `stanza_debug` module
|
||||
# - enable `log.debug = "*syslog"` in extraConfig
|
||||
# - interactive:
|
||||
# - `telnet localhost 5582` (this is equal to `prosodyctl shell` -- but doesn't hang)
|
||||
# - `watch:stanzas(target_spec, filter)` -> to log stanzas, for version > 0.12
|
||||
# - console docs: <https://prosody.im/doc/console>
|
||||
# - can modify/inspect arbitrary internals (lua) by prefixing line with `> `
|
||||
# - e.g. `> _G` to print all globals
|
||||
#
|
||||
# sanity checks:
|
||||
# - `sudo -u prosody -g prosody prosodyctl check connectivity`
|
||||
# - `sudo -u prosody -g prosody prosodyctl check turn`
|
||||
# - `sudo -u prosody -g prosody prosodyctl check turn -v --ping=stun.conversations.im`
|
||||
# - checks that my stun/turn server is usable by clients of conversations.im (?)
|
||||
# - `sudo -u prosody -g prosody prosodyctl check` (dns, config, certs)
|
||||
#
|
||||
#
|
||||
# create users with:
|
||||
# - `sudo -u prosody prosodyctl adduser colin@uninsane.org`
|
||||
#
|
||||
#
|
||||
# federation/support matrix:
|
||||
# - nixnet.services (runs ejabberd):
|
||||
# - WORKS: sending and receiving PMs and calls (2023/10/15)
|
||||
# - N.B.: it didn't originally work; was solved by disabling the lua-unbound DNS option & forcing the system/local resolver
|
||||
# - cheogram (XMPP <-> SMS gateway):
|
||||
# - WORKS: sending and receiving PMs, images (2023/10/15)
|
||||
# - PARTIAL: calls (xmpp -> tel works; tel -> xmpp fails)
|
||||
# - maybe i need to setup stun/turn
|
||||
#
|
||||
# TODO:
|
||||
# - MIGRATE TO NIXOS MODULE OPTIONS:
|
||||
# - `services.prosody.ssl.`...
|
||||
# - `services.prosody.log`
|
||||
# - this decreases likelihood of breakage during future upgrades
|
||||
# - enable push notifications (mod_cloud_notify)
|
||||
# - optimize coturn (e.g. move off of the VPN!)
|
||||
# - ensure muc is working
|
||||
# - enable file uploads
|
||||
# - "upload.xmpp.uninsane.org:http_upload: URL: <https://upload.xmpp.uninsane.org:5281/upload> - Ensure this can be reached by users"
|
||||
# - disable or fix bosh (jabber over http):
|
||||
# - "certmanager: No certificate/key found for client_https port 0"
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
# enables very verbose logging
|
||||
enableDebug = false;
|
||||
in
|
||||
{
|
||||
sane.persist.sys.byStore.private = [
|
||||
# TODO: mode?
|
||||
{ user = "prosody"; group = "prosody"; path = "/var/lib/prosody"; method = "bind"; }
|
||||
];
|
||||
sane.ports.ports."5000" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-prosody-fileshare-proxy65";
|
||||
};
|
||||
sane.ports.ports."5222" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-client-to-server";
|
||||
};
|
||||
sane.ports.ports."5223" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpps-client-to-server"; # XMPP over TLS
|
||||
};
|
||||
sane.ports.ports."5269" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
description = "colin-xmpp-server-to-server";
|
||||
};
|
||||
sane.ports.ports."5270" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
description = "colin-xmpps-server-to-server"; # XMPP over TLS
|
||||
};
|
||||
sane.ports.ports."5280" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-bosh";
|
||||
};
|
||||
sane.ports.ports."5281" = {
|
||||
protocol = [ "tcp" ];
|
||||
visibleTo.doof = true;
|
||||
visibleTo.lan = true;
|
||||
description = "colin-xmpp-prosody-https"; # necessary?
|
||||
};
|
||||
|
||||
users.users.prosody.extraGroups = [
|
||||
"nginx" # provide access to certs
|
||||
"ntfy-sh" # access to secret ntfy topic
|
||||
"turnserver" # to access the coturn shared secret
|
||||
];
|
||||
|
||||
security.acme.certs."uninsane.org".extraDomainNames = [
|
||||
"xmpp.uninsane.org"
|
||||
"conference.xmpp.uninsane.org"
|
||||
"upload.xmpp.uninsane.org"
|
||||
];
|
||||
|
||||
# exists so the XMPP server's cert can obtain altNames for all its resources
|
||||
services.nginx.virtualHosts."xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
services.nginx.virtualHosts."conference.xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
services.nginx.virtualHosts."upload.xmpp.uninsane.org" = {
|
||||
useACMEHost = "uninsane.org";
|
||||
};
|
||||
|
||||
sane.dns.zones."uninsane.org".inet = {
|
||||
# XXX: SRV records have to point to something with a A/AAAA record; no CNAMEs
|
||||
A."xmpp" = "%ANATIVE%";
|
||||
CNAME."conference.xmpp" = "xmpp";
|
||||
CNAME."upload.xmpp" = "xmpp";
|
||||
|
||||
# _Service._Proto.Name TTL Class SRV Priority Weight Port Target
|
||||
# - <https://xmpp.org/extensions/xep-0368.html>
|
||||
# something's requesting the SRV records for conference.xmpp, so let's include it
|
||||
# nothing seems to request XMPP SRVs for the other records (except @)
|
||||
# lower numerical priority field tells clients to prefer this method
|
||||
SRV."_xmpps-client._tcp.conference.xmpp" = "3 50 5223 xmpp";
|
||||
SRV."_xmpps-server._tcp.conference.xmpp" = "3 50 5270 xmpp";
|
||||
SRV."_xmpp-client._tcp.conference.xmpp" = "5 50 5222 xmpp";
|
||||
SRV."_xmpp-server._tcp.conference.xmpp" = "5 50 5269 xmpp";
|
||||
|
||||
SRV."_xmpps-client._tcp" = "3 50 5223 xmpp";
|
||||
SRV."_xmpps-server._tcp" = "3 50 5270 xmpp";
|
||||
SRV."_xmpp-client._tcp" = "5 50 5222 xmpp";
|
||||
SRV."_xmpp-server._tcp" = "5 50 5269 xmpp";
|
||||
};
|
||||
|
||||
# help Prosody find its certificates.
|
||||
# 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`
|
||||
# <https://prosody.im/doc/certificates#automatic_location>
|
||||
environment.etc."prosody/certs/uninsane.org/fullchain.pem".source = "/var/lib/acme/uninsane.org/fullchain.pem";
|
||||
environment.etc."prosody/certs/uninsane.org/privkey.pem".source = "/var/lib/acme/uninsane.org/key.pem";
|
||||
|
||||
services.prosody = {
|
||||
enable = true;
|
||||
package = pkgs.prosody.override {
|
||||
# XXX(2023/10/15): build without lua-unbound support.
|
||||
# this forces Prosody to fall back to the default Lua DNS resolver, which seems more reliable.
|
||||
# fixes errors like "unbound.queryXYZUV: Resolver error: out of memory"
|
||||
# related: <https://issues.prosody.im/1737#comment-11>
|
||||
lua.withPackages = selector: pkgs.lua.withPackages (p:
|
||||
selector (p // { luaunbound = null; })
|
||||
);
|
||||
# withCommunityModules = [ "turncredentials" ];
|
||||
};
|
||||
admins = [ "colin@uninsane.org" ];
|
||||
# allowRegistration = false; # defaults to false
|
||||
|
||||
muc = [
|
||||
{
|
||||
domain = "conference.xmpp.uninsane.org";
|
||||
}
|
||||
];
|
||||
httpFileShare.domain = "upload.xmpp.uninsane.org";
|
||||
|
||||
virtualHosts = {
|
||||
# "Prosody requires at least one enabled VirtualHost to function. You can
|
||||
# safely remove or disable 'localhost' once you have added another."
|
||||
# localhost = {
|
||||
# domain = "localhost";
|
||||
# enabled = true;
|
||||
# };
|
||||
"xmpp.uninsane.org" = {
|
||||
domain = "uninsane.org";
|
||||
enabled = true;
|
||||
};
|
||||
};
|
||||
|
||||
## modules:
|
||||
# these are enabled by default, via <repo:nixos/nixpkgs:/pkgs/servers/xmpp/prosody/default.nix>
|
||||
# - cloud_notify
|
||||
# - http_upload
|
||||
# - vcard_muc
|
||||
# these are enabled by the module defaults (services.prosody.modules.<foo>)
|
||||
# - admin_adhoc
|
||||
# - blocklist
|
||||
# - bookmarks
|
||||
# - carbons
|
||||
# - cloud_notify
|
||||
# - csi
|
||||
# - dialback
|
||||
# - disco
|
||||
# - http_files
|
||||
# - mam
|
||||
# - pep
|
||||
# - ping
|
||||
# - private
|
||||
# - XEP-0049: let clients store arbitrary (private) data on the server
|
||||
# - proxy65
|
||||
# - XEP-0065: allow server to proxy file transfers between two clients who are behind NAT
|
||||
# - register
|
||||
# - roster
|
||||
# - saslauth
|
||||
# - smacks
|
||||
# - time
|
||||
# - tls
|
||||
# - uptime
|
||||
# - vcard_legacy
|
||||
# - version
|
||||
|
||||
extraPluginPaths = [ ./modules ];
|
||||
|
||||
extraModules = [
|
||||
# admin_shell: allows `prosodyctl shell` to work
|
||||
# see: <https://prosody.im/doc/modules/mod_admin_shell>
|
||||
# see: <https://prosody.im/doc/console>
|
||||
"admin_shell"
|
||||
"admin_telnet" #< needed by admin_shell
|
||||
# lastactivity: XEP-0012: allow users to query how long another user has been idle for
|
||||
# - not sure why i enabled this; think it was in someone's config i referenced
|
||||
"lastactivity"
|
||||
# allows prosody to share TURN/STUN secrets with XMPP clients to provide them access to the coturn server.
|
||||
# see: <https://prosody.im/doc/coturn>
|
||||
"turn_external"
|
||||
# legacy coturn integration
|
||||
# see: <https://modules.prosody.im/mod_turncredentials.html>
|
||||
# "turncredentials"
|
||||
] ++ lib.optionals config.services.ntfy-sh.enable [
|
||||
"sane_ntfy"
|
||||
] ++ lib.optionals enableDebug [
|
||||
"stanza_debug" #< logs EVERY stanza as debug: <https://prosody.im/doc/modules/mod_stanza_debug>
|
||||
];
|
||||
|
||||
extraConfig = ''
|
||||
local function readAll(file)
|
||||
local f = Lua.assert(Lua.io.open(file, "rb"))
|
||||
local content = f:read("*all")
|
||||
f:close()
|
||||
-- remove trailing newline
|
||||
return Lua.string.gsub(content, "%s+", "")
|
||||
end
|
||||
|
||||
-- logging docs:
|
||||
-- - <https://prosody.im/doc/logging>
|
||||
-- - <https://prosody.im/doc/advanced_logging>
|
||||
-- levels: debug, info, warn, error
|
||||
log = {
|
||||
${if enableDebug then "debug" else "info"} = "*syslog";
|
||||
}
|
||||
|
||||
-- see: <https://prosody.im/doc/certificates#automatic_location>
|
||||
-- try to solve: "certmanager: Error indexing certificate directory /run/prosody/certs: cannot open /run/prosody/certs: No such file or directory"
|
||||
-- only, this doesn't work because prosody doesn't like acme's naming scheme
|
||||
-- certificates = "/var/lib/acme/uninsane.org"
|
||||
-- instead, point to /etc/prosody/certs and configure symlinks into this dir (see nix config)
|
||||
certificates = "/etc/prosody/certs"
|
||||
|
||||
c2s_direct_tls_ports = { 5223 }
|
||||
s2s_direct_tls_ports = { 5270 }
|
||||
|
||||
turn_external_host = "turn.uninsane.org"
|
||||
turn_external_secret = readAll("/run/secrets/coturn_shared_secret")
|
||||
-- turn_external_user = "prosody"
|
||||
|
||||
-- legacy mod_turncredentials integration
|
||||
-- turncredentials_host = "turn.uninsane.org"
|
||||
-- turncredentials_secret = readAll("/run/secrets/coturn_shared_secret")
|
||||
|
||||
-- s2s_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" ];
|
||||
};
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
-- simple proof-of-concept Prosody module
|
||||
-- module development guide: <https://prosody.im/doc/developers/modules>
|
||||
-- module API docs: <https://prosody.im/doc/developers/moduleapi>
|
||||
--
|
||||
-- much of this code is lifted from Prosody's own `mod_cloud_notify`
|
||||
|
||||
local jid = require"util.jid";
|
||||
|
||||
local ntfy = module:get_option_string("ntfy_binary", "ntfy");
|
||||
local ntfy_topic = module:get_option_string("ntfy_topic", "xmpp");
|
||||
|
||||
module:log("info", "initialized");
|
||||
|
||||
local function is_urgent(stanza)
|
||||
if stanza.name == "message" then
|
||||
if stanza:get_child("propose", "urn:xmpp:jingle-message:0") then
|
||||
return true, "jingle call";
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function publish_ntfy(message)
|
||||
-- message should be the message to publish
|
||||
local ntfy_url = string.format("https://ntfy.uninsane.org/%s", ntfy_topic)
|
||||
local cmd = string.format("%s pub %q %q", ntfy, ntfy_url, message)
|
||||
module.log("debug", "invoking ntfy: %s", cmd)
|
||||
local success, reason, code = os.execute(cmd)
|
||||
if not success then
|
||||
module:log("warn", "ntfy failed: %s => %s %d", cmd, reason, code)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function archive_message_added(event)
|
||||
-- event is: { origin = origin, stanza = stanza, for_user = store_user, id = id }
|
||||
local stanza = event.stanza;
|
||||
local to = stanza.attr.to;
|
||||
to = to and jid.split(to) or event.origin.username;
|
||||
|
||||
-- only notify if the stanza destination is the mam user we store it for
|
||||
if event.for_user == to then
|
||||
local is_urgent_stanza, urgent_reason = is_urgent(event.stanza);
|
||||
|
||||
if is_urgent_stanza then
|
||||
module:log("info", "urgent push for %s (%s)", to, urgent_reason);
|
||||
publish_ntfy(urgent_reason)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
module:hook("archive-message-added", archive_message_added);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user