Compare commits
4 Commits
master
...
wip-proot3
Author | SHA1 | Date | |
---|---|---|---|
52d8321066 | |||
98463ddeb4 | |||
5dcf2a533b | |||
de37188417 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
||||||
.working
|
|
||||||
result
|
result
|
||||||
result-*
|
|
||||||
/secrets/local.nix
|
/secrets/local.nix
|
||||||
|
|
|
@ -3,7 +3,6 @@ keys:
|
||||||
- &user_lappy_colin age1j2pqnl8j0krdzk6npe93s4nnqrzwx978qrc0u570gzlamqpnje9sc8le2g
|
- &user_lappy_colin age1j2pqnl8j0krdzk6npe93s4nnqrzwx978qrc0u570gzlamqpnje9sc8le2g
|
||||||
- &user_servo_colin age1z8fauff34cdecr6sjkre260luzxcca05kpcwvhx988d306tpcejsp63znu
|
- &user_servo_colin age1z8fauff34cdecr6sjkre260luzxcca05kpcwvhx988d306tpcejsp63znu
|
||||||
- &user_moby_colin age1zsrsvd7j6l62fjxpfd2qnhqlk8wk4p8r0dtxpe4sdgnh2474095qdu7xj9
|
- &user_moby_colin age1zsrsvd7j6l62fjxpfd2qnhqlk8wk4p8r0dtxpe4sdgnh2474095qdu7xj9
|
||||||
- &host_crappy age1hl50ufuxnqy0jnk8fqeu4tclh4vte2xn2d59pxff0gun20vsmv5sp78chj
|
|
||||||
- &host_desko age1vnw7lnfpdpjn62l3u5nyv5xt2c965k96p98kc43mcnyzpetrts9q54mc9v
|
- &host_desko age1vnw7lnfpdpjn62l3u5nyv5xt2c965k96p98kc43mcnyzpetrts9q54mc9v
|
||||||
- &host_lappy age1w7mectcjku6x3sd8plm8wkn2qfrhv9n6zhzlf329e2r2uycgke8qkf9dyn
|
- &host_lappy age1w7mectcjku6x3sd8plm8wkn2qfrhv9n6zhzlf329e2r2uycgke8qkf9dyn
|
||||||
- &host_servo age1tzlyex2z6t88tg9h82943e39shxhmqeyr7ywhlwpdjmyqsndv3qq27x0rf
|
- &host_servo age1tzlyex2z6t88tg9h82943e39shxhmqeyr7ywhlwpdjmyqsndv3qq27x0rf
|
||||||
|
@ -16,7 +15,6 @@ creation_rules:
|
||||||
- *user_lappy_colin
|
- *user_lappy_colin
|
||||||
- *user_servo_colin
|
- *user_servo_colin
|
||||||
- *user_moby_colin
|
- *user_moby_colin
|
||||||
- *host_crappy
|
|
||||||
- *host_desko
|
- *host_desko
|
||||||
- *host_lappy
|
- *host_lappy
|
||||||
- *host_servo
|
- *host_servo
|
||||||
|
|
139
README.md
139
README.md
|
@ -1,68 +1,53 @@
|
||||||
![hello](doc/hello.gif)
|
|
||||||
|
|
||||||
# .❄️≡We|_c0m3 7o m`/ f14k≡❄️.
|
|
||||||
|
|
||||||
(er, it's not a flake anymore. welcome to my nix files.)
|
|
||||||
|
|
||||||
## What's Here
|
## What's Here
|
||||||
|
|
||||||
this is the top-level repo from which i configure/deploy all my NixOS machines:
|
this is the top-level repo from which i configure/deploy all my NixOS machines:
|
||||||
- desktop
|
- desktop
|
||||||
- laptop
|
- laptop
|
||||||
- server
|
- server
|
||||||
- mobile phone (Pinephone)
|
- mobile phone
|
||||||
|
|
||||||
everything outside of [hosts/](./hosts/) and [secrets/](./secrets/) is intended for export, to be importable for use by 3rd parties.
|
i enjoy a monorepo approach. this repo references [nixpkgs][nixpkgs], a couple 3rd party
|
||||||
the only hard dependency for my exported pkgs/modules should be [nixpkgs][nixpkgs].
|
nix modules like [sops][sops], the sources for [uninsane.org][uninsane-org], and that's
|
||||||
building [hosts/](./hosts/) will require [sops][sops].
|
about it. custom derivations and modules (some of which i try to upstream) live
|
||||||
|
directly here; even the sources for those packages is often kept here too.
|
||||||
you might specifically be interested in these files (elaborated further in #key-points-of-interest):
|
|
||||||
- ~~[`sxmo-utils`](./pkgs/additional/sxmo-utils/default.nix)~~
|
|
||||||
- these files will remain until my config settles down, but i no longer use or maintain SXMO.
|
|
||||||
- [my implementation of impermanence](./modules/persist/default.nix)
|
|
||||||
- my 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)
|
|
||||||
|
|
||||||
[nixpkgs]: https://github.com/NixOS/nixpkgs
|
[nixpkgs]: https://github.com/NixOS/nixpkgs
|
||||||
[sops]: https://github.com/Mic92/sops-nix
|
[sops]: https://github.com/Mic92/sops-nix
|
||||||
[uninsane-org]: https://uninsane.org
|
[uninsane-org]: https://uninsane.org
|
||||||
|
|
||||||
## Using This Repo In Your Own Config
|
|
||||||
|
|
||||||
follow the instructions [here][NUR] to access my packages through the Nix User Repositories.
|
|
||||||
|
|
||||||
[NUR]: https://nur.nix-community.org/
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
- `doc/`
|
- `doc/`
|
||||||
- instructions for tasks i find myself doing semi-occasionally in this repo.
|
- instructions for tasks i find myself doing semi-occasionally in this repo.
|
||||||
- `hosts/`
|
- `hosts/`
|
||||||
- configs which aren't factored with external use in mind.
|
- the bulk of config which isn't factored with external use in mind.
|
||||||
- that is, if you were to add this repo to a flake.nix for your own use,
|
- that is, if you were to add this repo to a flake.nix for your own use,
|
||||||
you won't likely be depending on anything in this directory.
|
you won't likely be depending on anything in this directory.
|
||||||
- `integrations/`
|
- `integrations/`
|
||||||
- code intended for consumption by external tools (e.g. the Nix User Repos).
|
- code intended for consumption by external tools (e.g. the Nix User Repos)
|
||||||
- `modules/`
|
- `modules/`
|
||||||
- config which is gated behind `enable` flags, in similar style to nixpkgs' `nixos/` directory.
|
- config which is gated behind `enable` flags, in similar style to nixpkgs'
|
||||||
- if you depend on this repo for anything besides packages, it's most likely for something in this directory.
|
`nixos/` directory.
|
||||||
|
- if you depend on this repo, it's most likely for something in this directory.
|
||||||
|
- `nixpatches/`
|
||||||
|
- literally, diffs i apply atop upstream nixpkgs before performing further eval.
|
||||||
- `overlays/`
|
- `overlays/`
|
||||||
- predominantly a list of `callPackage` directives.
|
- exposed via the `overlays` output in `flake.nix`.
|
||||||
|
- predominantly a list of `callPackage` directives.
|
||||||
- `pkgs/`
|
- `pkgs/`
|
||||||
- derivations for things not yet packaged in nixpkgs.
|
- derivations for things not yet packaged in nixpkgs.
|
||||||
- derivations for things from nixpkgs which i need to `override` for some reason.
|
- derivations for things from nixpkgs which i need to `override` for some reason.
|
||||||
- inline code for wholly custom packages (e.g. `pkgs/additional/sane-scripts/` for CLI tools
|
- inline code for wholly custom packages (e.g. `pkgs/additional/sane-scripts/` for CLI tools
|
||||||
that are highly specific to my setup).
|
that are highly specific to my setup).
|
||||||
- `scripts/`
|
- `scripts/`
|
||||||
- scripts which aren't reachable on a deployed system, but may aid manual deployments.
|
- scripts which aren't reachable on a deployed system, but may aid manual deployments
|
||||||
- `secrets/`
|
- `secrets/`
|
||||||
- encrypted keys, API tokens, anything which one or more of my machines needs
|
- encrypted keys, API tokens, anything which one or more of my machines needs
|
||||||
read access to but shouldn't be world-readable.
|
read access to but shouldn't be world-readable.
|
||||||
- not much to see here.
|
- not much to see here
|
||||||
- `templates/`
|
- `templates/`
|
||||||
- used to instantiate short-lived environments.
|
- exposed via the `templates` output in `flake.nix`.
|
||||||
- used to auto-fill the boiler-plate portions of new packages.
|
- used to instantiate short-lived environments.
|
||||||
|
- used to auto-fill the boiler-plate portions of new packages.
|
||||||
|
|
||||||
|
|
||||||
## Key Points of Interest
|
## Key Points of Interest
|
||||||
|
@ -70,47 +55,47 @@ follow the instructions [here][NUR] to access my packages through the Nix User R
|
||||||
i.e. you might find value in using these in your own config:
|
i.e. you might find value in using these in your own config:
|
||||||
|
|
||||||
- `modules/fs/`
|
- `modules/fs/`
|
||||||
- use this to statically define leafs and nodes anywhere in the filesystem,
|
- use this to statically define leafs and nodes anywhere in the filesystem,
|
||||||
not just inside `/nix/store`.
|
not just inside `/nix/store`.
|
||||||
- e.g. specify that `/var/www` should be:
|
- e.g. specify that `/var/www` should be:
|
||||||
- owned by a specific user/group
|
- owned by a specific user/group
|
||||||
- set to a specific mode
|
- set to a specific mode
|
||||||
- symlinked to some other path
|
- symlinked to some other path
|
||||||
- populated with some statically-defined data
|
- populated with some statically-defined data
|
||||||
- populated according to some script
|
- populated according to some script
|
||||||
- created as a dependency of some service (e.g. `nginx`)
|
- created as a dependency of some service (e.g. `nginx`)
|
||||||
- values defined here are applied neither at evaluation time _nor_ at activation time.
|
- values defined here are applied neither at evaluation time _nor_ at activation time.
|
||||||
- rather, they become systemd services.
|
- rather, they become systemd services.
|
||||||
- systemd manages dependencies
|
- systemd manages dependencies
|
||||||
- e.g. link `/var/www -> /mnt/my-drive/www` only _after_ `/mnt/my-drive/www` appears)
|
- e.g. link `/var/www -> /mnt/my-drive/www` only _after_ `/mnt/my-drive/www` appears)
|
||||||
- this is akin to using [Home Manager's][home-manager] file API -- the part which lets you
|
- this is akin to using [Home Manager's][home-manager] file API -- the part which lets you
|
||||||
statically define `~/.config` files -- just with a different philosophy.
|
statically define `~/.config` files -- just with a different philosophy.
|
||||||
- `modules/persist/`
|
- `modules/persist/`
|
||||||
- my alternative to the Impermanence module.
|
- my alternative to the Impermanence module.
|
||||||
- this builds atop `modules/fs/` to achieve things stock impermanence can't:
|
- this builds atop `modules/fs/` to achieve things stock impermanence can't:
|
||||||
- persist things to encrypted storage which is unlocked at login time (pam_mount).
|
- persist things to encrypted storage which is unlocked at login time (pam_mount).
|
||||||
- "persist" cache directories -- to free up RAM -- but auto-wipe them on mount
|
- "persist" cache directories -- to free up RAM -- but auto-wipe them on mount
|
||||||
and encrypt them to ephemeral keys so they're unreadable post shutdown/unmount.
|
and encrypt them to ephemeral keys so they're unreadable post shutdown/unmount.
|
||||||
- `modules/programs/`
|
- `modules/programs.nix`
|
||||||
- like nixpkgs' `programs` options, but allows both system-wide or per-user deployment.
|
- like nixpkgs' `programs` options, but allows both system-wide or per-user deployment.
|
||||||
- allows `fs` and `persist` config values to be gated behind program deployment:
|
- allows `fs` and `persist` config values to be gated behind program deployment:
|
||||||
- e.g. `/home/<user>/.mozilla/firefox` is persisted only for users who
|
- e.g. `/home/<user>/.mozilla/firefox` is persisted only for users who
|
||||||
`sane.programs.firefox.enableFor.user."<user>" = true;`
|
`sane.programs.firefox.enableFor.user."<user>" = true;`
|
||||||
- allows aggressive sandboxing any program:
|
- `modules/users.nix`
|
||||||
- `sane.programs.firefox.sandbox.method = "bwrap"; # sandbox with bubblewrap`
|
- convenience layer atop the above modules so that you can just write
|
||||||
- `sane.programs.firefox.sandbox.whitelistWayland = true; # allow it to render a wayland window`
|
`fs.".config/git"` instead of `fs."/home/colin/.config/git"`
|
||||||
- `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"`
|
|
||||||
- per-user services managed by [s6-rc](https://www.skarnet.org/software/s6-rc/)
|
|
||||||
|
|
||||||
some things in here could easily find broader use. if you would find benefit in
|
some things in here could easily find broader use. if you would find benefit in
|
||||||
them being factored out of my config, message me and we could work to make that happen.
|
them being factored out of my config, message me and we could work to make that happen.
|
||||||
|
|
||||||
[home-manager]: https://github.com/nix-community/home-manager
|
[home-manager]: https://github.com/nix-community/home-manager
|
||||||
|
|
||||||
|
## Using This Repo In Your Own Config
|
||||||
|
|
||||||
|
this should be a pretty "standard" flake. just reference it, and import either
|
||||||
|
- `nixosModules.sane` (for the modules)
|
||||||
|
- `overlays.pkgs` (for the packages)
|
||||||
|
|
||||||
## Mirrors
|
## Mirrors
|
||||||
|
|
||||||
this repo exists in a few known locations:
|
this repo exists in a few known locations:
|
||||||
|
|
178
TODO.md
178
TODO.md
|
@ -1,177 +1,81 @@
|
||||||
## BUGS
|
## BUGS
|
||||||
- `rmDbusServices` may break sandboxing
|
- why i need to manually restart `wireguard-wg-ovpns` on servo periodically
|
||||||
- e.g. if the package ships a systemd unit which references $out, then make-sandboxed won't properly update that unit.
|
- else DNS fails
|
||||||
- `rmDbusServicesInPlace` is not affected
|
- fix epiphany URL bar input on moby
|
||||||
- when moby wlan is explicitly set down (via ip link set wlan0 down), /var/lib/trust-dns/dhcp-configs doesn't get reset
|
|
||||||
- `ip monitor` can detect those manual link state changes (NM-dispatcher it seems cannot)
|
|
||||||
- or try dnsmasq?
|
|
||||||
- trust-dns: can't recursively resolve api.mangadex.org
|
|
||||||
- and *sometimes* apple.com fails
|
|
||||||
- sandbox: link cache means that if i update ~/.config/... files inline, sandboxed programs still see the old version
|
|
||||||
- mpv: audiocast has mpv sending its output to the builtin speakers unless manually changed
|
|
||||||
- mpv: no way to exit fullscreen video on moby
|
|
||||||
- uosc hides controls on FS, and touch doesn't support unhiding
|
|
||||||
- Signal restart loop drains battery
|
|
||||||
- decrease s6 restart time?
|
|
||||||
- `ssh` access doesn't grant same linux capabilities as login
|
|
||||||
- ringer (i.e. dino incoming call) doesn't prevent moby from sleeping
|
|
||||||
- syshud (volume overlay): when casting with `blast`, syshud doesn't react to volume changes
|
|
||||||
- moby: kaslr is effectively disabled
|
|
||||||
- `dmesg | grep "KASLR disabled due to lack of seed"`
|
|
||||||
- fix by adding `kaslrseed` to uboot script before `booti`
|
|
||||||
- <https://github.com/armbian/build/pull/4352>
|
|
||||||
- not sure how that's supposed to work with tow-boot; maybe i should just update tow-boot
|
|
||||||
- moby: bpf is effectively disabled?
|
|
||||||
- `dmesg | grep 'systemd[1]: bpf-lsm: Failed to load BPF object: No such process'`
|
|
||||||
- `dmesg | grep 'hid_bpf: error while preloading HID BPF dispatcher: -22'`
|
|
||||||
- `s6` is not re-entrant
|
|
||||||
- so if the desktop crashes, the login process from `unl0kr` fails to re-launch the GUI
|
|
||||||
- swaync brightness slider does not work
|
|
||||||
- it reads brightness from /sys/class/backlight/....
|
|
||||||
- but to *set* the brightness it assumes systemd logind is running
|
|
||||||
<repo:ErikReider/SwayNotificationCenter:src/controlCenter/widgets/backlight/backlightUtil.vala>
|
|
||||||
no reason i can't just write to that file, or exec brightnessctl (if i learn vala)
|
|
||||||
|
|
||||||
## REFACTORING:
|
## REFACTORING:
|
||||||
- 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
|
|
||||||
|
|
||||||
### sops/secrets
|
### sops/secrets
|
||||||
|
- attach secrets to the thing they're used by (sane.programs)
|
||||||
- rework secrets to leverage `sane.fs`
|
- rework secrets to leverage `sane.fs`
|
||||||
- remove sops activation script as it's covered by my systemd sane.fs impl
|
- remove sops activation script as it's covered by my systemd sane.fs impl
|
||||||
- user secrets could just use `gocryptfs`, like with ~/private?
|
|
||||||
- can gocryptfs support nested filesystems, each with different perms (for desko, moby, etc)?
|
|
||||||
|
|
||||||
### roles
|
### roles
|
||||||
- allow any host to take the role of `uninsane.org`
|
- allow any host to take the role of `uninsane.org`
|
||||||
- will make it easier to test new services?
|
- will make it easier to test new services?
|
||||||
|
|
||||||
### upstreaming
|
### upstreaming
|
||||||
|
- split out a sxmo module usable by NUR consumers
|
||||||
|
- bump nodejs version in lemmy-ui
|
||||||
- add updateScripts to all my packages in nixpkgs
|
- add updateScripts to all my packages in nixpkgs
|
||||||
|
- fix lightdm-mobile-greeter for newer libhandy
|
||||||
#### upstreaming to non-nixpkgs repos
|
- port zecwallet-lite to a from-source build
|
||||||
- gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844>
|
- REVIEW/integrate jellyfin dataDir config: <https://github.com/NixOS/nixpkgs/pull/233617>
|
||||||
|
- remove `libsForQt5.callPackage` broadly: <https://github.com/NixOS/nixpkgs/issues/180841>
|
||||||
|
|
||||||
|
|
||||||
## IMPROVEMENTS:
|
## IMPROVEMENTS:
|
||||||
- systemd/journalctl: use a less shit pager
|
|
||||||
- there's an env var for it: SYSTEMD_PAGER? and a flag for journalctl
|
|
||||||
- kernels: ship the same kernel on every machine
|
|
||||||
- then i can tune the kernels for hardening, without duplicating that work 4 times
|
|
||||||
- zfs: replace this with something which doesn't require a custom kernel build
|
|
||||||
- mpv: add media looping controls (e.g. loop song, loop playlist)
|
|
||||||
|
|
||||||
### security/resilience
|
### security/resilience
|
||||||
- validate duplicity backups!
|
- validate duplicity backups!
|
||||||
- encrypt more ~ dirs (~/archives, ~/records, ..?)
|
- encrypt more ~ dirs (~/archives, ~/records, ..?)
|
||||||
- best to do this after i know for sure i have good backups
|
- best to do this after i know for sure i have good backups
|
||||||
- /mnt/desko/home, etc, shouldn't include secrets (~/private)
|
- have `sane.programs` be wrapped such that they run in a cgroup?
|
||||||
- 95% of its use is for remote media access and stuff which isn't in VCS (~/records)
|
- at least, only give them access to the portion of the fs they *need*.
|
||||||
- port all sane.programs to be sandboxed
|
- Android takes approach of giving each app its own user: could hack that in here.
|
||||||
- enforce that all `environment.packages` has a sandbox profile (or explicitly opts out)
|
- **systemd-run** takes a command and runs it in a temporary scope (cgroup)
|
||||||
- revisit "non-sandboxable" apps and check that i'm not actually just missing mountpoints
|
- presumably uses the same options as systemd services
|
||||||
- LL_FS_RW=/ isn't enough -- need all mount points like `=/:/proc:/sys:...`.
|
- see e.g. <https://github.com/NixOS/nixpkgs/issues/113903#issuecomment-857296349>
|
||||||
- ensure non-bin package outputs are linked for sandboxed apps
|
- flatpak does this, somehow
|
||||||
- i.e. `outputs.man`, `outputs.debug`, `outputs.doc`, ...
|
- apparmor? SElinux? (desktop) "portals"?
|
||||||
- lock down dbus calls within the sandbox
|
- see Spectrum OS; Alyssa Ross; etc
|
||||||
- otherwise anyone can `systemd-run --user ...` to potentially escape a sandbox
|
- bubblewrap-based sandboxing: <https://github.com/nixpak/nixpak>
|
||||||
- <https://github.com/flatpak/xdg-dbus-proxy>
|
|
||||||
- remove `.ssh` access from Firefox!
|
|
||||||
- limit access to `~/knowledge/secrets` through an agent that requires GUI approval, so a firefox exploit can't steal all my logins
|
|
||||||
- port sanebox to a compiled language (hare?)
|
|
||||||
- it adds like 50-70ms launch time _on my laptop_. i'd hate to know how much that is on the pinephone.
|
|
||||||
- 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
|
|
||||||
- canaries for important services
|
- canaries for important services
|
||||||
- e.g. daily email checks; daily backup checks
|
- e.g. daily email checks; daily backup checks
|
||||||
- integrate `nix check` into Gitea actions?
|
- integrate `nix check` into Gitea actions?
|
||||||
|
|
||||||
### user experience
|
### user experience
|
||||||
- rofi: sort items case-insensitively
|
- neovim: set up language server (lsp; rnix-lsp; nvim-lspconfig)
|
||||||
- xdg-desktop-portal shouldn't kill children on exit
|
- Helix: make copy-to-system clipboard be the default
|
||||||
- *maybe* a job for `setsid -f`?
|
- firefox/librewolf: persist history
|
||||||
- replace starship prompt with something more efficient
|
- just not cookies or tabs
|
||||||
- watch `forkstat`: it does way too much
|
|
||||||
- cleanup waybar/nwg-panel so that it's not invoking playerctl every 2 seconds
|
|
||||||
- nwg-panel: swaync icon is stuck as the refresh icon
|
|
||||||
- nwg-panel: doesn't appear on all desktops
|
|
||||||
- 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`)
|
|
||||||
- offline docs viewer (gtk): <https://github.com/workbenchdev/Biblioteca>
|
|
||||||
- some type of games manager/launcher
|
|
||||||
- Gnome Highscore (retro games)?: <https://gitlab.gnome.org/World/highscore>
|
|
||||||
- better maps for mobile (Osmin (QtQuick)? Pure Maps (Qt/Kirigami)?
|
|
||||||
- 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
|
|
||||||
- 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
|
|
||||||
|
|
||||||
#### moby
|
|
||||||
- fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html>
|
|
||||||
- moby: tune keyboard layout
|
|
||||||
- SwayNC:
|
|
||||||
- don't show MPRIS if no players detected
|
|
||||||
- this is a problem of playerctld, i guess
|
|
||||||
- add option to change audio output
|
|
||||||
- fix colors (red alert) to match overall theme
|
|
||||||
- moby: tune GPS
|
|
||||||
- tune QGPS setting in eg25-control, for less jitter?
|
|
||||||
- configure geoclue to do some smoothing?
|
|
||||||
- manually do smoothing, as some layer between mepo and geoclue?
|
|
||||||
- moby: port `freshen-agps` timer service to s6 (maybe i want some `s6-cron` or something)
|
|
||||||
- moby: show battery state on ssh login
|
|
||||||
- moby: improve gPodder launch time
|
- moby: improve gPodder launch time
|
||||||
- moby: theme GTK apps (i.e. non-adwaita styles)
|
- moby: theme GTK apps (i.e. non-adwaita styles)
|
||||||
- especially, make the menubar collapsible
|
- especially, make the menubar collapsible
|
||||||
- try Gradience tool specifically for theming adwaita? <https://linuxphoneapps.org/apps/com.github.gradienceteam.gradience/>
|
- try Gradience tool specifically for theming adwaita? <https://linuxphoneapps.org/apps/com.github.gradienceteam.gradience/>
|
||||||
|
|
||||||
#### non-moby
|
|
||||||
- RSS: integrate a paywall bypass
|
|
||||||
- e.g. self-hosted [ladder](https://github.com/everywall/ladder) (like 12ft.io)
|
|
||||||
- neovim: set up language server (lsp; rnix-lsp; nvim-lspconfig)
|
|
||||||
- neovim: integrate LLMs
|
|
||||||
- Helix: make copy-to-system clipboard be the default
|
|
||||||
- firefox/librewolf: persist history
|
|
||||||
- just not cookies or tabs
|
|
||||||
- package Nix/NixOS docs for Zeal
|
- package Nix/NixOS docs for Zeal
|
||||||
- install [doc-browser](https://github.com/qwfy/doc-browser)
|
- install [doc-browser](https://github.com/qwfy/doc-browser)
|
||||||
- this supports both dash (zeal) *and* the datasets from <https://devdocs.io> (which includes nix!)
|
- this supports both dash (zeal) *and* the datasets from <https://devdocs.io> (which includes nix!)
|
||||||
- install [devhelp](https://wiki.gnome.org/Apps/Devhelp) (gnome)
|
- install [devhelp](https://wiki.gnome.org/Apps/Devhelp) (gnome)
|
||||||
- have xdg-open parse `<repo:...> URIs (or adjust them so that it _can_ parse)
|
- have xdg-open parse `<repo:...> URIs (or adjust them so that it _can_ parse)
|
||||||
- sane-bt-search: show details like 5.1 vs stereo, h264 vs h265
|
- sane-bt-search: show details like 5.1 vs stereo, h264 vs h265
|
||||||
- maybe just color these "keywords" in all search results?
|
|
||||||
- uninsane.org: make URLs relative to allow local use (and as offline homepage)
|
- uninsane.org: make URLs relative to allow local use (and as offline homepage)
|
||||||
- email: fix so that local mail doesn't go to junk
|
- email: fix so that local mail doesn't go to junk
|
||||||
- git sendmail flow adds the DKIM signatures, but gets delivered locally w/o having the sig checked, so goes into Junk
|
- git sendmail flow adds the DKIM signatures, but gets delivered locally w/o having the sig checked, so goes into Junk
|
||||||
- could change junk filter from "no DKIM success" to explicit "DKIM failed"
|
- could change junk filter from "no DKIM success" to explicit "DKIM failed"
|
||||||
|
|
||||||
### perf
|
### perf
|
||||||
- debug nixos-rebuild times
|
|
||||||
- use `systemctl list-jobs` to show what's being waited on
|
|
||||||
- i think it's `systemd-networkd-wait-online.service` that's blocking this?
|
|
||||||
- i wonder what interface it's waiting for. i should use `--ignore=...` to ignore interfaces i don't care about.
|
|
||||||
- also `wireguard-wg-home.target` when net is offline
|
|
||||||
- add `pkgs.impure-cached.<foo>` package set to build things with ccache enabled
|
- add `pkgs.impure-cached.<foo>` package set to build things with ccache enabled
|
||||||
- every package here can be auto-generated, and marked with some env var so that it doesn't pollute the pure package set
|
- every package here can be auto-generated, and marked with some env var so that it doesn't pollute the pure package set
|
||||||
- would be super handy for package prototyping!
|
- would be super handy for package prototyping!
|
||||||
|
- why does nixos-rebuild switch take 5 minutes when net is flakey?
|
||||||
|
- trying to auto-mount servo?
|
||||||
|
- something to do with systemd services restarting/stalling
|
||||||
|
- maybe wireguard & its refresh operation, specifically?
|
||||||
|
- get moby to build without binfmt emulation (i.e. make all emulation explicit)
|
||||||
|
- then i can distribute builds across servo + desko, and also allow servo to pull packages from desko w/o worrying about purity
|
||||||
|
|
||||||
|
|
||||||
## NEW FEATURES:
|
## NEW FEATURES:
|
||||||
- migrate MAME cabinet to nix
|
- migrate MAME cabinet to nix
|
||||||
- boot it from PXE from servo?
|
- boot it from PXE from servo?
|
||||||
- enable IPv6
|
- enable IPv6
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{ ... }@args:
|
|
||||||
let
|
|
||||||
sane-nix-files = import ./pkgs/additional/sane-nix-files { };
|
|
||||||
in
|
|
||||||
import "${sane-nix-files}/impure.nix" args
|
|
|
@ -1,25 +0,0 @@
|
||||||
to add a host:
|
|
||||||
- create the new nix targets
|
|
||||||
- hosts/by-name/HOST
|
|
||||||
- let the toplevel (flake.nix) know about HOST
|
|
||||||
- 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: `sops updatekeys path/to/secret.yaml`
|
|
BIN
doc/hello.gif
BIN
doc/hello.gif
Binary file not shown.
Before Width: | Height: | Size: 127 KiB |
|
@ -1,12 +0,0 @@
|
||||||
## deploying to SD card
|
|
||||||
- build a toplevel config: `nix build '.#hostSystems.moby'`
|
|
||||||
- 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`
|
|
171
flake.lock
Normal file
171
flake.lock
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687709756,
|
||||||
|
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile-nixos": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1690059310,
|
||||||
|
"narHash": "sha256-4zcoDp8wwZVfGSzXltC5x+eH4kDWC/eJpyQNgr7shAA=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "mobile-nixos",
|
||||||
|
"rev": "56fc9f9619f305f0865354975a98d22410eed127",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "mobile-nixos",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-serve": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687251388,
|
||||||
|
"narHash": "sha256-E9cVlgeCvzPbA/G3mCDCzz8TdRwXyGYzIjmwcvIfghg=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "nix-serve",
|
||||||
|
"rev": "d6df5bd8584f37e22cff627db2fc4058a4aab5ee",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "nix-serve",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1606086654,
|
||||||
|
"narHash": "sha256-VFl+3eGIMqNp7cyOMJ6TjM/+UcsLKtodKoYexrlTJMI=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "19db3e5ea2777daa874563b5986288151f502e27",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-20.09",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1690066826,
|
||||||
|
"narHash": "sha256-6L2qb+Zc0BFkh72OS9uuX637gniOjzU6qCDBpjB2LGY=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "ce45b591975d070044ca24e3003c830d26fea1c8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "release-23.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-unpatched": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1691654369,
|
||||||
|
"narHash": "sha256-gSILTEx1jRaJjwZxRlnu3ZwMn1FVNk80qlwiCX8kmpo=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "ce5e4a6ef2e59d89a971bc434ca8ca222b9c7f5e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"mobile-nixos": "mobile-nixos",
|
||||||
|
"nix-serve": "nix-serve",
|
||||||
|
"nixpkgs-unpatched": "nixpkgs-unpatched",
|
||||||
|
"sops-nix": "sops-nix",
|
||||||
|
"uninsane-dot-org": "uninsane-dot-org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sops-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs-unpatched"
|
||||||
|
],
|
||||||
|
"nixpkgs-stable": "nixpkgs-stable"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1690199016,
|
||||||
|
"narHash": "sha256-yTLL72q6aqGmzHq+C3rDp3rIjno7EJZkFLof6Ika7cE=",
|
||||||
|
"owner": "Mic92",
|
||||||
|
"repo": "sops-nix",
|
||||||
|
"rev": "c36df4fe4bf4bb87759b1891cab21e7a05219500",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "Mic92",
|
||||||
|
"repo": "sops-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uninsane-dot-org": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs-unpatched"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1691106178,
|
||||||
|
"narHash": "sha256-3mZ9gTvMpbZA9ea9ovoQpn2wKuQY0QZ7MDdEjArYdAQ=",
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"rev": "f4d91aa201b6e49af690f250d4786bd1d8b4dcfd",
|
||||||
|
"revCount": 205,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.uninsane.org/colin/uninsane"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.uninsane.org/colin/uninsane"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
348
flake.nix
Normal file
348
flake.nix
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
# FLAKE FEEDBACK:
|
||||||
|
# - if flake inputs are meant to be human-readable, a human should be able to easily track them down given the URL.
|
||||||
|
# - this is not the case with registry URLs, like `nixpkgs/nixos-22.11`.
|
||||||
|
# - this is marginally the case with schemes like `github:nixos/nixpkgs`.
|
||||||
|
# - given the *existing* `git+https://` scheme, i propose expressing github URLs similarly:
|
||||||
|
# - `github+https://github.com/nixos/nixpkgs/tree/nixos-22.11`
|
||||||
|
# - need some way to apply local patches to inputs.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# DEVELOPMENT DOCS:
|
||||||
|
# - Flake docs: <https://nixos.wiki/wiki/Flakes>
|
||||||
|
# - Flake RFC: <https://github.com/tweag/rfcs/blob/flakes/rfcs/0049-flakes.md>
|
||||||
|
# - Discussion: <https://github.com/NixOS/rfcs/pull/49>
|
||||||
|
# - <https://serokell.io/blog/practical-nix-flakes>
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# COMMON OPERATIONS:
|
||||||
|
# - update a specific flake input:
|
||||||
|
# - `nix flake lock --update-input nixpkgs`
|
||||||
|
|
||||||
|
{
|
||||||
|
# XXX: use the `github:` scheme instead of the more readable git+https: because it's *way* more efficient
|
||||||
|
# preferably, i would rewrite the human-readable https URLs to nix-specific github: URLs with a helper,
|
||||||
|
# but `inputs` is required to be a strict attrset: not an expression.
|
||||||
|
inputs = {
|
||||||
|
# branch workflow:
|
||||||
|
# - daily:
|
||||||
|
# - nixos-unstable cut from master after enough packages have been built in caches.
|
||||||
|
# - every 6 hours:
|
||||||
|
# - master auto-merged into staging.
|
||||||
|
# - staging-next auto-merged into staging.
|
||||||
|
# - manually, approximately once per month:
|
||||||
|
# - staging-next is cut from staging.
|
||||||
|
# - staging-next merged into master.
|
||||||
|
#
|
||||||
|
# which branch to source from?
|
||||||
|
# - for everyday development, prefer `nixos-unstable` branch, as it provides good caching.
|
||||||
|
# - if need to test bleeding updates (e.g. if submitting code into staging):
|
||||||
|
# - use `staging-next` if it's been cut (i.e. if there's an active staging-next -> master PR)
|
||||||
|
# - use `staging` if no staging-next branch has been cut.
|
||||||
|
#
|
||||||
|
# <https://github.com/nixos/nixpkgs/tree/nixos-unstable>
|
||||||
|
nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
|
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=staging-next";
|
||||||
|
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=staging";
|
||||||
|
|
||||||
|
mobile-nixos = {
|
||||||
|
# <https://github.com/nixos/mobile-nixos>
|
||||||
|
url = "github:nixos/mobile-nixos";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
sops-nix = {
|
||||||
|
# <https://github.com/Mic92/sops-nix>
|
||||||
|
url = "github:Mic92/sops-nix";
|
||||||
|
# inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs-unpatched";
|
||||||
|
};
|
||||||
|
uninsane-dot-org = {
|
||||||
|
url = "git+https://git.uninsane.org/colin/uninsane";
|
||||||
|
# inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs-unpatched";
|
||||||
|
};
|
||||||
|
nix-serve = {
|
||||||
|
# <https://github.com/edolstra/nix-serve>
|
||||||
|
url = "github:edolstra/nix-serve";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs-unpatched,
|
||||||
|
mobile-nixos,
|
||||||
|
sops-nix,
|
||||||
|
uninsane-dot-org,
|
||||||
|
nix-serve,
|
||||||
|
...
|
||||||
|
}@inputs:
|
||||||
|
let
|
||||||
|
inherit (builtins) attrNames elem listToAttrs map mapAttrs;
|
||||||
|
mapAttrs' = f: set:
|
||||||
|
listToAttrs (map (attr: f attr set.${attr}) (attrNames set));
|
||||||
|
# mapAttrs but without the `name` argument
|
||||||
|
mapAttrValues = f: mapAttrs (_: f);
|
||||||
|
# rather than apply our nixpkgs patches as a flake input, do that here instead.
|
||||||
|
# this (temporarily?) resolves the bad UX wherein a subflake residing in the same git
|
||||||
|
# repo as the main flake causes the main flake to have an unstable hash.
|
||||||
|
nixpkgs = (import ./nixpatches/flake.nix).outputs {
|
||||||
|
self = nixpkgs;
|
||||||
|
nixpkgs = nixpkgs-unpatched;
|
||||||
|
};
|
||||||
|
|
||||||
|
nixpkgsCompiledBy = system: nixpkgs.legacyPackages."${system}";
|
||||||
|
|
||||||
|
evalHost = { name, local, target }: nixpkgs.lib.nixosSystem {
|
||||||
|
system = target;
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
nixpkgs = (if (local != null) then {
|
||||||
|
buildPlatform = local;
|
||||||
|
} else {}) // {
|
||||||
|
# TODO: does the earlier `system` arg to nixosSystem make its way here?
|
||||||
|
hostPlatform.system = target;
|
||||||
|
};
|
||||||
|
# nixpkgs.buildPlatform = local; # set by instantiate.nix instead
|
||||||
|
# nixpkgs.config.replaceStdenv = { pkgs }: pkgs.ccacheStdenv;
|
||||||
|
}
|
||||||
|
(import ./hosts/instantiate.nix { hostName = name; })
|
||||||
|
self.nixosModules.default
|
||||||
|
self.nixosModules.passthru
|
||||||
|
{
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
self.overlays.passthru
|
||||||
|
self.overlays.sane-all
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
nixosConfigurations =
|
||||||
|
let
|
||||||
|
hosts = {
|
||||||
|
servo = { name = "servo"; local = "x86_64-linux"; target = "x86_64-linux"; };
|
||||||
|
desko = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; };
|
||||||
|
lappy = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; };
|
||||||
|
moby = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; };
|
||||||
|
rescue = { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; };
|
||||||
|
};
|
||||||
|
# cross-compiled builds: instead of emulating the host, build using a cross-compiler.
|
||||||
|
# - these are faster to *build* than the emulated variants (useful when tweaking packages),
|
||||||
|
# - but fewer of their packages can be found in upstream caches.
|
||||||
|
cross = mapAttrValues evalHost hosts;
|
||||||
|
emulated = mapAttrValues
|
||||||
|
({name, local, target}: evalHost {
|
||||||
|
inherit name target;
|
||||||
|
local = null;
|
||||||
|
})
|
||||||
|
hosts;
|
||||||
|
prefixAttrs = prefix: attrs: mapAttrs'
|
||||||
|
(name: value: {
|
||||||
|
name = prefix + name;
|
||||||
|
inherit value;
|
||||||
|
})
|
||||||
|
attrs;
|
||||||
|
in
|
||||||
|
(prefixAttrs "cross-" cross) //
|
||||||
|
(prefixAttrs "emulated-" emulated) // {
|
||||||
|
# prefer native builds for these machines:
|
||||||
|
inherit (emulated) servo desko lappy rescue;
|
||||||
|
# prefer cross-compiled builds for these machines:
|
||||||
|
inherit (cross) moby;
|
||||||
|
};
|
||||||
|
|
||||||
|
# unofficial output
|
||||||
|
# this produces a EFI-bootable .img file (GPT with a /boot partition and a system (/ or /nix) partition).
|
||||||
|
# after building this:
|
||||||
|
# - flash it to a bootable medium (SD card, flash drive, HDD)
|
||||||
|
# - resize the root partition (use cfdisk)
|
||||||
|
# - mount the part
|
||||||
|
# - chown root:nixbld <part>/nix/store
|
||||||
|
# - chown root:root -R <part>/nix/store/*
|
||||||
|
# - chown root:root -R <part>/persist # if using impermanence
|
||||||
|
# - populate any important things (persist/, home/colin/.ssh, etc)
|
||||||
|
# - boot
|
||||||
|
# - if fs wasn't resized automatically, then `sudo btrfs filesystem resize max /`
|
||||||
|
# - checkout this flake into /etc/nixos AND UPDATE THE FS UUIDS.
|
||||||
|
# - `nixos-rebuild --flake './#<host>' switch`
|
||||||
|
imgs = mapAttrValues (host: host.config.system.build.img) self.nixosConfigurations;
|
||||||
|
|
||||||
|
# unofficial output
|
||||||
|
host-pkgs = mapAttrValues (host: host.config.system.build.pkgs) self.nixosConfigurations;
|
||||||
|
host-programs = mapAttrValues (host: mapAttrValues (p: p.package) host.config.sane.programs) self.nixosConfigurations;
|
||||||
|
|
||||||
|
overlays = {
|
||||||
|
# N.B.: `nix flake check` requires every overlay to take `final: prev:` at defn site,
|
||||||
|
# hence the weird redundancy.
|
||||||
|
default = final: prev: self.overlays.pkgs final prev;
|
||||||
|
sane-all = final: prev: import ./overlays/all.nix final prev;
|
||||||
|
disable-flakey-tests = final: prev: import ./overlays/disable-flakey-tests.nix final prev;
|
||||||
|
pkgs = final: prev: import ./overlays/pkgs.nix final prev;
|
||||||
|
pins = final: prev: import ./overlays/pins.nix final prev;
|
||||||
|
preferences = final: prev: import ./overlays/preferences.nix final prev;
|
||||||
|
optimizations = final: prev: import ./overlays/optimizations.nix final prev;
|
||||||
|
passthru = final: prev:
|
||||||
|
let
|
||||||
|
mobile = (import "${mobile-nixos}/overlay/overlay.nix");
|
||||||
|
uninsane = uninsane-dot-org.overlay;
|
||||||
|
# nix-serve' = nix-serve.overlay;
|
||||||
|
nix-serve' = next: prev: {
|
||||||
|
# XXX(2023/03/02): upstream isn't compatible with modern `nix`. probably the perl bindings.
|
||||||
|
# - we use the package built against `nixpkgs` specified in its flake rather than use its overlay,
|
||||||
|
# to get around this.
|
||||||
|
inherit (nix-serve.packages."${next.system}") nix-serve;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
(mobile final prev)
|
||||||
|
// (uninsane final prev)
|
||||||
|
// (nix-serve' final prev)
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules = rec {
|
||||||
|
default = sane;
|
||||||
|
sane = import ./modules;
|
||||||
|
passthru = { ... }: {
|
||||||
|
imports = [
|
||||||
|
sops-nix.nixosModules.sops
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# this includes both our native packages and all the nixpkgs packages.
|
||||||
|
legacyPackages =
|
||||||
|
let
|
||||||
|
allPkgsFor = sys: (nixpkgsCompiledBy sys).appendOverlays [
|
||||||
|
self.overlays.passthru self.overlays.pkgs
|
||||||
|
];
|
||||||
|
in {
|
||||||
|
x86_64-linux = allPkgsFor "x86_64-linux";
|
||||||
|
aarch64-linux = allPkgsFor "aarch64-linux";
|
||||||
|
};
|
||||||
|
|
||||||
|
# extract only our own packages from the full set.
|
||||||
|
# because of `nix flake check`, we flatten the package set and only surface x86_64-linux packages.
|
||||||
|
packages = mapAttrs
|
||||||
|
(system: allPkgs:
|
||||||
|
allPkgs.lib.filterAttrs (name: pkg:
|
||||||
|
# keep only packages which will pass `nix flake check`, i.e. keep only:
|
||||||
|
# - derivations (not package sets)
|
||||||
|
# - packages that build for the given platform
|
||||||
|
(! elem name [ "feeds" "pythonPackagesExtensions" ])
|
||||||
|
&& (allPkgs.lib.meta.availableOn allPkgs.stdenv.hostPlatform pkg)
|
||||||
|
)
|
||||||
|
(
|
||||||
|
# expose sane packages and chosen inputs (uninsane.org)
|
||||||
|
(import ./pkgs { pkgs = allPkgs; }) // {
|
||||||
|
inherit (allPkgs) uninsane-dot-org;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# self.legacyPackages;
|
||||||
|
{ inherit (self.legacyPackages) x86_64-linux; }
|
||||||
|
;
|
||||||
|
|
||||||
|
apps."x86_64-linux" =
|
||||||
|
let
|
||||||
|
pkgs = self.legacyPackages."x86_64-linux";
|
||||||
|
deployScript = host: addr: action: pkgs.writeShellScript "deploy-${host}" ''
|
||||||
|
nix build '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} $@
|
||||||
|
sudo nix sign-paths -r -k /run/secrets/nix_serve_privkey $(readlink ./result-${host})
|
||||||
|
|
||||||
|
# XXX: this triggers another config eval & (potentially) build.
|
||||||
|
# if the config changed between these invocations, the above signatures might not apply to the deployed config.
|
||||||
|
# let the user handle that edge case by re-running this whole command
|
||||||
|
nixos-rebuild --flake '.#${host}' ${action} --target-host colin@${addr} --use-remote-sudo $@
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
help = {
|
||||||
|
type = "app";
|
||||||
|
program = let
|
||||||
|
helpMsg = builtins.toFile "nixos-config-help-message" ''
|
||||||
|
commands:
|
||||||
|
- `nix run '.#help'`
|
||||||
|
- show this message
|
||||||
|
- `nix run '.#update-feeds'`
|
||||||
|
- updates metadata for all feeds
|
||||||
|
- `nix run '.#init-feed' <url>`
|
||||||
|
- `nix run '.#deploy-{lappy,moby,moby-test,servo}' [nixos-rebuild args ...]`
|
||||||
|
- `nix run '.#check-nur'`
|
||||||
|
'';
|
||||||
|
in builtins.toString (pkgs.writeShellScript "nixos-config-help" ''
|
||||||
|
cat ${helpMsg}
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
update-feeds = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkgs.feeds.updateScript}";
|
||||||
|
};
|
||||||
|
|
||||||
|
init-feed = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkgs.feeds.initFeedScript}";
|
||||||
|
};
|
||||||
|
|
||||||
|
deploy-lappy = {
|
||||||
|
type = "app";
|
||||||
|
program = ''${deployScript "lappy" "lappy" "switch"}'';
|
||||||
|
};
|
||||||
|
deploy-moby-test = {
|
||||||
|
type = "app";
|
||||||
|
program = ''${deployScript "moby" "moby-hn" "test"}'';
|
||||||
|
};
|
||||||
|
deploy-moby = {
|
||||||
|
type = "app";
|
||||||
|
program = ''${deployScript "moby" "moby-hn" "switch"}'';
|
||||||
|
};
|
||||||
|
deploy-servo = {
|
||||||
|
type = "app";
|
||||||
|
program = ''${deployScript "servo" "servo" "switch"}'';
|
||||||
|
};
|
||||||
|
|
||||||
|
check-nur = {
|
||||||
|
# `nix run '.#check-nur'`
|
||||||
|
# validates that my repo can be included in the Nix User Repository
|
||||||
|
type = "app";
|
||||||
|
program = builtins.toString (pkgs.writeShellScript "check-nur" ''
|
||||||
|
cd ${./.}/integrations/nur
|
||||||
|
NIX_PATH= NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix-env -f . -qa \* --meta --xml \
|
||||||
|
--allowed-uris https://static.rust-lang.org \
|
||||||
|
--option restrict-eval true \
|
||||||
|
--option allow-import-from-derivation true \
|
||||||
|
--drv-path --show-trace \
|
||||||
|
-I nixpkgs=$(nix-instantiate --find-file nixpkgs) \
|
||||||
|
-I ../../
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
templates = {
|
||||||
|
env.python-data = {
|
||||||
|
# initialize with:
|
||||||
|
# - `nix flake init -t '/home/colin/dev/nixos/#env.python-data'`
|
||||||
|
# then enter with:
|
||||||
|
# - `nix develop`
|
||||||
|
path = ./templates/env/python-data;
|
||||||
|
description = "python environment for data processing";
|
||||||
|
};
|
||||||
|
pkgs.rust-inline = {
|
||||||
|
# initialize with:
|
||||||
|
# - `nix flake init -t '/home/colin/dev/nixos/#pkgs.rust-inline'`
|
||||||
|
path = ./templates/pkgs/rust-inline;
|
||||||
|
description = "rust package and development environment (inline rust sources)";
|
||||||
|
};
|
||||||
|
pkgs.rust = {
|
||||||
|
# initialize with:
|
||||||
|
# - `nix flake init -t '/home/colin/dev/nixos/#pkgs.rust'`
|
||||||
|
path = ./templates/pkgs/rust;
|
||||||
|
description = "rust package fit to ship in nixpkgs";
|
||||||
|
};
|
||||||
|
pkgs.make = {
|
||||||
|
# initialize with:
|
||||||
|
# - `nix flake init -t '/home/colin/dev/nixos/#pkgs.make'`
|
||||||
|
path = ./templates/pkgs/make;
|
||||||
|
description = "default Makefile-based derivation";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,45 +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."gnome.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!
|
|
||||||
|
|
||||||
sane.programs.blueberry.enableFor.user.colin = false; # bluetooth manager: doesn't cross compile!
|
|
||||||
# sane.programs.brave.enableFor.user.colin = false; # 2024/06/03: fails eval if enabled on cross
|
|
||||||
# sane.programs.firefox.enableFor.user.colin = false; # 2024/06/03: this triggers an eval error in yarn stuff -- i'm doing IFD somewhere!!?
|
|
||||||
sane.programs.mepo.enableFor.user.colin = false; # 2024/06/04: doesn't cross compile (nodejs)
|
|
||||||
sane.programs.mercurial.enableFor.user.colin = false; # 2024/06/03: does not cross compile
|
|
||||||
sane.programs.nixpkgs-review.enableFor.user.colin = false; # 2024/06/03: OOMs when cross compiling
|
|
||||||
sane.programs.ntfy-sh.enableFor.user.colin = false; # 2024/06/04: doesn't cross compile (nodejs)
|
|
||||||
sane.programs.pwvucontrol.enableFor.user.colin = false; # 2024/06/03: doesn't cross compile (libspa-sys)
|
|
||||||
sane.programs."sane-scripts.bt-search".enableFor.user.colin = false; # 2024/06/03: does not cross compile
|
|
||||||
sane.programs.sequoia.enableFor.user.colin = false; # 2024/06/03: does not cross compile
|
|
||||||
sane.programs.zathura.enableFor.user.colin = false; # 2024/06/03: does not cross compile
|
|
||||||
}
|
|
|
@ -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,50 +1,43 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./fs.nix
|
./fs.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
sane.services.trust-dns.asSystemResolver = false; # TEMPORARY: TODO: re-enable trust-dns
|
|
||||||
# sane.programs.devPkgs.enableFor.user.colin = true;
|
|
||||||
# sane.guest.enable = true;
|
# sane.guest.enable = true;
|
||||||
|
|
||||||
# don't enable wifi by default: it messes with connectivity.
|
# services.distccd.enable = true;
|
||||||
# systemd.services.iwd.enable = false;
|
# sane.programs.distcc.enableFor.user.guest = true;
|
||||||
# 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;
|
|
||||||
|
|
||||||
sops.secrets.colin-passwd.neededForUsers = true;
|
sops.secrets.colin-passwd.neededForUsers = true;
|
||||||
|
|
||||||
sane.roles.build-machine.enable = true;
|
sane.roles.build-machine.enable = true;
|
||||||
|
sane.roles.ac = true;
|
||||||
sane.roles.client = true;
|
sane.roles.client = true;
|
||||||
sane.roles.dev-machine = true;
|
sane.roles.dev-machine = true;
|
||||||
sane.roles.pc = true;
|
|
||||||
sane.services.wg-home.enable = true;
|
sane.services.wg-home.enable = true;
|
||||||
sane.services.wg-home.ip = config.sane.hosts.by-name."desko".wg-home.ip;
|
sane.services.wg-home.ip = config.sane.hosts.by-name."desko".wg-home.ip;
|
||||||
sane.ovpn.addrV4 = "172.26.55.21";
|
|
||||||
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:20c1:a73c";
|
|
||||||
sane.services.duplicity.enable = true;
|
sane.services.duplicity.enable = true;
|
||||||
|
sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path;
|
||||||
|
|
||||||
sane.nixcache.remote-builders.desko = false;
|
sane.gui.sway.enable = true;
|
||||||
|
|
||||||
sane.programs.sway.enableFor.user.colin = true;
|
|
||||||
sane.programs.iphoneUtils.enableFor.user.colin = true;
|
sane.programs.iphoneUtils.enableFor.user.colin = true;
|
||||||
sane.programs.steam.enableFor.user.colin = true;
|
sane.programs.steam.enableFor.user.colin = true;
|
||||||
|
|
||||||
sane.programs."gnome.geary".config.autostart = true;
|
sane.programs.guiApps.suggestedPrograms = [ "desktopGuiApps" ];
|
||||||
sane.programs.signal-desktop.config.autostart = true;
|
sane.programs.consoleUtils.suggestedPrograms = [ "consoleMediaUtils" "desktopConsoleUtils" ];
|
||||||
|
# sane.programs.devPkgs.enableFor.user.colin = true;
|
||||||
sane.programs.nwg-panel.config = {
|
|
||||||
battery = false;
|
|
||||||
brightness = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
boot.loader.efi.canTouchEfiVariables = false;
|
||||||
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
||||||
|
|
||||||
# needed to use libimobiledevice/ifuse, for iphone sync
|
# needed to use libimobiledevice/ifuse, for iphone sync
|
||||||
services.usbmuxd.enable = true;
|
services.usbmuxd.enable = true;
|
||||||
|
|
||||||
|
# don't enable wifi by default: it messes with connectivity.
|
||||||
|
systemd.services.iwd.enable = false;
|
||||||
|
systemd.services.wpa_supplicant.enable = false;
|
||||||
|
|
||||||
# default config: https://man.archlinux.org/man/snapper-configs.5
|
# default config: https://man.archlinux.org/man/snapper-configs.5
|
||||||
# defaults to something like:
|
# defaults to something like:
|
||||||
# - hourly snapshots
|
# - hourly snapshots
|
||||||
|
@ -56,4 +49,7 @@
|
||||||
# TODO: ALLOW_USERS doesn't seem to work. still need `sudo snapper -c nix list`
|
# TODO: ALLOW_USERS doesn't seem to work. still need `sudo snapper -c nix list`
|
||||||
ALLOW_USERS = [ "colin" ];
|
ALLOW_USERS = [ "colin" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
|
||||||
|
system.stateVersion = "21.05";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
sane.persist.root-on-tmpfs = true;
|
||||||
# increase /tmp space (defaults to 50% of RAM) for building large nix things.
|
# increase /tmp space (defaults to 50% of RAM) for building large nix things.
|
||||||
# a cross-compiled kernel, particularly, will easily use 30+GB of tmp
|
# a cross-compiled kernel, particularly, will easily use 30+GB of tmp
|
||||||
fileSystems."/tmp".options = [ "size=64G" ];
|
fileSystems."/tmp".options = [ "size=64G" ];
|
||||||
|
|
||||||
fileSystems."/nix" = {
|
fileSystems."/nix" = {
|
||||||
device = "/dev/disk/by-uuid/845d85bf-761d-431b-a406-e6f20909154f";
|
# device = "/dev/disk/by-uuid/985a0a32-da52-4043-9df7-615adec2e4ff";
|
||||||
|
device = "/dev/disk/by-uuid/0ab0770b-7734-4167-88d9-6e4e20bb2a56";
|
||||||
fsType = "btrfs";
|
fsType = "btrfs";
|
||||||
options = [
|
options = [
|
||||||
"compress=zstd"
|
"compress=zstd"
|
||||||
|
@ -15,7 +17,8 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/boot" = {
|
fileSystems."/boot" = {
|
||||||
device = "/dev/disk/by-uuid/5049-9AFD";
|
# device = "/dev/disk/by-uuid/CAA7-E7D2";
|
||||||
|
device = "/dev/disk/by-uuid/41B6-BAEF";
|
||||||
fsType = "vfat";
|
fsType = "vfat";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,24 @@
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./fs.nix
|
./fs.nix
|
||||||
|
./polyfill.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
sane.roles.client = true;
|
sane.roles.client = true;
|
||||||
sane.roles.dev-machine = true;
|
sane.roles.dev-machine = true;
|
||||||
sane.roles.pc = true;
|
|
||||||
sane.services.wg-home.enable = true;
|
sane.services.wg-home.enable = true;
|
||||||
sane.services.wg-home.ip = config.sane.hosts.by-name."lappy".wg-home.ip;
|
sane.services.wg-home.ip = config.sane.hosts.by-name."lappy".wg-home.ip;
|
||||||
sane.ovpn.addrV4 = "172.23.119.72";
|
|
||||||
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:0332:aa96/128";
|
|
||||||
|
|
||||||
# sane.guest.enable = true;
|
# sane.guest.enable = true;
|
||||||
|
sane.gui.sway.enable = true;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = false;
|
||||||
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
||||||
|
|
||||||
sane.programs.stepmania.enableFor.user.colin = true;
|
sane.programs.guiApps.suggestedPrograms = [
|
||||||
sane.programs.sway.enableFor.user.colin = true;
|
"desktopGuiApps"
|
||||||
|
"stepmania"
|
||||||
sane.programs."gnome.geary".config.autostart = true;
|
];
|
||||||
sane.programs.signal-desktop.config.autostart = true;
|
sane.programs.consoleUtils.suggestedPrograms = [ "consoleMediaUtils" "desktopConsoleUtils" ];
|
||||||
|
|
||||||
sops.secrets.colin-passwd.neededForUsers = true;
|
sops.secrets.colin-passwd.neededForUsers = true;
|
||||||
|
|
||||||
|
@ -33,4 +33,7 @@
|
||||||
SUBVOLUME = "/nix";
|
SUBVOLUME = "/nix";
|
||||||
ALLOW_USERS = [ "colin" ];
|
ALLOW_USERS = [ "colin" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
|
||||||
|
system.stateVersion = "21.05";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
sane.persist.root-on-tmpfs = true;
|
||||||
|
|
||||||
fileSystems."/nix" = {
|
fileSystems."/nix" = {
|
||||||
device = "/dev/disk/by-uuid/75230e56-2c69-4e41-b03e-68475f119980";
|
device = "/dev/disk/by-uuid/75230e56-2c69-4e41-b03e-68475f119980";
|
||||||
fsType = "btrfs";
|
fsType = "btrfs";
|
||||||
|
@ -14,4 +16,24 @@
|
||||||
device = "/dev/disk/by-uuid/BD79-D6BB";
|
device = "/dev/disk/by-uuid/BD79-D6BB";
|
||||||
fsType = "vfat";
|
fsType = "vfat";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# fileSystems."/nix" = {
|
||||||
|
# device = "/dev/disk/by-uuid/5a7fa69c-9394-8144-a74c-6726048b129f";
|
||||||
|
# fsType = "btrfs";
|
||||||
|
# };
|
||||||
|
|
||||||
|
# fileSystems."/boot" = {
|
||||||
|
# device = "/dev/disk/by-uuid/4302-1685";
|
||||||
|
# fsType = "vfat";
|
||||||
|
# };
|
||||||
|
|
||||||
|
# fileSystems."/" = {
|
||||||
|
# device = "none";
|
||||||
|
# fsType = "tmpfs";
|
||||||
|
# options = [
|
||||||
|
# "mode=755"
|
||||||
|
# "size=1G"
|
||||||
|
# "defaults"
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
}
|
}
|
||||||
|
|
38
hosts/by-name/lappy/polyfill.nix
Normal file
38
hosts/by-name/lappy/polyfill.nix
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# doesn't actually *enable* anything,
|
||||||
|
# but sets up any modules such that if they *were* enabled, they'll act as expected.
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
sane.gui.sxmo = {
|
||||||
|
greeter = "sway";
|
||||||
|
settings = {
|
||||||
|
# XXX: make sure the user is part of the `input` group!
|
||||||
|
SXMO_LISGD_INPUT_DEVICE = "/dev/input/by-id/usb-Wacom_Co._Ltd._Pen_and_multitouch_sensor-event-if00";
|
||||||
|
# these identifiers are from `swaymsg -t get_inputs`
|
||||||
|
SXMO_VOLUME_BUTTON = "1:1:AT_Translated_Set_2_keyboard";
|
||||||
|
# SXMO_VOLUME_BUTTON = "none";
|
||||||
|
SXMO_POWER_BUTTON = "0:1:Power_Button";
|
||||||
|
# SXMO_POWER_BUTTON = "none";
|
||||||
|
SXMO_DISABLE_LEDS = "1";
|
||||||
|
SXMO_UNLOCK_IDLE_TIME = "120"; # default
|
||||||
|
# sxmo tries to determine device type from /proc/device-tree/compatible,
|
||||||
|
# but that doesn't seem to exist on NixOS? (or maybe it just doesn't exist
|
||||||
|
# on non-aarch64 builds).
|
||||||
|
# the device type informs (at least):
|
||||||
|
# - SXMO_WIFI_MODULE
|
||||||
|
# - SXMO_RTW_SCAN_INTERVAL
|
||||||
|
# - SXMO_SYS_FILES
|
||||||
|
# - SXMO_TOUCHSCREEN_ID
|
||||||
|
# - SXMO_MONITOR
|
||||||
|
# - SXMO_ALSA_CONTROL_NAME
|
||||||
|
# - SXMO_SWAY_SCALE
|
||||||
|
# see <repo:mil/sxmo-utils:scripts/deviceprofiles>
|
||||||
|
# SXMO_DEVICE_NAME = "pine64,pinephone-1.2";
|
||||||
|
};
|
||||||
|
package = pkgs.sxmo-utils.overrideAttrs (base: {
|
||||||
|
postPatch = (base.postPatch or "") + ''
|
||||||
|
# after volume-button navigation mode, restore full keyboard functionality
|
||||||
|
cp ${./xkb_mobile_normal_buttons} ./configs/xkb/xkb_mobile_normal_buttons
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
7
hosts/by-name/lappy/xkb_mobile_normal_buttons
Normal file
7
hosts/by-name/lappy/xkb_mobile_normal_buttons
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
xkb_keymap {
|
||||||
|
xkb_keycodes { include "evdev+aliases(qwerty)" };
|
||||||
|
xkb_types { include "complete" };
|
||||||
|
xkb_compat { include "complete" };
|
||||||
|
xkb_symbols { include "pc+us+inet(evdev)" };
|
||||||
|
xkb_geometry { include "pc(pc105)" };
|
||||||
|
};
|
12
hosts/by-name/moby/bootloader.nix
Normal file
12
hosts/by-name/moby/bootloader.nix
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
# we need space in the GPT header to place tow-boot.
|
||||||
|
# only actually need 1 MB, but better to over-allocate than under-allocate
|
||||||
|
sane.image.extraGPTPadding = 16 * 1024 * 1024;
|
||||||
|
sane.image.firstPartGap = 0;
|
||||||
|
system.build.img = pkgs.runCommand "nixos_full-disk-image.img" {} ''
|
||||||
|
cp -v ${config.system.build.img-without-firmware}/nixos.img $out
|
||||||
|
chmod +w $out
|
||||||
|
dd if=${pkgs.tow-boot-pinephone}/Tow-Boot.noenv.bin of=$out bs=1024 seek=8 conv=notrunc
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
# Pinephone
|
# Pinephone
|
||||||
|
# other setups to reference:
|
||||||
|
# - <https://hamblingreen.gitlab.io/2022/03/02/my-pinephone-setup.html>
|
||||||
|
# - sxmo Arch user. lots of app recommendations
|
||||||
#
|
#
|
||||||
# wikis, resources, ...:
|
# wikis, resources, ...:
|
||||||
# - Linux Phone Apps: <https://linuxphoneapps.org/>
|
# - Linux Phone Apps: <https://linuxphoneapps.org/>
|
||||||
|
@ -9,58 +12,160 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
./bootloader.nix
|
||||||
./fs.nix
|
./fs.nix
|
||||||
|
./gps.nix
|
||||||
|
./kernel.nix
|
||||||
|
./polyfill.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
sane.hal.pine64.enable = true;
|
|
||||||
sane.roles.client = true;
|
sane.roles.client = true;
|
||||||
sane.roles.handheld = true;
|
sane.zsh.showDeadlines = false; # unlikely to act on them when in shell
|
||||||
sane.services.wg-home.enable = true;
|
sane.services.wg-home.enable = true;
|
||||||
sane.services.wg-home.ip = config.sane.hosts.by-name."moby".wg-home.ip;
|
sane.services.wg-home.ip = config.sane.hosts.by-name."moby".wg-home.ip;
|
||||||
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,
|
# XXX colin: phosh doesn't work well with passwordless login,
|
||||||
# so set this more reliable default password should anything go wrong
|
# so set this more reliable default password should anything go wrong
|
||||||
users.users.colin.initialPassword = "147147";
|
users.users.colin.initialPassword = "147147";
|
||||||
# services.getty.autologinUser = "root"; # allows for emergency maintenance?
|
services.getty.autologinUser = "root"; # allows for emergency maintenance?
|
||||||
|
|
||||||
sops.secrets.colin-passwd.neededForUsers = true;
|
sops.secrets.colin-passwd.neededForUsers = true;
|
||||||
|
|
||||||
sane.programs.sway.enableFor.user.colin = true;
|
sane.user.persist.plaintext = [
|
||||||
sane.programs.sway.config.mod = "Mod1"; #< alt key instead of Super
|
# TODO: make this just generally conditional upon pulse being enabled?
|
||||||
sane.programs.blueberry.enableFor.user.colin = false; # bluetooth manager: doesn't cross compile!
|
".config/pulse" # persist pulseaudio volume
|
||||||
sane.programs.fcitx5.enableFor.user.colin = false; # does not cross compile
|
];
|
||||||
sane.programs.mercurial.enableFor.user.colin = false; # does not cross compile
|
|
||||||
sane.programs.nvme-cli.enableFor.system = false; # does not cross compile (libhugetlbfs)
|
|
||||||
|
|
||||||
# enabled for easier debugging
|
sane.gui.sxmo.enable = true;
|
||||||
sane.programs.eg25-control.enableFor.user.colin = true;
|
sane.services.eg25-manager.enable = true;
|
||||||
sane.programs.rtl8723cs-wowlan.enableFor.user.colin = true;
|
sane.programs.guiApps.suggestedPrograms = [ "handheldGuiApps" ];
|
||||||
|
# sane.programs.consoleUtils.enableFor.user.colin = false;
|
||||||
|
# sane.programs.guiApps.enableFor.user.colin = false;
|
||||||
|
sane.programs.sequoia.enableFor.user.colin = false;
|
||||||
|
sane.programs.tuiApps.enableFor.user.colin = false; # visidata, others, don't compile well
|
||||||
|
# disabled for faster deploys
|
||||||
|
sane.programs.soundconverter.enableFor.user.colin = false;
|
||||||
|
|
||||||
# sane.programs.ntfy-sh.config.autostart = true;
|
# sane.programs.firefox.mime.priority = 300; # prefer other browsers when possible
|
||||||
sane.programs.dino.config.autostart = true;
|
# HACK/TODO: make `programs.P.env.VAR` behave according to `mime.priority`
|
||||||
# sane.programs.signal-desktop.config.autostart = true; # TODO: enable once electron stops derping.
|
# sane.programs.firefox.env = lib.mkForce {};
|
||||||
# sane.programs."gnome.geary".config.autostart = true;
|
# sane.programs.epiphany.env.BROWSER = "epiphany";
|
||||||
# sane.programs.calls.config.autostart = true;
|
# sane.programs.firefox.enableFor.user.colin = false; # use epiphany instead
|
||||||
|
|
||||||
sane.programs.pipewire.config = {
|
# sane.programs.mpv.enableFor.user.colin = true;
|
||||||
# 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
boot.loader.efi.canTouchEfiVariables = false;
|
||||||
# /boot space is at a premium. default was 20.
|
# /boot space is at a premium. default was 20.
|
||||||
# even 10 can be too much
|
# even 10 can be too much
|
||||||
boot.loader.generic-extlinux-compatible.configurationLimit = 8;
|
boot.loader.generic-extlinux-compatible.configurationLimit = 8;
|
||||||
|
# mobile.bootloader.enable = false;
|
||||||
|
# mobile.boot.stage-1.enable = false;
|
||||||
|
# boot.initrd.systemd.enable = false;
|
||||||
|
# boot.initrd.services.swraid.enable = false; # attempt to fix dm_mod stuff
|
||||||
|
# disable proximity sensor.
|
||||||
|
# the filtering/calibration is bad that it causes the screen to go fully dark at times.
|
||||||
|
boot.blacklistedKernelModules = [ "stk3310" ];
|
||||||
|
|
||||||
|
# without this some GUI apps fail: `DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory`
|
||||||
|
# this is because they can't allocate enough video ram.
|
||||||
|
# the default CMA seems to be 32M.
|
||||||
|
# i was running fine with 256MB from 2022/07-ish through 2022/12-ish, but then the phone quit reliably coming back from sleep: maybe a memory leak?
|
||||||
|
# `cat /proc/meminfo` to see CmaTotal/CmaFree if interested in tuning this.
|
||||||
|
boot.kernelParams = [ "cma=512M" ];
|
||||||
|
|
||||||
|
# mobile-nixos' /lib/firmware includes:
|
||||||
|
# rtl_bt (bluetooth)
|
||||||
|
# anx7688-fw.bin (USB-C -> HDMI bridge)
|
||||||
|
# ov5640_af.bin (camera module)
|
||||||
|
# hardware.firmware = [ config.mobile.device.firmware ];
|
||||||
|
hardware.firmware = [ pkgs.rtl8723cs-firmware ];
|
||||||
|
|
||||||
|
system.stateVersion = "21.11";
|
||||||
|
|
||||||
|
# defined: https://www.freedesktop.org/software/systemd/man/machine-info.html
|
||||||
|
# XXX colin: not sure which, if any, software makes use of this
|
||||||
|
environment.etc."machine-info".text = ''
|
||||||
|
CHASSIS="handset"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# enable rotation sensor
|
||||||
|
hardware.sensor.iio.enable = true;
|
||||||
|
|
||||||
|
# inject specialized alsa configs via the environment.
|
||||||
|
# specifically, this gets the pinephone headphones & internal earpiece working.
|
||||||
|
# see pkgs/patched/alsa-ucm-conf for more info.
|
||||||
|
environment.variables.ALSA_CONFIG_UCM2 = "/run/current-system/sw/share/alsa/ucm2";
|
||||||
|
environment.pathsToLink = [ "/share/alsa/ucm2" ];
|
||||||
|
environment.systemPackages = [ pkgs.alsa-ucm-conf-sane ];
|
||||||
|
systemd =
|
||||||
|
let ucm-env = config.environment.variables.ALSA_CONFIG_UCM2;
|
||||||
|
in {
|
||||||
|
# cribbed from <repo:nixos/mobile-nixos:modules/quirks/audio.nix>
|
||||||
|
|
||||||
|
# pulseaudio
|
||||||
|
user.services.pulseaudio.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
services.pulseaudio.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
|
||||||
|
# pipewire
|
||||||
|
user.services.pipewire.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
user.services.pipewire-pulse.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
user.services.wireplumber.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
services.pipewire.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
services.pipewire-pulse.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
services.wireplumber.environment.ALSA_CONFIG_UCM2 = ucm-env;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.udev.extraRules = let
|
||||||
|
chmod = "${pkgs.coreutils}/bin/chmod";
|
||||||
|
chown = "${pkgs.coreutils}/bin/chown";
|
||||||
|
in ''
|
||||||
|
# make Pinephone flashlight writable by user.
|
||||||
|
# taken from postmarketOS: <repo:postmarketOS/pmaports:device/main/device-pine64-pinephone/60-flashlight.rules>
|
||||||
|
SUBSYSTEM=="leds", DEVPATH=="*/*:flash", RUN+="${chmod} g+w /sys%p/brightness /sys%p/flash_strobe", RUN+="${chown} :video /sys%p/brightness /sys%p/flash_strobe"
|
||||||
|
|
||||||
|
# make Pinephone front LEDs writable by user.
|
||||||
|
SUBSYSTEM=="leds", DEVPATH=="*/*:indicator", RUN+="${chmod} g+w /sys%p/brightness", RUN+="${chown} :video /sys%p/brightness"
|
||||||
|
'';
|
||||||
|
|
||||||
|
hardware.opengl.driSupport = true;
|
||||||
|
|
||||||
|
services.xserver.displayManager.job.preStart = let
|
||||||
|
dmesg = "${pkgs.util-linux}/bin/dmesg";
|
||||||
|
grep = "${pkgs.gnugrep}/bin/grep";
|
||||||
|
modprobe = "${pkgs.kmod}/bin/modprobe";
|
||||||
|
in ''
|
||||||
|
# common boot failure:
|
||||||
|
# blank screen (no backlight even), with the following log:
|
||||||
|
# ```syslog
|
||||||
|
# sun8i-dw-hdmi 1ee0000.hdmi: Couldn't get the HDMI PHY
|
||||||
|
# ...
|
||||||
|
# sun4i-drm display-engine: Couldn't bind all pipelines components
|
||||||
|
# ...
|
||||||
|
# sun8i-dw-hdmi: probe of 1ee0000.hdmi failed with error -17
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# in particular, that `probe ... failed` occurs *only* on failed boots
|
||||||
|
# (the other messages might sometimes occur even on successful runs?)
|
||||||
|
#
|
||||||
|
# reloading the sun8i hdmi driver usually gets the screen on, showing boot text.
|
||||||
|
# then restarting display-manager.service gets us to the login.
|
||||||
|
#
|
||||||
|
# NB: the above log is default level. though less specific, there's a `err` level message that also signals this:
|
||||||
|
# sun4i-drm display-engine: failed to bind 1ee0000.hdmi (ops sun8i_dw_hdmi_ops [sun8i_drm_hdmi]): -17
|
||||||
|
# NB: this is the most common, but not the only, failure mode for `display-manager`.
|
||||||
|
# another error seems characterized by these dmesg logs, in which reprobing sun8i_drm_hdmi does not fix:
|
||||||
|
# ```syslog
|
||||||
|
# sun6i-mipi-dsi 1ca0000.dsi: Couldn't get the MIPI D-PHY
|
||||||
|
# sun4i-drm display-engine: Couldn't bind all pipelines components
|
||||||
|
# sun6i-mipi-dsi 1ca0000.dsi: Couldn't register our component
|
||||||
|
# ```
|
||||||
|
|
||||||
|
if (${dmesg} --kernel --level err --color=never --notime | ${grep} -q 'sun4i-drm display-engine: failed to bind 1ee0000.hdmi')
|
||||||
|
then
|
||||||
|
echo "reprobing sun8i_drm_hdmi"
|
||||||
|
# if a command here fails it errors the whole service, so prefer to log instead
|
||||||
|
${modprobe} -r sun8i_drm_hdmi || echo "failed to unload sun8i_drm_hdmi"
|
||||||
|
${modprobe} sun8i_drm_hdmi || echo "failed to load sub8i_drm_hdmi"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
sane.persist.root-on-tmpfs = true;
|
||||||
fileSystems."/nix" = {
|
fileSystems."/nix" = {
|
||||||
device = "/dev/disk/by-uuid/1f1271f8-53ce-4081-8a29-60a4a6b5d6f9";
|
device = "/dev/disk/by-uuid/1f1271f8-53ce-4081-8a29-60a4a6b5d6f9";
|
||||||
fsType = "btrfs";
|
fsType = "btrfs";
|
||||||
|
|
24
hosts/by-name/moby/gps.nix
Normal file
24
hosts/by-name/moby/gps.nix
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# pinephone GPS happens in EG25 modem
|
||||||
|
# serial control interface to modem is /dev/ttyUSB2
|
||||||
|
# after enabling GPS, readout is /dev/ttyUSB1
|
||||||
|
#
|
||||||
|
# minimal process to enable modem and GPS:
|
||||||
|
# - `echo 1 > /sys/class/modem-power/modem-power/device/powered`
|
||||||
|
# - `screen /dev/ttyUSB2 115200`
|
||||||
|
# - `AT+QGPSCFG="nmeasrc",1`
|
||||||
|
# - `AT+QGPS=1`
|
||||||
|
#
|
||||||
|
# now, something like `gpsd` can directly read from /dev/ttyUSB1.
|
||||||
|
#
|
||||||
|
# initial GPS fix can take 15+ minutes.
|
||||||
|
# meanwhile, services like eg25-manager can speed this up by uploading assisted GPS data to the modem.
|
||||||
|
#
|
||||||
|
# geoclue somehow fits in here as a geospatial provider that leverages GPS and also other sources like radio towers
|
||||||
|
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
services.gpsd.enable = true;
|
||||||
|
services.gpsd.devices = [ "/dev/ttyUSB1" ];
|
||||||
|
|
||||||
|
# TODO: enable eg25-manager, and bring online both the modem and GPS on boot
|
||||||
|
}
|
83
hosts/by-name/moby/kernel.nix
Normal file
83
hosts/by-name/moby/kernel.nix
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{ lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
# use the last commit on the 5.18 branch (5.18.14)
|
||||||
|
# manjaro's changes between kernel patch versions tend to be minimal if any.
|
||||||
|
manjaroBase = "https://gitlab.manjaro.org/manjaro-arm/packages/core/linux/-/raw/25bd828cd47b1c6e09fcbcf394a649b89d2876dd";
|
||||||
|
manjaroPatch = name: sha256: {
|
||||||
|
inherit name;
|
||||||
|
patch = pkgs.fetchpatch {
|
||||||
|
inherit name;
|
||||||
|
url = "${manjaroBase}/${name}?inline=false";
|
||||||
|
inherit sha256;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# the idea for patching off Manjaro's kernel comes from jakewaksbaum:
|
||||||
|
# - https://git.sr.ht/~jakewaksbaum/pi/tree/af20aae5653545d6e67a459b59ee3e1ca8a680b0/item/kernel/default.nix
|
||||||
|
# - he later abandoned this, i think because he's using the Pinephone Pro which received mainline support.
|
||||||
|
manjaroPatches = [
|
||||||
|
(manjaroPatch
|
||||||
|
"1001-arm64-dts-allwinner-add-hdmi-sound-to-pine-devices.patch"
|
||||||
|
"sha256-DApd791A+AxB28Ven/MVAyuyVphdo8KQDx8O7oxVPnc="
|
||||||
|
)
|
||||||
|
# these patches below are critical to enable wifi (RTL8723CS)
|
||||||
|
# - the alternative is a wholly forked kernel by megi/megous:
|
||||||
|
# - https://xnux.eu/howtos/build-pinephone-kernel.html#toc-how-to-build-megi-s-pinehpone-kernel
|
||||||
|
# - i don't know if these patches are based on megi's or original
|
||||||
|
(manjaroPatch
|
||||||
|
"2001-Bluetooth-Add-new-quirk-for-broken-local-ext-features.patch"
|
||||||
|
"sha256-CExhJuUWivegxPdnzKINEsKrMFx/m/1kOZFmlZ2SEOc="
|
||||||
|
)
|
||||||
|
(manjaroPatch
|
||||||
|
"2002-Bluetooth-btrtl-add-support-for-the-RTL8723CS.patch"
|
||||||
|
"sha256-dDdvOphTcP/Aog93HyH+L9m55laTgtjndPSE4/rnzUA="
|
||||||
|
)
|
||||||
|
(manjaroPatch
|
||||||
|
"2004-arm64-dts-allwinner-enable-bluetooth-pinetab-pinepho.patch"
|
||||||
|
"sha256-o43P3WzXyHK1PF+Kdter4asuyGAEKO6wf5ixcco2kCQ="
|
||||||
|
)
|
||||||
|
# XXX: this one has a Makefile, which hardcodes /sbin/depmod:
|
||||||
|
# - drivers/staging/rtl8723cs/Makefile
|
||||||
|
# - not sure if this is problematic?
|
||||||
|
(manjaroPatch
|
||||||
|
"2005-staging-add-rtl8723cs-driver.patch"
|
||||||
|
"sha256-6ywm3dQQ5JYl60CLKarxlSUukwi4QzqctCj3tVgzFbo="
|
||||||
|
)
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# use Megi's kernel:
|
||||||
|
# even with the Manjaro patches, stock 5.18 has a few issues on Pinephone:
|
||||||
|
# - no battery charging
|
||||||
|
# - phone rotation sensor is off by 90 degrees
|
||||||
|
# - ambient light sensor causes screen brightness to be shakey
|
||||||
|
# - phosh greeter may not appear after wake from sleep
|
||||||
|
boot.kernelPackages = pkgs.linuxPackagesFor pkgs.linux-megous;
|
||||||
|
|
||||||
|
# alternatively, use nixos' kernel and add the stuff we want:
|
||||||
|
# # cross-compilation optimization:
|
||||||
|
# boot.kernelPackages =
|
||||||
|
# let p = (import nixpkgs { localSystem = "x86_64-linux"; });
|
||||||
|
# in p.pkgsCross.aarch64-multiplatform.linuxPackages_5_18;
|
||||||
|
# # non-cross:
|
||||||
|
# # boot.kernelPackages = pkgs.linuxPackages_5_18;
|
||||||
|
|
||||||
|
# boot.kernelPatches = manjaroPatches ++ [
|
||||||
|
# (patchDefconfig kernelConfig)
|
||||||
|
# ];
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform.linux-kernel = {
|
||||||
|
# defaults:
|
||||||
|
name = "aarch64-multiplatform";
|
||||||
|
baseConfig = "defconfig";
|
||||||
|
DTB = true;
|
||||||
|
autoModules = true;
|
||||||
|
preferBuiltin = true;
|
||||||
|
# extraConfig = ...
|
||||||
|
# ^-- raspberry pi stuff: we don't need it.
|
||||||
|
|
||||||
|
# target = "Image"; # <-- default
|
||||||
|
target = "Image.gz"; # <-- compress the kernel image
|
||||||
|
# target = "zImage"; # <-- confuses other parts of nixos :-(
|
||||||
|
};
|
||||||
|
}
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
179
hosts/by-name/moby/polyfill.nix
Normal file
179
hosts/by-name/moby/polyfill.nix
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
# this file configures preferences per program, without actually enabling any programs.
|
||||||
|
# the goal is to separate the place where we decide *what* to use (i.e. `sane.programs.firefox.enable = true` -- at the toplevel)
|
||||||
|
# from where we specific how that thing should behave *if* it's in use.
|
||||||
|
#
|
||||||
|
# NixOS backgrounds:
|
||||||
|
# - <https://github.com/NixOS/nixos-artwork>
|
||||||
|
# - <https://github.com/NixOS/nixos-artwork/issues/50> (colorful; unmerged)
|
||||||
|
# - <https://github.com/NixOS/nixos-artwork/pull/60/files> (desktop-oriented; clean; unmerged)
|
||||||
|
# - <https://itsfoss.com/content/images/2023/04/nixos-tutorials.png>
|
||||||
|
|
||||||
|
{ lib, pkgs, sane-lib, ... }:
|
||||||
|
let
|
||||||
|
# TODO: generate this from the .svg
|
||||||
|
# bg = ./nixos-bg-02.png;
|
||||||
|
bg = pkgs.runCommand "nixos-bg.png" { nativeBuildInputs = [ pkgs.inkscape ]; } ''
|
||||||
|
inkscape ${./nixos-bg-02.svg} -o $out
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
sane.programs.firefox.config = {
|
||||||
|
# compromise impermanence for the sake of usability
|
||||||
|
persistCache = "private";
|
||||||
|
persistData = "private";
|
||||||
|
|
||||||
|
# i don't do crypto stuff on moby
|
||||||
|
addons.ether-metamask.enable = false;
|
||||||
|
# sidebery UX doesn't make sense on small screen
|
||||||
|
addons.sidebery.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
sane.gui.sxmo = {
|
||||||
|
nogesture = true;
|
||||||
|
settings = {
|
||||||
|
### hardware: touch screen
|
||||||
|
SXMO_LISGD_INPUT_DEVICE = "/dev/input/by-path/platform-1c2ac00.i2c-event";
|
||||||
|
# vol and power are detected correctly by upstream
|
||||||
|
|
||||||
|
|
||||||
|
### preferences
|
||||||
|
# notable bemenu options:
|
||||||
|
# - see `bemenu --help` for all
|
||||||
|
# -P, --prefix text to show before highlighted item.
|
||||||
|
# --scrollbar display scrollbar. (none (default), always, autohide)
|
||||||
|
# -H, --line-height defines the height to make each menu line (0 = default height). (wx)
|
||||||
|
# -M, --margin defines the empty space on either side of the menu. (wx)
|
||||||
|
# -W, --width-factor defines the relative width factor of the menu (from 0 to 1). (wx)
|
||||||
|
# -B, --border defines the width of the border in pixels around the menu. (wx)
|
||||||
|
# -R --border-radius defines the radius of the border around the menu (0 = no curved borders).
|
||||||
|
# --ch defines the height of the cursor (0 = scales with line height). (wx)
|
||||||
|
# --cw defines the width of the cursor. (wx)
|
||||||
|
# --hp defines the horizontal padding for the entries in single line mode. (wx)
|
||||||
|
# --fn defines the font to be used ('name [size]'). (wx)
|
||||||
|
# --tb defines the title background color. (wx)
|
||||||
|
# --tf defines the title foreground color. (wx)
|
||||||
|
# --fb defines the filter background color. (wx)
|
||||||
|
# --ff defines the filter foreground color. (wx)
|
||||||
|
# --nb defines the normal background color. (wx)
|
||||||
|
# --nf defines the normal foreground color. (wx)
|
||||||
|
# --hb defines the highlighted background color. (wx)
|
||||||
|
# --hf defines the highlighted foreground color. (wx)
|
||||||
|
# --fbb defines the feedback background color. (wx)
|
||||||
|
# --fbf defines the feedback foreground color. (wx)
|
||||||
|
# --sb defines the selected background color. (wx)
|
||||||
|
# --sf defines the selected foreground color. (wx)
|
||||||
|
# --ab defines the alternating background color. (wx)
|
||||||
|
# --af defines the alternating foreground color. (wx)
|
||||||
|
# --scb defines the scrollbar background color. (wx)
|
||||||
|
# --scf defines the scrollbar foreground color. (wx)
|
||||||
|
# --bdr defines the border color. (wx)
|
||||||
|
#
|
||||||
|
# colors are specified as `#RRGGBB`
|
||||||
|
# defaults:
|
||||||
|
# --ab "#222222"
|
||||||
|
# --af "#bbbbbb"
|
||||||
|
# --bdr "#005577"
|
||||||
|
# --border 3
|
||||||
|
# --cb "#222222"
|
||||||
|
# --center
|
||||||
|
# --cf "#bbbbbb"
|
||||||
|
# --fb "#222222"
|
||||||
|
# --fbb "#eeeeee"
|
||||||
|
# --fbf "#222222"
|
||||||
|
# --ff "#bbbbbb"
|
||||||
|
# --fixed-height
|
||||||
|
# --fn 'Sxmo 14'
|
||||||
|
# --hb "#005577"
|
||||||
|
# --hf "#eeeeee"
|
||||||
|
# --line-height 20
|
||||||
|
# --list 16
|
||||||
|
# --margin 40
|
||||||
|
# --nb "#222222"
|
||||||
|
# --nf "#bbbbbb"
|
||||||
|
# --no-overlap
|
||||||
|
# --no-spacing
|
||||||
|
# --sb "#323232"
|
||||||
|
# --scb "#005577"
|
||||||
|
# --scf "#eeeeee"
|
||||||
|
# --scrollbar autohide
|
||||||
|
# --tb "#005577"
|
||||||
|
# --tf "#eeeeee"
|
||||||
|
# --wrap
|
||||||
|
BEMENU_OPTS = let
|
||||||
|
bg = "#1d1721"; # slight purple
|
||||||
|
fg0 = "#d8d8d8"; # inactive text (light grey)
|
||||||
|
fg1 = "#ffffff"; # active text (white)
|
||||||
|
accent0 = "#1f5e54"; # darker but saturated teal
|
||||||
|
accent1 = "#418379"; # teal (matches nixos-bg)
|
||||||
|
accent2 = "#5b938a"; # brighter but muted teal
|
||||||
|
in lib.concatStringsSep " " [
|
||||||
|
"--wrap --scrollbar autohide --fixed-height"
|
||||||
|
"--center --margin 45"
|
||||||
|
"--no-spacing"
|
||||||
|
# XXX: font size doesn't seem to take effect (would prefer larger)
|
||||||
|
"--fn 'Sxmo 14' --line-height 22 --border 3"
|
||||||
|
"--bdr '${accent0}'" # border
|
||||||
|
"--scf '${accent2}' --scb '${accent0}'" # scrollbar
|
||||||
|
"--tb '${accent0}' --tf '${fg0}'" # title
|
||||||
|
"--fb '${accent0}' --ff '${fg1}'" # filter (i.e. text that's been entered)
|
||||||
|
"--hb '${accent1}' --hf '${fg1}'" # selected item
|
||||||
|
"--nb '${bg}' --nf '${fg0}'" # normal lines (even)
|
||||||
|
"--ab '${bg}' --af '${fg0}'" # alternated lines (odd)
|
||||||
|
"--cf '${accent0}' --cb '${accent0}'" # cursor (not very useful)
|
||||||
|
];
|
||||||
|
DEFAULT_COUNTRY = "US";
|
||||||
|
|
||||||
|
# BEMENU lines (wayland DMENU):
|
||||||
|
# - camera is 9th entry
|
||||||
|
# - flashlight is 10th entry
|
||||||
|
# - config is 14th entry. inside that:
|
||||||
|
# - autorotate is 11th entry
|
||||||
|
# - system menu is 19th entry
|
||||||
|
# - close is 20th entry
|
||||||
|
# - power is 15th entry
|
||||||
|
# - close is 16th entry
|
||||||
|
SXMO_BEMENU_LANDSCAPE_LINES = "11"; # default 8
|
||||||
|
SXMO_BEMENU_PORTRAIT_LINES = "16"; # default 16
|
||||||
|
SXMO_BG_IMG = "${bg}";
|
||||||
|
SXMO_LOCK_IDLE_TIME = "15"; # how long between screenoff -> lock -> back to screenoff (default: 8)
|
||||||
|
# gravity: how far to tilt the device before the screen rotates
|
||||||
|
# for a given setting, normal <-> invert requires more movement then left <-> right
|
||||||
|
# i.e. the settingd doesn't feel completely symmetric
|
||||||
|
# SXMO_ROTATION_GRAVITY default is 16374
|
||||||
|
# SXMO_ROTATION_GRAVITY = "12800"; # uncomfortably high
|
||||||
|
# SXMO_ROTATION_GRAVITY = "12500"; # kinda uncomfortable when walking
|
||||||
|
SXMO_ROTATION_GRAVITY = "12000";
|
||||||
|
SXMO_SCREENSHOT_DIR = "/home/colin/Pictures"; # default: "$HOME"
|
||||||
|
# test new scales by running `swaymsg -- output DSI-1 scale x.y`
|
||||||
|
# SXMO_SWAY_SCALE = "1.5"; # hard to press gPodder icons
|
||||||
|
SXMO_SWAY_SCALE = "1.8";
|
||||||
|
# SXMO_SWAY_SCALE = "2";
|
||||||
|
SXMO_WORKSPACE_WRAPPING = "5"; # how many workspaces. default: 4
|
||||||
|
|
||||||
|
# wvkbd layers:
|
||||||
|
# - full
|
||||||
|
# - landscape
|
||||||
|
# - special (e.g. coding symbols like ~)
|
||||||
|
# - emoji
|
||||||
|
# - nav
|
||||||
|
# - simple (like landscape, but no parens/tab/etc; even fewer chars)
|
||||||
|
# - simplegrid (simple, but grid layout)
|
||||||
|
# - dialer (digits)
|
||||||
|
# - cyrillic
|
||||||
|
# - arabic
|
||||||
|
# - persian
|
||||||
|
# - greek
|
||||||
|
# - georgian
|
||||||
|
WVKBD_LANDSCAPE_LAYERS = "landscape,special,emoji";
|
||||||
|
WVKBD_LAYERS = "full,special,emoji";
|
||||||
|
};
|
||||||
|
package = pkgs.sxmo-utils.overrideAttrs (base: {
|
||||||
|
postPatch = (base.postPatch or "") + ''
|
||||||
|
cat <<EOF >> ./configs/default_hooks/sxmo_hook_start.sh
|
||||||
|
# rotate UI based on physical display angle by default
|
||||||
|
sxmo_daemons.sh start autorotate sxmo_autorotate.sh
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -4,11 +4,12 @@
|
||||||
./fs.nix
|
./fs.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
boot.loader.generic-extlinux-compatible.enable = true;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = false;
|
||||||
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
||||||
sane.persist.enable = false; # what we mean here is that the image is immutable; `/` is still tmpfs.
|
# sane.persist.enable = false; # TODO: disable (but run `nix flake check` to ensure it works!)
|
||||||
sane.nixcache.enable = false; # don't want to be calling out to dead machines that we're *trying* to rescue
|
sane.nixcache.enable = false; # don't want to be calling out to dead machines that we're *trying* to rescue
|
||||||
|
|
||||||
# auto-login at shell
|
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
|
||||||
services.getty.autologinUser = "colin";
|
system.stateVersion = "21.05";
|
||||||
# users.users.colin.initialPassword = "colin";
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
fileSystems."/nix" = {
|
fileSystems."/" = {
|
||||||
device = "/dev/disk/by-uuid/44445555-6666-7777-8888-999900001111";
|
device = "/dev/disk/by-uuid/44445555-6666-7777-8888-999900001111";
|
||||||
fsType = "ext4";
|
fsType = "ext4";
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,30 +14,27 @@
|
||||||
signaldctl.enableFor.user.colin = true;
|
signaldctl.enableFor.user.colin = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sane.roles.ac = true;
|
||||||
sane.roles.build-machine.enable = true;
|
sane.roles.build-machine.enable = true;
|
||||||
sane.programs.zsh.config.showDeadlines = false; # ~/knowledge doesn't always exist
|
sane.roles.build-machine.emulation = false;
|
||||||
|
sane.zsh.showDeadlines = false; # ~/knowledge doesn't always exist
|
||||||
sane.programs.consoleUtils.suggestedPrograms = [
|
sane.programs.consoleUtils.suggestedPrograms = [
|
||||||
"consoleMediaUtils" # notably, for go2tv / casting
|
"desktopConsoleUtils"
|
||||||
"pcConsoleUtils"
|
|
||||||
"sane-scripts.stop-all-servo"
|
"sane-scripts.stop-all-servo"
|
||||||
];
|
];
|
||||||
sane.services.dyn-dns.enable = true;
|
sane.services.dyn-dns.enable = true;
|
||||||
sane.services.trust-dns.asSystemResolver = false; # TODO: enable once it's all working well
|
|
||||||
sane.services.wg-home.enable = true;
|
sane.services.wg-home.enable = true;
|
||||||
sane.services.wg-home.visibleToWan = true;
|
sane.services.wg-home.enableWan = true;
|
||||||
sane.services.wg-home.forwardToWan = true;
|
|
||||||
sane.services.wg-home.routeThroughServo = false;
|
|
||||||
sane.services.wg-home.ip = config.sane.hosts.by-name."servo".wg-home.ip;
|
sane.services.wg-home.ip = config.sane.hosts.by-name."servo".wg-home.ip;
|
||||||
sane.ovpn.addrV4 = "172.23.174.114";
|
sane.nixcache.substituters.servo = false;
|
||||||
# sane.ovpn.addrV6 = "fd00:0000:1337:cafe:1111:1111:8df3:14b0";
|
sane.nixcache.substituters.desko = false;
|
||||||
sane.nixcache.remote-builders.desko = false;
|
|
||||||
sane.nixcache.remote-builders.servo = false;
|
|
||||||
# sane.services.duplicity.enable = true; # TODO: re-enable after HW upgrade
|
# sane.services.duplicity.enable = true; # TODO: re-enable after HW upgrade
|
||||||
|
|
||||||
# automatically log in at the virtual consoles.
|
# automatically log in at the virtual consoles.
|
||||||
# using root here makes sure we always have an escape hatch
|
# using root here makes sure we always have an escape hatch
|
||||||
services.getty.autologinUser = "root";
|
services.getty.autologinUser = "root";
|
||||||
|
|
||||||
|
boot.loader.efi.canTouchEfiVariables = false;
|
||||||
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
|
||||||
|
|
||||||
# both transmission and ipfs try to set different net defaults.
|
# both transmission and ipfs try to set different net defaults.
|
||||||
|
@ -45,5 +42,13 @@
|
||||||
boot.kernel.sysctl = {
|
boot.kernel.sysctl = {
|
||||||
"net.core.rmem_max" = 4194304; # 4MB
|
"net.core.rmem_max" = 4194304; # 4MB
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# This value determines the NixOS release from which the default
|
||||||
|
# settings for stateful data, like file locations and database versions
|
||||||
|
# on your system were taken. It‘s perfectly fine and recommended to leave
|
||||||
|
# this value at the release version of the first install of this system.
|
||||||
|
# Before changing this value read the documentation for this option
|
||||||
|
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
|
||||||
|
system.stateVersion = "21.11";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,67 +1,7 @@
|
||||||
# zfs docs:
|
|
||||||
# - <https://nixos.wiki/wiki/ZFS>
|
|
||||||
# - <repo:nixos/nixpkgs:nixos/modules/tasks/filesystems/zfs.nix>
|
|
||||||
#
|
|
||||||
# zfs check health: `zpool status`
|
|
||||||
#
|
|
||||||
# zfs pool creation (requires `boot.supportedFilesystems = [ "zfs" ];`
|
|
||||||
# - 1. identify disk IDs: `ls -l /dev/disk/by-id`
|
|
||||||
# - 2. pool these disks: `zpool create -f -m legacy pool raidz ata-ST4000VN008-2DR166_WDH0VB45 ata-ST4000VN008-2DR166_WDH17616 ata-ST4000VN008-2DR166_WDH0VC8Q ata-ST4000VN008-2DR166_WDH17680`
|
|
||||||
# - legacy documented: <https://superuser.com/questions/790036/what-is-a-zfs-legacy-mount-point>
|
|
||||||
# - 3. enable acl support: `zfs set acltype=posixacl pool`
|
|
||||||
#
|
|
||||||
# import pools: `zpool import pool`
|
|
||||||
# show zfs datasets: `zfs list` (will be empty if haven't imported)
|
|
||||||
# show zfs properties (e.g. compression): `zfs get all pool`
|
|
||||||
# set zfs properties: `zfs set compression=on pool`
|
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
# hostId: not used for anything except zfs guardrail?
|
sane.persist.root-on-tmpfs = true;
|
||||||
# [hex(ord(x)) for x in 'serv']
|
|
||||||
networking.hostId = "73657276";
|
|
||||||
boot.supportedFilesystems = [ "zfs" ];
|
|
||||||
# boot.zfs.enabled = true;
|
|
||||||
boot.zfs.forceImportRoot = false;
|
|
||||||
# scrub all zfs pools weekly:
|
|
||||||
services.zfs.autoScrub.enable = true;
|
|
||||||
boot.extraModprobeConfig = ''
|
|
||||||
### zfs_arc_max tunable:
|
|
||||||
# ZFS likes to use half the ram for its own cache and let the kernel push everything else to swap.
|
|
||||||
# so, reduce its cache size
|
|
||||||
# see: <https://askubuntu.com/a/1290387>
|
|
||||||
# see: <https://serverfault.com/a/1119083>
|
|
||||||
# see: <https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max>
|
|
||||||
# for all tunables, see: `man 4 zfs`
|
|
||||||
# to update these parameters without rebooting:
|
|
||||||
# - `echo '4294967296' | sane-sudo-redirect /sys/module/zfs/parameters/zfs_arc_max`
|
|
||||||
### zfs_bclone_enabled tunable
|
|
||||||
# this allows `cp --reflink=always FOO BAR` to work. i.e. shallow copies.
|
|
||||||
# it's unstable as of 2.2.3. led to *actual* corruption in 2.2.1, but hopefully better by now.
|
|
||||||
# - <https://github.com/openzfs/zfs/issues/405>
|
|
||||||
# note that `du -h` won't *always* show the reduced size for reflink'd files (?).
|
|
||||||
# `zpool get all | grep clone` seems to be the way to *actually* see how much data is being deduped
|
|
||||||
options zfs zfs_arc_max=4294967296 zfs_bclone_enabled=1
|
|
||||||
'';
|
|
||||||
# to be able to mount the pool like this, make sure to tell zfs to NOT manage it itself.
|
|
||||||
# otherwise local-fs.target will FAIL and you will be dropped into a rescue shell.
|
|
||||||
# - `zfs set mountpoint=legacy pool`
|
|
||||||
# if done correctly, the pool can be mounted before this `fileSystems` entry is created:
|
|
||||||
# - `sudo mount -t zfs pool /mnt/persist/pool`
|
|
||||||
fileSystems."/mnt/pool" = {
|
|
||||||
device = "pool";
|
|
||||||
fsType = "zfs";
|
|
||||||
options = [ "acl" ]; #< not sure if this `acl` flag is actually necessary. it mounts without it.
|
|
||||||
};
|
|
||||||
# services.zfs.zed = ... # TODO: zfs can send me emails when disks fail
|
|
||||||
sane.programs.sysadminUtils.suggestedPrograms = [ "zfs" ];
|
|
||||||
|
|
||||||
sane.persist.stores."ext" = {
|
|
||||||
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.
|
# increase /tmp space (defaults to 50% of RAM) for building large nix things.
|
||||||
# even the stock `nixpkgs.linux` consumes > 16 GB of tmp
|
# even the stock `nixpkgs.linux` consumes > 16 GB of tmp
|
||||||
fileSystems."/tmp".options = [ "size=32G" ];
|
fileSystems."/tmp".options = [ "size=32G" ];
|
||||||
|
@ -81,7 +21,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
# slow, external storage (for archiving, etc)
|
# slow, external storage (for archiving, etc)
|
||||||
fileSystems."/mnt/usb-hdd" = {
|
fileSystems."/mnt/persist/ext" = {
|
||||||
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
|
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
|
||||||
fsType = "btrfs";
|
fsType = "btrfs";
|
||||||
options = [
|
options = [
|
||||||
|
@ -89,47 +29,38 @@
|
||||||
"defaults"
|
"defaults"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
sane.fs."/mnt/usb-hdd".mount = {};
|
|
||||||
|
|
||||||
# FIRST TIME SETUP FOR MEDIA DIRECTORY:
|
sane.persist.stores."ext" = {
|
||||||
# - set the group stick bit: `sudo find /var/media -type d -exec chmod g+s {} +`
|
origin = "/mnt/persist/ext/persist";
|
||||||
# - this ensures new files/dirs inherit the group of their parent dir (instead of the user who creates them)
|
storeDescription = "external HDD storage";
|
||||||
# - 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`
|
sane.fs."/mnt/persist/ext".mount = {};
|
||||||
# - 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 = {};
|
|
||||||
# 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/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/datasets".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 = {};
|
|
||||||
|
|
||||||
# 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.persist.sys.plaintext = [
|
||||||
sane.fs."/var/lib/uninsane/datasets/README.md".file.text = ''
|
# TODO: this is overly broad; only need media and share directories to be persisted
|
||||||
this directory may seem redundant with ../media/datasets. it isn't.
|
{ user = "colin"; group = "users"; path = "/var/lib/uninsane"; }
|
||||||
this directory exists on SSD, allowing for speedy access to specific datasets when necessary.
|
];
|
||||||
the contents should be a subset of what's in ../media/datasets.
|
# make sure large media is stored to the HDD
|
||||||
'';
|
sane.persist.sys.ext = [
|
||||||
|
{
|
||||||
|
user = "colin";
|
||||||
|
group = "users";
|
||||||
|
mode = "0777";
|
||||||
|
path = "/var/lib/uninsane/media/Videos";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
user = "colin";
|
||||||
|
group = "users";
|
||||||
|
mode = "0777";
|
||||||
|
path = "/var/lib/uninsane/media/freeleech";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
user = "colin";
|
||||||
|
group = "users";
|
||||||
|
mode = "0777";
|
||||||
|
path = "/var/lib/uninsane/media/datasets";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
# btrfs doesn't easily support swapfiles
|
# btrfs doesn't easily support swapfiles
|
||||||
# swapDevices = [
|
# swapDevices = [
|
||||||
|
|
|
@ -1,116 +1,220 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
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; {
|
networking.domain = "uninsane.org";
|
||||||
sane.ports.ports = mkOption {
|
|
||||||
# add the `visibleTo.{doof,ovpns}` options
|
sane.ports.openFirewall = true;
|
||||||
type = types.attrsOf portOpts;
|
sane.ports.openUpnp = true;
|
||||||
};
|
|
||||||
|
# view refused packets with: `sudo journalctl -k`
|
||||||
|
# networking.firewall.logRefusedPackets = true;
|
||||||
|
|
||||||
|
# The global useDHCP flag is deprecated, therefore explicitly set to false here.
|
||||||
|
# Per-interface useDHCP will be mandatory in the future, so this generated config
|
||||||
|
# replicates the default behaviour.
|
||||||
|
networking.useDHCP = false;
|
||||||
|
networking.interfaces.eth0.useDHCP = true;
|
||||||
|
# XXX colin: probably don't need this. wlan0 won't be populated unless i touch a value in networking.interfaces.wlan0
|
||||||
|
networking.wireless.enable = false;
|
||||||
|
|
||||||
|
# this is needed to forward packets from the VPN to the host
|
||||||
|
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
||||||
|
|
||||||
|
# unless we add interface-specific settings for each VPN, we have to define nameservers globally.
|
||||||
|
# networking.nameservers = [
|
||||||
|
# "1.1.1.1"
|
||||||
|
# "9.9.9.9"
|
||||||
|
# ];
|
||||||
|
|
||||||
|
# use systemd's stub resolver.
|
||||||
|
# /etc/resolv.conf isn't sophisticated enough to use different servers per net namespace (or link).
|
||||||
|
# instead, running the stub resolver on a known address in the root ns lets us rewrite packets
|
||||||
|
# in the ovnps namespace to use the provider's DNS resolvers.
|
||||||
|
# a weakness is we can only query 1 NS at a time (unless we were to clone the packets?)
|
||||||
|
# there also seems to be some cache somewhere that's shared between the two namespaces.
|
||||||
|
# i think this is a libc thing. might need to leverage proper cgroups to _really_ kill it.
|
||||||
|
# - getent ahostsv4 www.google.com
|
||||||
|
# - try fix: <https://serverfault.com/questions/765989/connect-to-3rd-party-vpn-server-but-dont-use-it-as-the-default-route/766290#766290>
|
||||||
|
services.resolved.enable = true;
|
||||||
|
# without DNSSEC:
|
||||||
|
# - dig matrix.org => works
|
||||||
|
# - curl https://matrix.org => works
|
||||||
|
# with default DNSSEC:
|
||||||
|
# - dig matrix.org => works
|
||||||
|
# - curl https://matrix.org => fails
|
||||||
|
# i don't know why. this might somehow be interfering with the DNS run on this device (trust-dns)
|
||||||
|
services.resolved.dnssec = "false";
|
||||||
|
networking.nameservers = [
|
||||||
|
# use systemd-resolved resolver
|
||||||
|
# full resolver (which understands /etc/hosts) lives on 127.0.0.53
|
||||||
|
# stub resolver (just forwards upstream) lives on 127.0.0.54
|
||||||
|
"127.0.0.53"
|
||||||
|
];
|
||||||
|
|
||||||
|
# nscd -- the Name Service Caching Daemon -- caches DNS query responses
|
||||||
|
# in a way that's unaware of my VPN routing, so routes are frequently poor against
|
||||||
|
# services which advertise different IPs based on geolocation.
|
||||||
|
# nscd claims to be usable without a cache, but in practice i can't get it to not cache!
|
||||||
|
# nsncd is the Name Service NON-Caching Daemon. it's a drop-in that doesn't cache;
|
||||||
|
# this is OK on the host -- because systemd-resolved caches. it's probably sub-optimal
|
||||||
|
# in the netns and we query upstream DNS more often than needed. hm.
|
||||||
|
# TODO: run a separate recursive resolver in each namespace.
|
||||||
|
services.nscd.enableNsncd = true;
|
||||||
|
|
||||||
|
# services.resolved.extraConfig = ''
|
||||||
|
# # docs: `man resolved.conf`
|
||||||
|
# # DNS servers to use via the `wg-ovpns` interface.
|
||||||
|
# # i hope that from the root ns, these aren't visible.
|
||||||
|
# DNS=46.227.67.134%wg-ovpns 192.165.9.158%wg-ovpns
|
||||||
|
# FallbackDNS=1.1.1.1 9.9.9.9
|
||||||
|
# '';
|
||||||
|
|
||||||
|
# OVPN CONFIG (https://www.ovpn.com):
|
||||||
|
# DOCS: https://nixos.wiki/wiki/WireGuard
|
||||||
|
# if you `systemctl restart wireguard-wg-ovpns`, make sure to also restart any other services in `NetworkNamespacePath = .../ovpns`.
|
||||||
|
# TODO: why not create the namespace as a seperate operation (nix config for that?)
|
||||||
|
networking.wireguard.enable = true;
|
||||||
|
networking.wireguard.interfaces.wg-ovpns = let
|
||||||
|
ip = "${pkgs.iproute2}/bin/ip";
|
||||||
|
in-ns = "${ip} netns exec ovpns";
|
||||||
|
iptables = "${pkgs.iptables}/bin/iptables";
|
||||||
|
veth-host-ip = "10.0.1.5";
|
||||||
|
veth-local-ip = "10.0.1.6";
|
||||||
|
vpn-ip = "185.157.162.178";
|
||||||
|
# DNS = 46.227.67.134, 192.165.9.158, 2a07:a880:4601:10f0:cd45::1, 2001:67c:750:1:cafe:cd45::1
|
||||||
|
vpn-dns = "46.227.67.134";
|
||||||
|
in {
|
||||||
|
privateKeyFile = config.sops.secrets.wg_ovpns_privkey.path;
|
||||||
|
# wg is active only in this namespace.
|
||||||
|
# run e.g. ip netns exec ovpns <some command like ping/curl/etc, it'll go through wg>
|
||||||
|
# sudo ip netns exec ovpns ping www.google.com
|
||||||
|
interfaceNamespace = "ovpns";
|
||||||
|
ips = [
|
||||||
|
"185.157.162.178/32"
|
||||||
|
];
|
||||||
|
peers = [
|
||||||
|
{
|
||||||
|
publicKey = "SkkEZDCBde22KTs/Hc7FWvDBfdOCQA4YtBEuC3n5KGs=";
|
||||||
|
endpoint = "185.157.162.10:9930";
|
||||||
|
# alternatively: use hostname, but that presents bootstrapping issues (e.g. if host net flakes)
|
||||||
|
# endpoint = "vpn36.prd.amsterdam.ovpn.com:9930";
|
||||||
|
allowedIPs = [ "0.0.0.0/0" ];
|
||||||
|
# nixOS says this is important for keeping NATs active
|
||||||
|
persistentKeepalive = 25;
|
||||||
|
# re-executes wg this often. docs hint that this might help wg notice DNS/hostname changes.
|
||||||
|
# so, maybe that helps if we specify endpoint as a domain name
|
||||||
|
# dynamicEndpointRefreshSeconds = 30;
|
||||||
|
# when refresh fails, try it again after this period instead.
|
||||||
|
# TODO: not avail until nixpkgs upgrade
|
||||||
|
# dynamicEndpointRefreshRestartSeconds = 5;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
preSetup = "" + ''
|
||||||
|
${ip} netns add ovpns || echo "ovpns already exists"
|
||||||
|
'';
|
||||||
|
postShutdown = "" + ''
|
||||||
|
${in-ns} ip link del ovpns-veth-b || echo "couldn't delete ovpns-veth-b"
|
||||||
|
${ip} link del ovpns-veth-a || echo "couldn't delete ovpns-veth-a"
|
||||||
|
${ip} netns delete ovpns || echo "couldn't delete ovpns"
|
||||||
|
# restore rules/routes
|
||||||
|
${ip} rule del from ${veth-host-ip} lookup ovpns pref 50 || echo "couldn't delete init -> ovpns rule"
|
||||||
|
${ip} route del default via ${veth-local-ip} dev ovpns-veth-a proto kernel src ${veth-host-ip} metric 1002 table ovpns || echo "couldn't delete init -> ovpns route"
|
||||||
|
${ip} rule add from all lookup local pref 0
|
||||||
|
${ip} rule del from all lookup local pref 100
|
||||||
|
'';
|
||||||
|
postSetup = "" + ''
|
||||||
|
# DOCS:
|
||||||
|
# - some of this approach is described here: <https://josephmuia.ca/2018-05-16-net-namespaces-veth-nat/>
|
||||||
|
# - iptables primer: <https://danielmiessler.com/study/iptables/>
|
||||||
|
# create veth pair
|
||||||
|
${ip} link add ovpns-veth-a type veth peer name ovpns-veth-b
|
||||||
|
${ip} addr add ${veth-host-ip}/24 dev ovpns-veth-a
|
||||||
|
${ip} link set ovpns-veth-a up
|
||||||
|
|
||||||
|
# mv veth-b into the ovpns namespace
|
||||||
|
${ip} link set ovpns-veth-b netns ovpns
|
||||||
|
${in-ns} ip addr add ${veth-local-ip}/24 dev ovpns-veth-b
|
||||||
|
${in-ns} ip link set ovpns-veth-b up
|
||||||
|
|
||||||
|
# make it so traffic originating from the host side of the veth
|
||||||
|
# is sent over the veth no matter its destination.
|
||||||
|
${ip} rule add from ${veth-host-ip} lookup ovpns pref 50
|
||||||
|
# for traffic originating at the host veth to the WAN, use the veth as our gateway
|
||||||
|
# not sure if the metric 1002 matters.
|
||||||
|
${ip} route add default via ${veth-local-ip} dev ovpns-veth-a proto kernel src ${veth-host-ip} metric 1002 table ovpns
|
||||||
|
# give the default route lower priority
|
||||||
|
${ip} rule add from all lookup local pref 100
|
||||||
|
${ip} rule del from all lookup local pref 0
|
||||||
|
|
||||||
|
# bridge HTTP traffic:
|
||||||
|
# any external port-80 request sent to the VPN addr will be forwarded to the rootns.
|
||||||
|
# this exists so LetsEncrypt can procure a cert for the MX over http.
|
||||||
|
# TODO: we could use _acme_challence.mx.uninsane.org CNAME to avoid this forwarding
|
||||||
|
# - <https://community.letsencrypt.org/t/where-does-letsencrypt-resolve-dns-from/37607/8>
|
||||||
|
${in-ns} ${iptables} -A PREROUTING -t nat -p tcp --dport 80 -m iprange --dst-range ${vpn-ip} \
|
||||||
|
-j DNAT --to-destination ${veth-host-ip}:80
|
||||||
|
|
||||||
|
# we also bridge DNS traffic
|
||||||
|
${in-ns} ${iptables} -A PREROUTING -t nat -p udp --dport 53 -m iprange --dst-range ${vpn-ip} \
|
||||||
|
-j DNAT --to-destination ${veth-host-ip}
|
||||||
|
${in-ns} ${iptables} -A PREROUTING -t nat -p tcp --dport 53 -m iprange --dst-range ${vpn-ip} \
|
||||||
|
-j DNAT --to-destination ${veth-host-ip}
|
||||||
|
|
||||||
|
# in order to access DNS in this netns, we need to route it to the VPN's nameservers
|
||||||
|
# - alternatively, we could fix DNS servers like 1.1.1.1.
|
||||||
|
${in-ns} ${iptables} -A OUTPUT -t nat -p udp --dport 53 -m iprange --dst-range 127.0.0.53 \
|
||||||
|
-j DNAT --to-destination ${vpn-dns}:53
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
# create a new routing table that we can use to proxy traffic out of the root namespace
|
||||||
networking.domain = "uninsane.org";
|
# through the ovpns namespace, and to the WAN via VPN.
|
||||||
|
networking.iproute2.rttablesExtraConfig = ''
|
||||||
|
5 ovpns
|
||||||
|
'';
|
||||||
|
networking.iproute2.enable = true;
|
||||||
|
|
||||||
sane.ports.openFirewall = true;
|
|
||||||
sane.ports.openUpnp = true;
|
|
||||||
|
|
||||||
# unless we add interface-specific settings for each VPN, we have to define nameservers globally.
|
# HURRICANE ELECTRIC CONFIG:
|
||||||
# networking.nameservers = [
|
# networking.sits = {
|
||||||
# "1.1.1.1"
|
# hurricane = {
|
||||||
# "9.9.9.9"
|
# remote = "216.218.226.238";
|
||||||
# ];
|
# local = "192.168.0.5";
|
||||||
|
# # local = "10.0.0.5";
|
||||||
|
# # remote = "10.0.0.1";
|
||||||
|
# # local = "10.0.0.22";
|
||||||
|
# dev = "eth0";
|
||||||
|
# ttl = 255;
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# networking.interfaces."hurricane".ipv6 = {
|
||||||
|
# addresses = [
|
||||||
|
# # mx.uninsane.org (publically routed /64)
|
||||||
|
# {
|
||||||
|
# address = "2001:470:b:465::1";
|
||||||
|
# prefixLength = 128;
|
||||||
|
# }
|
||||||
|
# # client addr
|
||||||
|
# # {
|
||||||
|
# # address = "2001:470:a:466::2";
|
||||||
|
# # prefixLength = 64;
|
||||||
|
# # }
|
||||||
|
# ];
|
||||||
|
# routes = [
|
||||||
|
# {
|
||||||
|
# address = "::";
|
||||||
|
# prefixLength = 0;
|
||||||
|
# # via = "2001:470:a:466::1";
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
|
||||||
# services.resolved.extraConfig = ''
|
# # after configuration, we want the hurricane device to look like this:
|
||||||
# # docs: `man resolved.conf`
|
# # hurricane: flags=209<UP,POINTOPOINT,RUNNING,NOARP> mtu 1480
|
||||||
# # DNS servers to use via the `wg-ovpns` interface.
|
# # inet6 2001:470:a:450::2 prefixlen 64 scopeid 0x0<global>
|
||||||
# # i hope that from the root ns, these aren't visible.
|
# # inet6 fe80::c0a8:16 prefixlen 64 scopeid 0x20<link>
|
||||||
# DNS=46.227.67.134%wg-ovpns 192.165.9.158%wg-ovpns
|
# # sit txqueuelen 1000 (IPv6-in-IPv4)
|
||||||
# FallbackDNS=1.1.1.1 9.9.9.9
|
# # test with:
|
||||||
# '';
|
# # curl --interface hurricane http://[2607:f8b0:400a:80b::2004]
|
||||||
|
# # ping 2607:f8b0:400a:80b::2004
|
||||||
# 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
|
|
||||||
networking.wireguard.interfaces.wg-doof = {
|
|
||||||
privateKeyFile = config.sops.secrets.wg_doof_privkey.path;
|
|
||||||
# wg is active only in this namespace.
|
|
||||||
# run e.g. ip netns exec doof <some command like ping/curl/etc, it'll go through wg>
|
|
||||||
# sudo ip netns exec doof ping www.google.com
|
|
||||||
interfaceNamespace = "doof";
|
|
||||||
ips = [
|
|
||||||
"205.201.63.12"
|
|
||||||
# "2602:fce8:106::51/128" #< TODO: enable IPv6
|
|
||||||
];
|
|
||||||
peers = [
|
|
||||||
{
|
|
||||||
publicKey = "nuESyYEJ3YU0hTZZgAd7iHBz1ytWBVM5PjEL1VEoTkU=";
|
|
||||||
# TODO: configure DNS within the doof ns and use tun-sea.doof.net endpoint
|
|
||||||
# endpoint = "tun-sea.doof.net:53263";
|
|
||||||
endpoint = "205.201.63.44:53263";
|
|
||||||
allowedIPs = [ "0.0.0.0/0" "::/0" ];
|
|
||||||
persistentKeepalive = 25; #< keep the NAT alive
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
sane.netns.doof.hostVethIpv4 = "10.0.2.5";
|
|
||||||
sane.netns.doof.netnsVethIpv4 = "10.0.2.6";
|
|
||||||
sane.netns.doof.netnsPubIpv4 = "205.201.63.12";
|
|
||||||
sane.netns.doof.routeTable = 12;
|
|
||||||
|
|
||||||
# OVPN CONFIG (https://www.ovpn.com):
|
|
||||||
# DOCS: https://nixos.wiki/wiki/WireGuard
|
|
||||||
# if you `systemctl restart wireguard-wg-ovpns`, make sure to also restart any other services in `NetworkNamespacePath = .../ovpns`.
|
|
||||||
# TODO: why not create the namespace as a seperate operation (nix config for that?)
|
|
||||||
networking.wireguard.enable = true;
|
|
||||||
networking.wireguard.interfaces.wg-ovpns = {
|
|
||||||
privateKeyFile = config.sops.secrets.wg_ovpns_privkey.path;
|
|
||||||
# wg is active only in this namespace.
|
|
||||||
# run e.g. ip netns exec ovpns <some command like ping/curl/etc, it'll go through wg>
|
|
||||||
# sudo ip netns exec ovpns ping www.google.com
|
|
||||||
interfaceNamespace = "ovpns";
|
|
||||||
ips = [ "185.157.162.178" ];
|
|
||||||
peers = [
|
|
||||||
{
|
|
||||||
publicKey = "SkkEZDCBde22KTs/Hc7FWvDBfdOCQA4YtBEuC3n5KGs=";
|
|
||||||
endpoint = "185.157.162.10:9930";
|
|
||||||
# alternatively: use hostname, but that presents bootstrapping issues (e.g. if host net flakes)
|
|
||||||
# endpoint = "vpn36.prd.amsterdam.ovpn.com:9930";
|
|
||||||
allowedIPs = [ "0.0.0.0/0" ];
|
|
||||||
# nixOS says this is important for keeping NATs active
|
|
||||||
persistentKeepalive = 25;
|
|
||||||
# re-executes wg this often. docs hint that this might help wg notice DNS/hostname changes.
|
|
||||||
# so, maybe that helps if we specify endpoint as a domain name
|
|
||||||
# dynamicEndpointRefreshSeconds = 30;
|
|
||||||
# when refresh fails, try it again after this period instead.
|
|
||||||
# TODO: not avail until nixpkgs upgrade
|
|
||||||
# dynamicEndpointRefreshRestartSeconds = 5;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
sane.netns.ovpns.hostVethIpv4 = "10.0.1.5";
|
|
||||||
sane.netns.ovpns.netnsVethIpv4 = "10.0.1.6";
|
|
||||||
sane.netns.ovpns.netnsPubIpv4 = "185.157.162.178";
|
|
||||||
sane.netns.ovpns.routeTable = 11;
|
|
||||||
sane.netns.ovpns.dns = "46.227.67.134"; #< DNS requests inside the namespace are forwarded here
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ in
|
||||||
# > AttributeError: 'NoneType' object has no attribute 'query'
|
# > AttributeError: 'NoneType' object has no attribute 'query'
|
||||||
lib.mkIf false
|
lib.mkIf false
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ inherit user group; mode = "0700"; path = svc-dir; method = "bind"; }
|
{ inherit user group; mode = "0700"; path = svc-dir; }
|
||||||
];
|
];
|
||||||
|
|
||||||
services.calibre-web.enable = true;
|
services.calibre-web.enable = true;
|
||||||
|
@ -24,7 +24,7 @@ lib.mkIf false
|
||||||
# services.calibre-web.options.calibreLibrary = svc-dir;
|
# services.calibre-web.options.calibreLibrary = svc-dir;
|
||||||
|
|
||||||
services.nginx.virtualHosts."calibre.uninsane.org" = {
|
services.nginx.virtualHosts."calibre.uninsane.org" = {
|
||||||
forceSSL = true;
|
addSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://${ip}:${builtins.toString port}";
|
proxyPass = "http://${ip}:${builtins.toString port}";
|
||||||
|
|
|
@ -1,145 +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";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.derived-secrets."/var/lib/coturn/shared_secret.bin" = {
|
|
||||||
encoding = "base64";
|
|
||||||
# TODO: make this not globally readable
|
|
||||||
acl.mode = "0644";
|
|
||||||
};
|
|
||||||
sane.fs."/var/lib/coturn/shared_secret.bin".wantedBeforeBy = [ "coturn.service" ];
|
|
||||||
|
|
||||||
# provide access to certs
|
|
||||||
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";
|
|
||||||
|
|
||||||
#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 = "/var/lib/coturn/shared_secret.bin";
|
|
||||||
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.hostVethIpv4}" "external-ip=${config.sane.netns.ovpns.netnsPubIpv4}" #< 2024/04/25: works, if running in root namespace
|
|
||||||
"listening-ip=${config.sane.netns.ovpns.netnsPubIpv4}" "external-ip=${config.sane.netns.ovpns.netnsPubIpv4}"
|
|
||||||
|
|
||||||
# old attempts:
|
|
||||||
# "external-ip=${config.sane.netns.ovpns.netnsPubIpv4}/${config.sane.netns.ovpns.hostVethIpv4}"
|
|
||||||
# "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,83 +0,0 @@
|
||||||
# as of 2023/12/02: complete blockchain is 530 GiB (on-disk size may be larger)
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
# wrapper to run bitcoind with the tor onion address as externalip (computed at runtime)
|
|
||||||
_bitcoindWithExternalIp = with pkgs; writeShellScriptBin "bitcoind" ''
|
|
||||||
externalip="$(cat /var/lib/tor/onion/bitcoind/hostname)"
|
|
||||||
exec ${bitcoind}/bin/bitcoind "-externalip=$externalip" "$@"
|
|
||||||
'';
|
|
||||||
# the package i provide to services.bitcoind ends up on system PATH, and used by other tools like clightning.
|
|
||||||
# therefore, even though services.bitcoind only needs `bitcoind` binary, provide all the other bitcoin-related binaries (notably `bitcoin-cli`) as well:
|
|
||||||
bitcoindWithExternalIp = with 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 = ''
|
|
||||||
# don't load the wallet, and disable wallet RPC calls
|
|
||||||
disablewallet=1
|
|
||||||
# proxy all outbound traffic through Tor
|
|
||||||
proxy=127.0.0.1:9050
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.bitcoind-mainnet.extraGroups = [ "tor" ];
|
|
||||||
|
|
||||||
systemd.services.bitcoind-mainnet.serviceConfig.RestartSec = "30s"; #< default is 0
|
|
||||||
|
|
||||||
sane.users.colin.fs.".bitcoin/bitcoin.conf" = sane-lib.fs.wantedSymlinkTo config.sops.secrets."bitcoin.conf".path;
|
|
||||||
sops.secrets."bitcoin.conf" = {
|
|
||||||
mode = "0600";
|
|
||||||
owner = "colin";
|
|
||||||
group = "users";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.programs.bitcoind.enableFor.user.colin = true; # for debugging/administration: `bitcoin-cli`
|
|
||||||
}
|
|
|
@ -1,782 +0,0 @@
|
||||||
#!/usr/bin/env nix-shell
|
|
||||||
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.pyln-client ])"
|
|
||||||
|
|
||||||
"""
|
|
||||||
clightning-sane: helper to perform common Lightning node admin operations:
|
|
||||||
- view channel balances
|
|
||||||
- rebalance channels
|
|
||||||
|
|
||||||
COMMON OPERATIONS:
|
|
||||||
- view channel balances: `clightning-sane status`
|
|
||||||
- rebalance channels to improve routability (without paying any fees): `clightning-sane autobalance`
|
|
||||||
|
|
||||||
FULL OPERATION:
|
|
||||||
- `clightning-sane status --full`
|
|
||||||
- `P$`: represents how many msats i've captured in fees from this channel.
|
|
||||||
- `COST`: rough measure of how much it's "costing" me to let my channel partner hold funds on his side of the channel.
|
|
||||||
this is based on the notion that i only capture fees from outbound transactions, and so the channel partner holding all liquidity means i can't capture fees on that liquidity.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pyln-client docs: <https://github.com/ElementsProject/lightning/tree/master/contrib/pyln-client>
|
|
||||||
# terminology:
|
|
||||||
# - "scid": "Short Channel ID", e.g. 123456x7890x0
|
|
||||||
# from this id, we can locate the actual channel, its peers, and its parameters
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from pyln.client import LightningRpc, Millisatoshi, RpcError
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
RPC_FILE = "/var/lib/clightning/bitcoin/lightning-rpc"
|
|
||||||
# CLTV (HLTC delta) of the final hop
|
|
||||||
# set this too low and you might get inadvertent channel closures (?)
|
|
||||||
CLTV = 18
|
|
||||||
|
|
||||||
# for every sequentally failed transaction, delay this much before trying again.
|
|
||||||
# note that the initial route building process can involve 10-20 "transient" failures, as it discovers dead channels.
|
|
||||||
TX_FAIL_BACKOFF = 0.8
|
|
||||||
MAX_SEQUENTIAL_JOB_FAILURES = 200
|
|
||||||
|
|
||||||
class LoopError(Enum):
|
|
||||||
""" error when trying to loop sats, or when unable to calculate a route for the loop """
|
|
||||||
TRANSIENT = "TRANSIENT" # try again, we'll maybe find a different route
|
|
||||||
NO_ROUTE = "NO_ROUTE"
|
|
||||||
|
|
||||||
class RouteError(Enum):
|
|
||||||
""" error when calculated a route """
|
|
||||||
HAS_BASE_FEE = "HAS_BASE_FEE"
|
|
||||||
NO_ROUTE = "NO_ROUTE"
|
|
||||||
|
|
||||||
class Metrics:
|
|
||||||
looped_msat: int = 0
|
|
||||||
sendpay_fail: int = 0
|
|
||||||
sendpay_succeed: int = 0
|
|
||||||
own_bad_channel: int = 0
|
|
||||||
no_route: int = 0
|
|
||||||
in_ch_unsatisfiable: int = 0
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"looped:{self.looped_msat}, tx:{self.sendpay_succeed}, tx_fail:{self.sendpay_fail}, own_bad_ch:{self.own_bad_channel}, no_route:{self.no_route}, in_ch_restricted:{self.in_ch_unsatisfiable}"
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TxBounds:
|
|
||||||
max_msat: int
|
|
||||||
min_msat: int = 0
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"TxBounds({self.min_msat} <= msat <= {self.max_msat})"
|
|
||||||
|
|
||||||
def is_satisfiable(self) -> bool:
|
|
||||||
return self.min_msat <= self.max_msat
|
|
||||||
|
|
||||||
def raise_max_to_be_satisfiable(self) -> "Self":
|
|
||||||
if self.max_msat < self.min_msat:
|
|
||||||
logger.debug(f"raising max_msat to be consistent: {self.max_msat} -> {self.min_msat}")
|
|
||||||
return TxBounds(self.min_msat, self.min_msat)
|
|
||||||
return TxBounds(min_msat=self.min_msat, max_msat=self.max_msat)
|
|
||||||
|
|
||||||
def intersect(self, other: "TxBounds") -> "Self":
|
|
||||||
return TxBounds(
|
|
||||||
min_msat=max(self.min_msat, other.min_msat),
|
|
||||||
max_msat=min(self.max_msat, other.max_msat),
|
|
||||||
)
|
|
||||||
|
|
||||||
def restrict_to_htlc(self, ch: "LocalChannel", why: str = "") -> "Self":
|
|
||||||
"""
|
|
||||||
apply min/max HTLC size restrictions of the given channel.
|
|
||||||
"""
|
|
||||||
if ch:
|
|
||||||
why = why or ch.directed_scid_to_me
|
|
||||||
if why: why = f"{why}: "
|
|
||||||
|
|
||||||
new_min, new_max = self.min_msat, self.max_msat
|
|
||||||
if ch.htlc_minimum_to_me > self.min_msat:
|
|
||||||
new_min = ch.htlc_minimum_to_me
|
|
||||||
logger.debug(f"{why}raising min_msat due to HTLC requirements: {self.min_msat} -> {new_min}")
|
|
||||||
if ch.htlc_maximum_to_me < self.max_msat:
|
|
||||||
new_max = ch.htlc_maximum_to_me
|
|
||||||
logger.debug(f"{why}lowering max_msat due to HTLC requirements: {self.max_msat} -> {new_max}")
|
|
||||||
return TxBounds(min_msat=new_min, max_msat=new_max)
|
|
||||||
|
|
||||||
def restrict_to_zero_fees(self, ch: "LocalChannel"=None, base: int=0, ppm: int=0, why:str = "") -> "Self":
|
|
||||||
"""
|
|
||||||
restrict tx size such that PPM fees are zero.
|
|
||||||
if the channel has a base fee, then `max_msat` is forced to 0.
|
|
||||||
"""
|
|
||||||
if ch:
|
|
||||||
why = why or ch.directed_scid_to_me
|
|
||||||
self = self.restrict_to_zero_fees(base=ch.to_me["base_fee_millisatoshi"], ppm=ch.to_me["fee_per_millionth"], why=why)
|
|
||||||
|
|
||||||
if why: why = f"{why}: "
|
|
||||||
|
|
||||||
new_max = self.max_msat
|
|
||||||
ppm_max = math.ceil(1000000 / ppm) - 1 if ppm != 0 else new_max
|
|
||||||
if ppm_max < new_max:
|
|
||||||
logger.debug(f"{why}decreasing max_msat due to fee ppm: {new_max} -> {ppm_max}")
|
|
||||||
new_max = ppm_max
|
|
||||||
|
|
||||||
if base != 0:
|
|
||||||
logger.debug(f"{why}free route impossible: channel has base fees")
|
|
||||||
new_max = 0
|
|
||||||
|
|
||||||
return TxBounds(min_msat=self.min_msat, max_msat=new_max)
|
|
||||||
|
|
||||||
|
|
||||||
class LocalChannel:
|
|
||||||
def __init__(self, channels: list, rpc: "RpcHelper"):
|
|
||||||
assert 0 < len(channels) <= 2, f"unexpected: channel count: {channels}"
|
|
||||||
out = None
|
|
||||||
in_ = None
|
|
||||||
for c in channels:
|
|
||||||
if c["source"] == rpc.self_id:
|
|
||||||
assert out is None, f"unexpected: multiple channels from self: {channels}"
|
|
||||||
out = c
|
|
||||||
if c["destination"] == rpc.self_id:
|
|
||||||
assert in_ is None, f"unexpected: multiple channels to self: {channels}"
|
|
||||||
in_ = c
|
|
||||||
|
|
||||||
# assert out is not None, f"no channel from self: {channels}"
|
|
||||||
# assert in_ is not None, f"no channel to self: {channels}"
|
|
||||||
|
|
||||||
if out and in_:
|
|
||||||
assert out["destination"] == in_["source"], f"channel peers are asymmetric?! {channels}"
|
|
||||||
assert out["short_channel_id"] == in_["short_channel_id"], f"channel ids differ?! {channels}"
|
|
||||||
|
|
||||||
self.from_me = out
|
|
||||||
self.to_me = in_
|
|
||||||
self.remote_node = rpc.node(self.remote_peer)
|
|
||||||
self.peer_ch = rpc.peerchannel(self.scid, self.remote_peer)
|
|
||||||
self.forwards_from_me = rpc.rpc.listforwards(out_channel=self.scid, status="settled")["forwards"]
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return self.to_str(with_scid=True, with_bal_ratio=True, with_cost=False, with_ppm_theirs=False)
|
|
||||||
|
|
||||||
def to_str(
|
|
||||||
self,
|
|
||||||
with_peer_id:bool = False,
|
|
||||||
with_scid:bool = False,
|
|
||||||
with_bal_msat:bool = False,
|
|
||||||
with_bal_ratio:bool = False,
|
|
||||||
with_cost:bool = False,
|
|
||||||
with_ppm_theirs:bool = False,
|
|
||||||
with_ppm_mine:bool = False,
|
|
||||||
with_profits:bool = True,
|
|
||||||
with_payments:bool = False,
|
|
||||||
) -> str:
|
|
||||||
base_flag = "*" if not self.online or self.base_fee_to_me != 0 else ""
|
|
||||||
alias = f"({self.remote_alias}){base_flag}"
|
|
||||||
peerid = f" {self.remote_peer}" if with_peer_id else ""
|
|
||||||
scid = f" scid:{self.scid:>13}" if with_scid else ""
|
|
||||||
bal = f" S:{int(self.sendable):11}/R:{int(self.receivable):11}" if with_bal_msat else ""
|
|
||||||
ratio = f" MINE:{(100*self.send_ratio):>8.4f}%" if with_bal_ratio else ""
|
|
||||||
payments = f" OUT:{int(self.out_fulfilled_msat):>11}/IN:{int(self.in_fulfilled_msat):>11}" if with_payments else ""
|
|
||||||
profits = f" P$:{int(self.fees_lifetime_mine):>8}" if with_profits else ""
|
|
||||||
cost = f" COST:{self.opportunity_cost_lent:>8}" if with_cost else ""
|
|
||||||
ppm_theirs = self.ppm_to_me if self.to_me else "N/A"
|
|
||||||
ppm_theirs = f" PPM_THEIRS:{ppm_theirs:>6}" if with_ppm_theirs else ""
|
|
||||||
ppm_mine = self.ppm_from_me if self.from_me else "N/A"
|
|
||||||
ppm_mine = f" PPM_MINE:{ppm_mine:>6}" if with_ppm_mine else ""
|
|
||||||
return f"channel{alias:30}{peerid}{scid}{bal}{ratio}{payments}{profits}{cost}{ppm_theirs}{ppm_mine}"
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def online(self) -> bool:
|
|
||||||
return self.from_me and self.to_me
|
|
||||||
|
|
||||||
@property
|
|
||||||
def remote_peer(self) -> str:
|
|
||||||
if self.from_me:
|
|
||||||
return self.from_me["destination"]
|
|
||||||
else:
|
|
||||||
return self.to_me["source"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def remote_alias(self) -> str:
|
|
||||||
return self.remote_node["alias"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scid(self) -> str:
|
|
||||||
if self.from_me:
|
|
||||||
return self.from_me["short_channel_id"]
|
|
||||||
else:
|
|
||||||
return self.to_me["short_channel_id"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def htlc_minimum_to_me(self) -> Millisatoshi:
|
|
||||||
return self.to_me["htlc_minimum_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def htlc_minimum_from_me(self) -> Millisatoshi:
|
|
||||||
return self.from_me["htlc_minimum_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def htlc_minimum(self) -> Millisatoshi:
|
|
||||||
return max(self.htlc_minimum_to_me, self.htlc_minimum_from_me)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def htlc_maximum_to_me(self) -> Millisatoshi:
|
|
||||||
return self.to_me["htlc_maximum_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def htlc_maximum_from_me(self) -> Millisatoshi:
|
|
||||||
return self.from_me["htlc_maximum_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def htlc_maximum(self) -> Millisatoshi:
|
|
||||||
return min(self.htlc_maximum_to_me, self.htlc_maximum_from_me)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def direction_to_me(self) -> int:
|
|
||||||
return self.to_me["direction"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def direction_from_me(self) -> int:
|
|
||||||
return self.from_me["direction"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def directed_scid_to_me(self) -> str:
|
|
||||||
return f"{self.scid}/{self.direction_to_me}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def directed_scid_from_me(self) -> str:
|
|
||||||
return f"{self.scid}/{self.direction_from_me}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def delay_them(self) -> str:
|
|
||||||
return self.to_me["delay"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def delay_me(self) -> str:
|
|
||||||
return self.from_me["delay"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ppm_to_me(self) -> int:
|
|
||||||
return self.to_me["fee_per_millionth"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ppm_from_me(self) -> int:
|
|
||||||
return self.from_me["fee_per_millionth"]
|
|
||||||
# return self.peer_ch["fee_proportional_millionths"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def base_fee_to_me(self) -> int:
|
|
||||||
return self.to_me["base_fee_millisatoshi"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def receivable(self) -> int:
|
|
||||||
return self.peer_ch["receivable_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sendable(self) -> int:
|
|
||||||
return self.peer_ch["spendable_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def in_fulfilled_msat(self) -> Millisatoshi:
|
|
||||||
return self.peer_ch["in_fulfilled_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def out_fulfilled_msat(self) -> Millisatoshi:
|
|
||||||
return self.peer_ch["out_fulfilled_msat"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fees_lifetime_mine(self) -> Millisatoshi:
|
|
||||||
return sum(fwd["fee_msat"] for fwd in self.forwards_from_me)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def send_ratio(self) -> float:
|
|
||||||
cap = self.receivable + self.sendable
|
|
||||||
return self.sendable / cap
|
|
||||||
|
|
||||||
@property
|
|
||||||
def opportunity_cost_lent(self) -> int:
|
|
||||||
""" how much msat did we gain by pushing their channel to its current balance? """
|
|
||||||
return int(self.receivable * self.ppm_from_me / 1000000)
|
|
||||||
|
|
||||||
class RpcHelper:
|
|
||||||
def __init__(self, rpc: LightningRpc):
|
|
||||||
self.rpc = rpc
|
|
||||||
self.self_id = rpc.getinfo()["id"]
|
|
||||||
|
|
||||||
def localchannel(self, scid: str) -> LocalChannel:
|
|
||||||
listchan = self.rpc.listchannels(scid)
|
|
||||||
# this assertion would probably indicate a typo in the scid
|
|
||||||
assert listchan and listchan.get("channels", []) != [], f"bad listchannels for {scid}: {listchan}"
|
|
||||||
return LocalChannel(listchan["channels"], self)
|
|
||||||
|
|
||||||
def node(self, id: str) -> dict:
|
|
||||||
nodes = self.rpc.listnodes(id)["nodes"]
|
|
||||||
assert len(nodes) == 1, f"unexpected: multiple nodes for {id}: {nodes}"
|
|
||||||
return nodes[0]
|
|
||||||
|
|
||||||
def peerchannel(self, scid: str, peer_id: str) -> dict:
|
|
||||||
peerchannels = self.rpc.listpeerchannels(peer_id)["channels"]
|
|
||||||
channels = [c for c in peerchannels if c["short_channel_id"] == scid]
|
|
||||||
assert len(channels) == 1, f"expected exactly 1 channel, got: {channels}"
|
|
||||||
return channels[0]
|
|
||||||
|
|
||||||
def try_getroute(self, *args, **kwargs) -> dict | None:
|
|
||||||
""" wrapper for getroute which returns None instead of error if no route exists """
|
|
||||||
try:
|
|
||||||
route = self.rpc.getroute(*args, **kwargs)
|
|
||||||
except RpcError as e:
|
|
||||||
logger.debug(f"rpc failed: {e}")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
route = route["route"]
|
|
||||||
if route == []: return None
|
|
||||||
return route
|
|
||||||
|
|
||||||
class LoopRouter:
|
|
||||||
def __init__(self, rpc: RpcHelper, metrics: Metrics = None):
|
|
||||||
self.rpc = rpc
|
|
||||||
self.metrics = metrics or Metrics()
|
|
||||||
self.bad_channels = [] # list of directed scid
|
|
||||||
self.nonzero_base_channels = [] # list of directed scid
|
|
||||||
|
|
||||||
def drop_caches(self) -> None:
|
|
||||||
logger.info("LoopRouter.drop_caches()")
|
|
||||||
self.bad_channels = []
|
|
||||||
|
|
||||||
def _get_directed_scid(self, scid: str, direction: int) -> dict:
|
|
||||||
channels = self.rpc.rpc.listchannels(scid)["channels"]
|
|
||||||
channels = [c for c in channels if c["direction"] == direction]
|
|
||||||
assert len(channels) == 1, f"expected exactly 1 channel: {channels}"
|
|
||||||
return channels[0]
|
|
||||||
|
|
||||||
def loop_once(self, out_scid: str, in_scid: str, bounds: TxBounds) -> LoopError|int:
|
|
||||||
out_ch = self.rpc.localchannel(out_scid)
|
|
||||||
in_ch = self.rpc.localchannel(in_scid)
|
|
||||||
|
|
||||||
if out_ch.directed_scid_from_me in self.bad_channels or in_ch.directed_scid_to_me in self.bad_channels:
|
|
||||||
logger.info(f"loop {out_scid} -> {in_scid} failed in our own channel")
|
|
||||||
self.metrics.own_bad_channel += 1
|
|
||||||
return LoopError.TRANSIENT
|
|
||||||
|
|
||||||
# bounds = bounds.restrict_to_htlc(out_ch) # htlc bounds seem to be enforced only in the outward direction
|
|
||||||
bounds = bounds.restrict_to_htlc(in_ch)
|
|
||||||
bounds = bounds.restrict_to_zero_fees(in_ch)
|
|
||||||
if not bounds.is_satisfiable():
|
|
||||||
self.metrics.in_ch_unsatisfiable += 1
|
|
||||||
return LoopError.NO_ROUTE
|
|
||||||
|
|
||||||
logger.debug(f"route with bounds {bounds}")
|
|
||||||
route = self.route(out_ch, in_ch, bounds)
|
|
||||||
logger.debug(f"route: {route}")
|
|
||||||
if route == RouteError.NO_ROUTE:
|
|
||||||
self.metrics.no_route += 1
|
|
||||||
return LoopError.NO_ROUTE
|
|
||||||
elif route == RouteError.HAS_BASE_FEE:
|
|
||||||
# try again with a different route
|
|
||||||
return LoopError.TRANSIENT
|
|
||||||
|
|
||||||
amount_msat = route[0]["amount_msat"]
|
|
||||||
invoice_id = f"loop-{time.time():.6f}".replace(".", "_")
|
|
||||||
invoice_desc = f"bal {out_scid}:{in_scid}"
|
|
||||||
invoice = self.rpc.rpc.invoice("any", invoice_id, invoice_desc)
|
|
||||||
logger.debug(f"invoice: {invoice}")
|
|
||||||
|
|
||||||
payment = self.rpc.rpc.sendpay(route, invoice["payment_hash"], invoice_id, amount_msat, invoice["bolt11"], invoice["payment_secret"])
|
|
||||||
logger.debug(f"sent: {payment}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
wait = self.rpc.rpc.waitsendpay(invoice["payment_hash"])
|
|
||||||
logger.debug(f"result: {wait}")
|
|
||||||
except RpcError as e:
|
|
||||||
self.metrics.sendpay_fail += 1
|
|
||||||
err_data = e.error["data"]
|
|
||||||
err_scid, err_dir = err_data["erring_channel"], err_data["erring_direction"]
|
|
||||||
err_directed_scid = f"{err_scid}/{err_dir}"
|
|
||||||
logger.debug(f"ch failed, adding to excludes: {err_directed_scid}; {e.error}")
|
|
||||||
self.bad_channels.append(err_directed_scid)
|
|
||||||
return LoopError.TRANSIENT
|
|
||||||
else:
|
|
||||||
self.metrics.sendpay_succeed += 1
|
|
||||||
self.metrics.looped_msat += int(amount_msat)
|
|
||||||
return int(amount_msat)
|
|
||||||
|
|
||||||
def route(self, out_ch: LocalChannel, in_ch: LocalChannel, bounds: TxBounds) -> list[dict] | RouteError:
|
|
||||||
exclude = [
|
|
||||||
# ensure the payment doesn't cross either channel in reverse.
|
|
||||||
# note that this doesn't preclude it from taking additional trips through self, with other peers.
|
|
||||||
# out_ch.directed_scid_to_me,
|
|
||||||
# in_ch.directed_scid_from_me,
|
|
||||||
|
|
||||||
# alternatively, never route through self. this avoids a class of logic error, like what to do with fees i charge "myself".
|
|
||||||
self.rpc.self_id
|
|
||||||
] + self.bad_channels + self.nonzero_base_channels
|
|
||||||
|
|
||||||
out_peer = out_ch.remote_peer
|
|
||||||
in_peer = in_ch.remote_peer
|
|
||||||
|
|
||||||
route_or_bounds = bounds
|
|
||||||
while isinstance(route_or_bounds, TxBounds):
|
|
||||||
old_bounds = route_or_bounds
|
|
||||||
route_or_bounds = self._find_partial_route(out_peer, in_peer, old_bounds, exclude=exclude)
|
|
||||||
if route_or_bounds == old_bounds:
|
|
||||||
return RouteError.NO_ROUTE
|
|
||||||
|
|
||||||
if isinstance(route_or_bounds, RouteError):
|
|
||||||
return route_or_bounds
|
|
||||||
|
|
||||||
route = self._add_route_endpoints(route_or_bounds, out_ch, in_ch)
|
|
||||||
return route
|
|
||||||
|
|
||||||
def _find_partial_route(self, out_peer: str, in_peer: str, bounds: TxBounds, exclude: list[str]=[]) -> list[dict] | RouteError | TxBounds:
|
|
||||||
route = self.rpc.try_getroute(in_peer, amount_msat=bounds.max_msat, riskfactor=0, fromid=out_peer, exclude=exclude, cltv=CLTV)
|
|
||||||
if route is None:
|
|
||||||
logger.debug(f"no route for {bounds.max_msat}msat {out_peer} -> {in_peer}")
|
|
||||||
return RouteError.NO_ROUTE
|
|
||||||
|
|
||||||
send_msat = route[0]["amount_msat"]
|
|
||||||
if send_msat != Millisatoshi(bounds.max_msat):
|
|
||||||
logger.debug(f"found route with non-zero fee: {send_msat} -> {bounds.max_msat}. {route}")
|
|
||||||
|
|
||||||
error = None
|
|
||||||
for hop in route:
|
|
||||||
hop_scid = hop["channel"]
|
|
||||||
hop_dir = hop["direction"]
|
|
||||||
directed_scid = f"{hop_scid}/{hop_dir}"
|
|
||||||
ch = self._get_directed_scid(hop_scid, hop_dir)
|
|
||||||
if ch["base_fee_millisatoshi"] != 0:
|
|
||||||
self.nonzero_base_channels.append(directed_scid)
|
|
||||||
error = RouteError.HAS_BASE_FEE
|
|
||||||
bounds = bounds.restrict_to_zero_fees(ppm=ch["fee_per_millionth"], why=directed_scid)
|
|
||||||
|
|
||||||
return bounds.raise_max_to_be_satisfiable() if error is None else error
|
|
||||||
|
|
||||||
return route
|
|
||||||
|
|
||||||
def _add_route_endpoints(self, route, out_ch: LocalChannel, in_ch: LocalChannel):
|
|
||||||
inbound_hop = dict(
|
|
||||||
id=self.rpc.self_id,
|
|
||||||
channel=in_ch.scid,
|
|
||||||
direction=in_ch.direction_to_me,
|
|
||||||
amount_msat=route[-1]["amount_msat"],
|
|
||||||
delay=route[-1]["delay"],
|
|
||||||
style="tlv",
|
|
||||||
)
|
|
||||||
route = self._add_route_delay(route, in_ch.delay_them) + [ inbound_hop ]
|
|
||||||
|
|
||||||
outbound_hop = dict(
|
|
||||||
id=out_ch.remote_peer,
|
|
||||||
channel=out_ch.scid,
|
|
||||||
direction=out_ch.direction_from_me,
|
|
||||||
amount_msat=route[0]["amount_msat"],
|
|
||||||
delay=route[0]["delay"] + out_ch.delay_them,
|
|
||||||
style="tlv",
|
|
||||||
)
|
|
||||||
route = [ outbound_hop ] + route
|
|
||||||
return route
|
|
||||||
|
|
||||||
def _add_route_delay(self, route: list[dict], delay: int) -> list[dict]:
|
|
||||||
return [ dict(hop, delay=hop["delay"] + delay) for hop in route ]
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LoopJob:
|
|
||||||
out: str # scid
|
|
||||||
in_: str # scid
|
|
||||||
amount: int
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LoopJobIdle:
|
|
||||||
sec: int = 10
|
|
||||||
|
|
||||||
class LoopJobDone(Enum):
|
|
||||||
COMPLETED = "COMPLETED"
|
|
||||||
ABORTED = "ABORTED"
|
|
||||||
|
|
||||||
class AbstractLoopRunner:
|
|
||||||
def __init__(self, looper: LoopRouter, bounds: TxBounds, parallelism: int):
|
|
||||||
self.looper = looper
|
|
||||||
self.bounds = bounds
|
|
||||||
self.parallelism = parallelism
|
|
||||||
self.bounds_map = {} # map (out:str, in_:str) -> TxBounds. it's a cache so we don't have to try 10 routes every time.
|
|
||||||
|
|
||||||
def pop_job(self) -> LoopJob | LoopJobIdle | LoopJobDone:
|
|
||||||
raise NotImplemented # abstract method
|
|
||||||
|
|
||||||
def finished_job(self, job: LoopJob, progress: int|LoopError) -> None:
|
|
||||||
raise NotImplemented # abstract method
|
|
||||||
|
|
||||||
def run_to_completion(self, exit_on_any_completed:bool = False) -> None:
|
|
||||||
self.exiting = False
|
|
||||||
self.exit_on_any_completed = exit_on_any_completed
|
|
||||||
if self.parallelism == 1:
|
|
||||||
# run inline to aid debugging
|
|
||||||
self._worker_thread()
|
|
||||||
else:
|
|
||||||
with ThreadPoolExecutor(max_workers=self.parallelism) as executor:
|
|
||||||
_ = list(executor.map(lambda _i: self._try_invoke(self._worker_thread), range(self.parallelism)))
|
|
||||||
|
|
||||||
def drop_caches(self) -> None:
|
|
||||||
logger.info("AbstractLoopRunner.drop_caches()")
|
|
||||||
self.looper.drop_caches()
|
|
||||||
self.bounds_map = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _try_invoke(self, f, *args) -> None:
|
|
||||||
"""
|
|
||||||
try to invoke `f` with the provided `args`, and log if it fails.
|
|
||||||
this overcomes the issue that background tasks which fail via Exception otherwise do so silently.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
f(*args)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"task failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _worker_thread(self) -> None:
|
|
||||||
while not self.exiting:
|
|
||||||
job = self.pop_job()
|
|
||||||
logger.debug(f"popped job: {job}")
|
|
||||||
if isinstance(job, LoopJobDone):
|
|
||||||
return self._worker_finished(job)
|
|
||||||
|
|
||||||
if isinstance(job, LoopJobIdle):
|
|
||||||
logger.debug(f"idling for {job.sec}")
|
|
||||||
time.sleep(job.sec)
|
|
||||||
continue
|
|
||||||
|
|
||||||
result = self._execute_job(job)
|
|
||||||
logger.debug(f"finishing job {job} with {result}")
|
|
||||||
self.finished_job(job, result)
|
|
||||||
|
|
||||||
def _execute_job(self, job: LoopJob) -> LoopError|int:
|
|
||||||
bounds = self.bounds_map.get((job.out, job.in_), self.bounds)
|
|
||||||
bounds = bounds.intersect(TxBounds(max_msat=job.amount))
|
|
||||||
if not bounds.is_satisfiable():
|
|
||||||
logger.debug(f"TxBounds for job are unsatisfiable; skipping: {bounds} {job}")
|
|
||||||
return LoopError.NO_ROUTE
|
|
||||||
|
|
||||||
amt_looped = self.looper.loop_once(job.out, job.in_, bounds)
|
|
||||||
if amt_looped in (0, LoopError.NO_ROUTE, LoopError.TRANSIENT):
|
|
||||||
return amt_looped
|
|
||||||
|
|
||||||
logger.info(f"looped {amt_looped} from {job.out} -> {job.in_}")
|
|
||||||
bounds = bounds.intersect(TxBounds(max_msat=amt_looped))
|
|
||||||
|
|
||||||
self.bounds_map[(job.out, job.in_)] = bounds
|
|
||||||
return amt_looped
|
|
||||||
|
|
||||||
def _worker_finished(self, job: LoopJobDone) -> None:
|
|
||||||
if job == LoopJobDone.COMPLETED and self.exit_on_any_completed:
|
|
||||||
logger.debug(f"worker completed -> exiting pool")
|
|
||||||
self.exiting = True
|
|
||||||
|
|
||||||
class LoopPairState:
|
|
||||||
# TODO: use this in MultiLoopBalancer, or stop shoving state in here and put it on LoopBalancer instead.
|
|
||||||
def __init__(self, out: str, in_: str, amount: int):
|
|
||||||
self.out = out
|
|
||||||
self.in_ = in_
|
|
||||||
self.amount_target = amount
|
|
||||||
self.amount_looped = 0
|
|
||||||
self.amount_outstanding = 0
|
|
||||||
self.tx_fail_count = 0
|
|
||||||
self.route_fail_count = 0
|
|
||||||
self.last_job_start_time = None
|
|
||||||
self.failed_tx_throttler = 0 # increase by one every time we fail, decreases more gradually, when we succeed
|
|
||||||
|
|
||||||
class LoopBalancer(AbstractLoopRunner):
|
|
||||||
def __init__(self, out: str, in_: str, amount: int, looper: LoopRouter, bounds: TxBounds, parallelism: int=1):
|
|
||||||
super().__init__(looper, bounds, parallelism)
|
|
||||||
self.state = LoopPairState(out, in_, amount)
|
|
||||||
|
|
||||||
def pop_job(self) -> LoopJob | LoopJobIdle | LoopJobDone:
|
|
||||||
if self.state.tx_fail_count + 10*self.state.route_fail_count >= MAX_SEQUENTIAL_JOB_FAILURES:
|
|
||||||
logger.info(f"giving up ({self.state.out} -> {self.state.in_}): {self.state.tx_fail_count} tx failures, {self.state.route_fail_count} route failures")
|
|
||||||
return LoopJobDone.ABORTED
|
|
||||||
|
|
||||||
if self.state.tx_fail_count + self.state.route_fail_count > 0:
|
|
||||||
# N.B.: last_job_start_time is guaranteed to have been set by now
|
|
||||||
idle_until = self.state.last_job_start_time + TX_FAIL_BACKOFF*self.state.failed_tx_throttler
|
|
||||||
idle_for = idle_until - time.time()
|
|
||||||
if self.state.amount_outstanding != 0 or idle_for > 0:
|
|
||||||
# when we hit transient failures, restrict to just one job in flight at a time.
|
|
||||||
# this is aimed for the initial route building, where multiple jobs in flight is just useless,
|
|
||||||
# but it's not a bad idea for network blips, etc, either.
|
|
||||||
logger.info(f"throttling ({self.state.out} -> {self.state.in_}) for {idle_for:.0f}: {self.state.tx_fail_count} tx failures, {self.state.route_fail_count} route failures")
|
|
||||||
return LoopJobIdle(idle_for) if idle_for > 0 else LoopJobIdle()
|
|
||||||
|
|
||||||
amount_avail = self.state.amount_target - self.state.amount_looped - self.state.amount_outstanding
|
|
||||||
if amount_avail < self.bounds.min_msat:
|
|
||||||
if self.state.amount_outstanding == 0: return LoopJobDone.COMPLETED
|
|
||||||
return LoopJobIdle() # sending out another job would risk over-transferring
|
|
||||||
amount_this_job = min(amount_avail, self.bounds.max_msat)
|
|
||||||
|
|
||||||
self.state.amount_outstanding += amount_this_job
|
|
||||||
self.state.last_job_start_time = time.time()
|
|
||||||
return LoopJob(out=self.state.out, in_=self.state.in_, amount=amount_this_job)
|
|
||||||
|
|
||||||
def finished_job(self, job: LoopJob, progress: int) -> None:
|
|
||||||
self.state.amount_outstanding -= job.amount
|
|
||||||
if progress == LoopError.NO_ROUTE:
|
|
||||||
self.state.route_fail_count += 1
|
|
||||||
self.state.failed_tx_throttler += 10
|
|
||||||
elif progress == LoopError.TRANSIENT:
|
|
||||||
self.state.tx_fail_count += 1
|
|
||||||
self.state.failed_tx_throttler += 1
|
|
||||||
else:
|
|
||||||
self.state.amount_looped += progress
|
|
||||||
self.state.tx_fail_count = 0
|
|
||||||
self.state.route_fail_count = 0
|
|
||||||
self.state.failed_tx_throttler = max(0, self.state.failed_tx_throttler - 0.2)
|
|
||||||
logger.info(f"loop progressed ({job.out} -> {job.in_}) {progress}: {self.state.amount_looped} of {self.state.amount_target}")
|
|
||||||
|
|
||||||
class MultiLoopBalancer(AbstractLoopRunner):
|
|
||||||
"""
|
|
||||||
multiplexes jobs between multiple LoopBalancers.
|
|
||||||
note that the child LoopBalancers don't actually execute the jobs -- just produce them.
|
|
||||||
"""
|
|
||||||
def __init__(self, looper: LoopRouter, bounds: TxBounds, parallelism: int=1):
|
|
||||||
super().__init__(looper, bounds, parallelism)
|
|
||||||
self.loops = []
|
|
||||||
# job_index: increments on every job so we can grab jobs evenly from each LoopBalancer.
|
|
||||||
# in that event that producers are idling, it can actually increment more than once,
|
|
||||||
# so don't take this too literally
|
|
||||||
self.job_index = 0
|
|
||||||
|
|
||||||
def add_loop(self, out: LocalChannel, in_: LocalChannel, amount: int) -> None:
|
|
||||||
"""
|
|
||||||
start looping sats from out -> in_
|
|
||||||
"""
|
|
||||||
assert not any(l.state.out == out.scid and l.state.in_ == in_.scid for l in self.loops), f"tried to add duplicate loops from {out} -> {in_}"
|
|
||||||
logger.info(f"looping from ({out}) to ({in_})")
|
|
||||||
self.loops.append(LoopBalancer(out.scid, in_.scid, amount, self.looper, self.bounds, self.parallelism))
|
|
||||||
|
|
||||||
def pop_job(self) -> LoopJob | LoopJobIdle | LoopJobDone:
|
|
||||||
# N.B.: this can be called in parallel, so try to be consistent enough to not crash
|
|
||||||
|
|
||||||
idle_job = None
|
|
||||||
abort_job = None
|
|
||||||
for i, _ in enumerate(self.loops):
|
|
||||||
loop = self.loops[(self.job_index + i) % len(self.loops)]
|
|
||||||
self.job_index += 1
|
|
||||||
job = loop.pop_job()
|
|
||||||
if isinstance(job, LoopJob):
|
|
||||||
return job
|
|
||||||
if isinstance(job, LoopJobIdle):
|
|
||||||
idle_job = LoopJobIdle(min(job.sec, idle_job.sec)) if idle_job is not None else job
|
|
||||||
if job == LoopJobDone.ABORTED:
|
|
||||||
abort_job = job
|
|
||||||
|
|
||||||
# either there's a task to idle, or we have to terminate.
|
|
||||||
# if terminating, terminate ABORTED if any job aborted, else COMPLETED
|
|
||||||
if idle_job is not None: return idle_job
|
|
||||||
if abort_job is not None: return abort_job
|
|
||||||
return LoopJobDone.COMPLETED
|
|
||||||
|
|
||||||
def finished_job(self, job: LoopJob, progress: int) -> None:
|
|
||||||
# this assumes (enforced externally) that we have only one loop for a given out/in_ pair
|
|
||||||
for l in self.loops:
|
|
||||||
if l.state.out == job.out and l.state.in_ == job.in_:
|
|
||||||
l.finished_job(job, progress)
|
|
||||||
|
|
||||||
logger.info(f"total: {self.looper.metrics}")
|
|
||||||
|
|
||||||
|
|
||||||
def balance_loop(rpc: RpcHelper, out: str, in_: str, amount_msat: int, min_msat: int, max_msat: int, parallelism: int):
|
|
||||||
looper = LoopRouter(rpc)
|
|
||||||
bounds = TxBounds(min_msat=min_msat, max_msat=max_msat)
|
|
||||||
balancer = LoopBalancer(out, in_, amount_msat, looper, bounds, parallelism)
|
|
||||||
|
|
||||||
balancer.run_to_completion()
|
|
||||||
|
|
||||||
def autobalance_once(rpc: RpcHelper, metrics: Metrics, bounds: TxBounds, parallelism: int) -> bool:
|
|
||||||
"""
|
|
||||||
autobalances all channels.
|
|
||||||
returns True if channels are balanced (or as balanced as can be); False if in need of further balancing
|
|
||||||
"""
|
|
||||||
looper = LoopRouter(rpc, metrics)
|
|
||||||
balancer = MultiLoopBalancer(looper, bounds, parallelism)
|
|
||||||
|
|
||||||
channels = []
|
|
||||||
for peerch in rpc.rpc.listpeerchannels()["channels"]:
|
|
||||||
try:
|
|
||||||
channels.append(rpc.localchannel(peerch["short_channel_id"]))
|
|
||||||
except:
|
|
||||||
logger.info(f"NO CHANNELS for {peerch['peer_id']}")
|
|
||||||
|
|
||||||
channels = [ch for ch in channels if ch.online and ch.base_fee_to_me == 0]
|
|
||||||
give_to = [ ch for ch in channels if ch.send_ratio > 0.95 ]
|
|
||||||
take_from = [ ch for ch in channels if ch.send_ratio < 0.20 ]
|
|
||||||
|
|
||||||
if give_to == [] and take_from == []:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for to in give_to:
|
|
||||||
for from_ in take_from:
|
|
||||||
balancer.add_loop(to, from_, 10000000)
|
|
||||||
|
|
||||||
balancer.run_to_completion(exit_on_any_completed=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def autobalance(rpc: RpcHelper, min_msat: int, max_msat: int, parallelism: int):
|
|
||||||
bounds = TxBounds(min_msat=min_msat, max_msat=max_msat)
|
|
||||||
metrics = Metrics()
|
|
||||||
while not autobalance_once(rpc, metrics, bounds, parallelism):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def show_status(rpc: RpcHelper, full: bool=False):
|
|
||||||
"""
|
|
||||||
show a table of channel balances between peers.
|
|
||||||
"""
|
|
||||||
for peerch in rpc.rpc.listpeerchannels()["channels"]:
|
|
||||||
try:
|
|
||||||
ch = rpc.localchannel(peerch["short_channel_id"])
|
|
||||||
except:
|
|
||||||
print(f"{peerch['peer_id']} scid:{peerch['short_channel_id']} state:{peerch['state']} NO CHANNELS")
|
|
||||||
else:
|
|
||||||
print(ch.to_str(with_scid=True, with_bal_ratio=True, with_payments=True, with_cost=full, with_ppm_theirs=True, with_ppm_mine=True, with_peer_id=full))
|
|
||||||
|
|
||||||
def main():
|
|
||||||
logging.basicConfig()
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("--verbose", action="store_true", help="more logging")
|
|
||||||
parser.add_argument("--min-msat", default="999", help="min transaction size")
|
|
||||||
parser.add_argument("--max-msat", default="1000000", help="max transaction size")
|
|
||||||
parser.add_argument("--jobs", default="1", help="how many HTLCs to keep in-flight at once")
|
|
||||||
subparsers = parser.add_subparsers(help="action")
|
|
||||||
|
|
||||||
status_parser = subparsers.add_parser("status")
|
|
||||||
status_parser.set_defaults(action="status")
|
|
||||||
status_parser.add_argument("--full", action="store_true", help="more info per channel")
|
|
||||||
|
|
||||||
loop_parser = subparsers.add_parser("loop")
|
|
||||||
loop_parser.set_defaults(action="loop")
|
|
||||||
loop_parser.add_argument("out", help="peer id to send tx through")
|
|
||||||
loop_parser.add_argument("in_", help="peer id to receive tx through")
|
|
||||||
loop_parser.add_argument("amount", help="total amount of msat to loop")
|
|
||||||
|
|
||||||
autobal_parser = subparsers.add_parser("autobalance")
|
|
||||||
autobal_parser.set_defaults(action="autobalance")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
rpc = RpcHelper(LightningRpc(RPC_FILE))
|
|
||||||
|
|
||||||
if args.action == "status":
|
|
||||||
show_status(rpc, full=args.full)
|
|
||||||
|
|
||||||
if args.action == "loop":
|
|
||||||
balance_loop(rpc, out=args.out, in_=args.in_, amount_msat=int(args.amount), min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs))
|
|
||||||
|
|
||||||
if args.action == "autobalance":
|
|
||||||
autobalance(rpc, min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
|
@ -1,135 +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.ext = [
|
|
||||||
{ user = "clightning"; group = "clightning"; mode = "0710"; path = "/var/lib/clightning"; method = "bind"; }
|
|
||||||
];
|
|
||||||
|
|
||||||
# `lightning-cli` finds its RPC file via `~/.lightning/bitcoin/lightning-rpc`, to message the daemon
|
|
||||||
sane.user.fs.".lightning".symlink.target = "/var/lib/clightning";
|
|
||||||
|
|
||||||
# see bitcoin.nix for how to generate this
|
|
||||||
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" ];
|
|
||||||
|
|
||||||
# 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-level=debug:lightningd
|
|
||||||
# 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=${pkgs.peerswap}/bin/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.clightning.enableFor.user.colin = true; # for debugging/admin: `lightning-cli`
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./bitcoin.nix
|
|
||||||
./clightning.nix
|
|
||||||
./i2p.nix
|
|
||||||
./monero.nix
|
|
||||||
./tor.nix
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
services.i2p.enable = true;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
# as of 2023/11/26: complete downloaded blockchain should be 200GiB on disk, give or take.
|
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
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, ... }:
|
|
||||||
{
|
|
||||||
# tor hidden service hostnames aren't deterministic, so persist.
|
|
||||||
# might be able to get away with just persisting /var/lib/tor/onion, not sure.
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
|
||||||
{ 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
|
|
||||||
}
|
|
27
hosts/by-name/servo/services/ddns-afraid.nix
Normal file
27
hosts/by-name/servo/services/ddns-afraid.nix
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
# using manual ddns now
|
||||||
|
lib.mkIf false
|
||||||
|
{
|
||||||
|
systemd.services.ddns-afraid = {
|
||||||
|
description = "update dynamic DNS entries for freedns.afraid.org";
|
||||||
|
serviceConfig = {
|
||||||
|
EnvironmentFile = config.sops.secrets."ddns_afraid.env".path;
|
||||||
|
# TODO: ProtectSystem = "strict";
|
||||||
|
# TODO: ProtectHome = "full";
|
||||||
|
# TODO: PrivateTmp = true;
|
||||||
|
};
|
||||||
|
script = let
|
||||||
|
curl = "${pkgs.curl}/bin/curl -4";
|
||||||
|
in ''
|
||||||
|
${curl} "https://freedns.afraid.org/dynamic/update.php?$AFRAID_KEY"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
systemd.timers.ddns-afraid = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnStartupSec = "2min";
|
||||||
|
OnUnitActiveSec = "10min";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
30
hosts/by-name/servo/services/ddns-he.nix
Normal file
30
hosts/by-name/servo/services/ddns-he.nix
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
# we use manual DDNS now
|
||||||
|
lib.mkIf false
|
||||||
|
{
|
||||||
|
systemd.services.ddns-he = {
|
||||||
|
description = "update dynamic DNS entries for HurricaneElectric";
|
||||||
|
serviceConfig = {
|
||||||
|
EnvironmentFile = config.sops.secrets."ddns_he.env".path;
|
||||||
|
# TODO: ProtectSystem = "strict";
|
||||||
|
# TODO: ProtectHome = "full";
|
||||||
|
# TODO: PrivateTmp = true;
|
||||||
|
};
|
||||||
|
# HE DDNS API is documented: https://dns.he.net/docs.html
|
||||||
|
script = let
|
||||||
|
crl = "${pkgs.curl}/bin/curl -4";
|
||||||
|
in ''
|
||||||
|
${crl} "https://he.uninsane.org:$HE_PASSPHRASE@dyn.dns.he.net/nic/update?hostname=he.uninsane.org"
|
||||||
|
${crl} "https://native.uninsane.org:$HE_PASSPHRASE@dyn.dns.he.net/nic/update?hostname=native.uninsane.org"
|
||||||
|
${crl} "https://uninsane.org:$HE_PASSPHRASE@dyn.dns.he.net/nic/update?hostname=uninsane.org"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
systemd.timers.ddns-he = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnStartupSec = "2min";
|
||||||
|
OnUnitActiveSec = "10min";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,12 +2,12 @@
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./calibre.nix
|
./calibre.nix
|
||||||
./coturn.nix
|
./ddns-afraid.nix
|
||||||
./cryptocurrencies
|
./ddns-he.nix
|
||||||
./email
|
./email
|
||||||
./ejabberd.nix
|
./ejabberd.nix
|
||||||
./freshrss.nix
|
./freshrss.nix
|
||||||
./export
|
./ftp
|
||||||
./gitea.nix
|
./gitea.nix
|
||||||
./goaccess.nix
|
./goaccess.nix
|
||||||
./ipfs.nix
|
./ipfs.nix
|
||||||
|
@ -18,15 +18,14 @@
|
||||||
./lemmy.nix
|
./lemmy.nix
|
||||||
./matrix
|
./matrix
|
||||||
./navidrome.nix
|
./navidrome.nix
|
||||||
|
./nfs.nix
|
||||||
|
./nixserve.nix
|
||||||
./nginx.nix
|
./nginx.nix
|
||||||
./nixos-prebuild.nix
|
|
||||||
./ntfy
|
|
||||||
./pict-rs.nix
|
./pict-rs.nix
|
||||||
./pleroma.nix
|
./pleroma.nix
|
||||||
./postgres.nix
|
./postgres.nix
|
||||||
./prosody
|
./prosody.nix
|
||||||
./slskd.nix
|
./transmission.nix
|
||||||
./transmission
|
|
||||||
./trust-dns.nix
|
./trust-dns.nix
|
||||||
./wikipedia.nix
|
./wikipedia.nix
|
||||||
];
|
];
|
||||||
|
|
|
@ -14,111 +14,76 @@
|
||||||
#
|
#
|
||||||
# compliance tests:
|
# compliance tests:
|
||||||
# - <https://compliance.conversations.im/server/uninsane.org/#xep0352>
|
# - <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, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
# XXX: avatar support works in MUCs but not DMs
|
||||||
# TODO: this range could be larger, but right now that's costly because each element is its own UPnP forward
|
# lib.mkIf false
|
||||||
# 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.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; method = "bind"; }
|
{ user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; }
|
||||||
];
|
];
|
||||||
sane.ports.ports = lib.mkMerge ([
|
sane.ports.ports."3478" = {
|
||||||
{
|
protocol = [ "tcp" "udp" ];
|
||||||
"3478" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" "udp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpp-stun-turn";
|
||||||
visibleTo.lan = true;
|
};
|
||||||
description = "colin-xmpp-stun-turn";
|
sane.ports.ports."5222" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5222" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpp-client-to-server";
|
||||||
visibleTo.lan = true;
|
};
|
||||||
description = "colin-xmpp-client-to-server";
|
sane.ports.ports."5223" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5223" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpps-client-to-server"; # XMPP over TLS
|
||||||
visibleTo.lan = true;
|
};
|
||||||
description = "colin-xmpps-client-to-server"; # XMPP over TLS
|
sane.ports.ports."5269" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5269" = {
|
visibleTo.wan = true;
|
||||||
protocol = [ "tcp" ];
|
description = "colin-xmpp-server-to-server";
|
||||||
visibleTo.doof = true;
|
};
|
||||||
description = "colin-xmpp-server-to-server";
|
sane.ports.ports."5270" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5270" = {
|
visibleTo.wan = true;
|
||||||
protocol = [ "tcp" ];
|
description = "colin-xmpps-server-to-server"; # XMPP over TLS
|
||||||
visibleTo.doof = true;
|
};
|
||||||
description = "colin-xmpps-server-to-server"; # XMPP over TLS
|
sane.ports.ports."5280" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5280" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpp-bosh";
|
||||||
visibleTo.lan = true;
|
};
|
||||||
description = "colin-xmpp-bosh";
|
sane.ports.ports."5281" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5281" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpp-bosh-https";
|
||||||
visibleTo.lan = true;
|
};
|
||||||
description = "colin-xmpp-bosh-https";
|
sane.ports.ports."5349" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5349" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpp-stun-turn-over-tls";
|
||||||
visibleTo.lan = true;
|
};
|
||||||
description = "colin-xmpp-stun-turn-over-tls";
|
sane.ports.ports."5443" = {
|
||||||
};
|
protocol = [ "tcp" ];
|
||||||
"5443" = {
|
visibleTo.lan = true;
|
||||||
protocol = [ "tcp" ];
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
description = "colin-xmpp-web-services"; # file uploads, websockets, admin
|
||||||
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
|
# TODO: forward these TURN ports!
|
||||||
services.coturn.enable = false;
|
networking.firewall.allowedTCPPortRanges = [{
|
||||||
|
from = 49152; # TURN
|
||||||
|
to = 49408;
|
||||||
|
}];
|
||||||
|
networking.firewall.allowedUDPPortRanges = [{
|
||||||
|
from = 49152; # TURN
|
||||||
|
to = 49408;
|
||||||
|
}];
|
||||||
|
|
||||||
# provide access to certs
|
# provide access to certs
|
||||||
# TODO: this should just be `acme`. then we also add nginx to the `acme` group.
|
# TODO: this should just be `acme`. then we also add nginx to the `acme` group.
|
||||||
|
@ -185,285 +150,284 @@ lib.mkIf false
|
||||||
services.ejabberd.enable = true;
|
services.ejabberd.enable = true;
|
||||||
services.ejabberd.configFile = "/var/lib/ejabberd/ejabberd.yaml";
|
services.ejabberd.configFile = "/var/lib/ejabberd/ejabberd.yaml";
|
||||||
systemd.services.ejabberd.preStart = let
|
systemd.services.ejabberd.preStart = let
|
||||||
config-in = pkgs.writeText "ejabberd.yaml.in" (lib.generators.toYAML {} {
|
config-in = pkgs.writeTextFile {
|
||||||
hosts = [ "uninsane.org" ];
|
name = "ejabberd.yaml.in";
|
||||||
# none | emergency | alert | critical | error | warning | notice | info | debug
|
text = ''
|
||||||
loglevel = "debug";
|
hosts:
|
||||||
acme.auto = false;
|
- uninsane.org
|
||||||
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";
|
# none | emergency | alert | critical | error | warning | notice | info | debug
|
||||||
acl = {
|
loglevel: debug
|
||||||
admin.user = [ "colin@uninsane.org" ];
|
# loglevel: info
|
||||||
local.user_regexp = "";
|
# loglevel: notice
|
||||||
loopback.ip = [ "127.0.0.0/8" "::1/128" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
access_rules = {
|
acme:
|
||||||
local.allow = "local";
|
auto: false
|
||||||
c2s_access.allow = "all";
|
certfiles:
|
||||||
announce.allow = "admin";
|
- /var/lib/acme/uninsane.org/full.pem
|
||||||
configure.allow = "admin";
|
# ca_file: ${pkgs.cacert.unbundled}/etc/ssl/certs/
|
||||||
muc_create.allow = "local";
|
# ca_file: ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||||
pubsub_createnode_access.allow = "all";
|
|
||||||
trusted_network.allow = "loopback";
|
|
||||||
};
|
|
||||||
|
|
||||||
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shaper-rules>
|
pam_userinfotype: jid
|
||||||
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>
|
acl:
|
||||||
# this limits the bytes/sec.
|
admin:
|
||||||
# for example, burst: 3_000_000 and rate: 100_000 means:
|
user:
|
||||||
# - each client has a BW budget that accumulates 100kB/sec and is capped at 3 MB
|
- "colin@uninsane.org"
|
||||||
shaper.fast = 1000000;
|
local:
|
||||||
shaper.med = 500000;
|
user_regexp: ""
|
||||||
# shaper.fast.rate = 1000000;
|
loopback:
|
||||||
# shaper.fast.burst_size = 10000000;
|
ip:
|
||||||
# shaper.med.rate = 500000;
|
- 127.0.0.0/8
|
||||||
# shaper.med.burst_size = 5000000;
|
- ::1/128
|
||||||
|
|
||||||
# see: <https://docs.ejabberd.im/admin/configuration/listen/>
|
access_rules:
|
||||||
# s2s_use_starttls = true;
|
local:
|
||||||
s2s_use_starttls = "optional";
|
allow: local
|
||||||
# lessens 504: remote-server-timeout errors
|
c2s_access:
|
||||||
# see: <https://github.com/processone/ejabberd/issues/3105#issuecomment-562182967>
|
allow: all
|
||||||
negotiation_timeout = 60;
|
announce:
|
||||||
|
allow: admin
|
||||||
|
configure:
|
||||||
|
allow: admin
|
||||||
|
muc_create:
|
||||||
|
allow: local
|
||||||
|
pubsub_createnode_access:
|
||||||
|
allow: all
|
||||||
|
trusted_network:
|
||||||
|
allow: loopback
|
||||||
|
|
||||||
listen = [
|
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shaper-rules>
|
||||||
{
|
shaper_rules:
|
||||||
port = 5222;
|
# setting this to above 1 may break outgoing messages
|
||||||
module = "ejabberd_c2s";
|
# - maybe some servers rate limit? or just don't understand simultaneous connections?
|
||||||
shaper = "c2s_shaper";
|
max_s2s_connections: 1
|
||||||
starttls = true;
|
max_user_sessions: 10
|
||||||
access = "c2s_access";
|
max_user_offline_messages: 5000
|
||||||
}
|
c2s_shaper:
|
||||||
{
|
fast: all
|
||||||
port = 5223;
|
s2s_shaper:
|
||||||
module = "ejabberd_c2s";
|
med: all
|
||||||
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
|
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shapers>
|
||||||
# TODO(low): look into mod_http_fileserver for serving macros?
|
# this limits the bytes/sec.
|
||||||
modules = {
|
# for example, burst: 3_000_000 and rate: 100_000 means:
|
||||||
# mod_adhoc = {};
|
# - each client has a BW budget that accumulates 100kB/sec and is capped at 3 MB
|
||||||
# mod_announce = {
|
shaper:
|
||||||
# access = "admin";
|
fast: 1000000
|
||||||
# };
|
med: 500000
|
||||||
# allows users to set avatars in vCard
|
# fast:
|
||||||
# - <https://docs.ejabberd.im/admin/configuration/modules/#mod-avatar>
|
# - rate: 1000000
|
||||||
mod_avatar = {};
|
# - burst_size: 10000000
|
||||||
mod_caps = {}; # for mod_pubsub
|
# med:
|
||||||
mod_carboncopy = {}; # allows multiple clients to receive a user's message
|
# - rate: 500000
|
||||||
# queues messages when recipient is offline, including PEP and presence messages.
|
# - burst_size: 5000000
|
||||||
# compliance test suggests this be enabled
|
|
||||||
mod_client_state = {};
|
|
||||||
|
|
||||||
# mod_conversejs: TODO: enable once on 21.12
|
# see: <https://docs.ejabberd.im/admin/configuration/listen/>
|
||||||
# allows clients like Dino to discover where to upload files
|
# s2s_use_starttls: true
|
||||||
mod_disco.server_info = [
|
s2s_use_starttls: optional
|
||||||
{
|
# lessens 504: remote-server-timeout errors
|
||||||
modules = "all";
|
# see: <https://github.com/processone/ejabberd/issues/3105#issuecomment-562182967>
|
||||||
name = "abuse-addresses";
|
negotiation_timeout: 60
|
||||||
urls = [
|
|
||||||
"mailto:admin.xmpp@uninsane.org"
|
listen:
|
||||||
"xmpp:colin@uninsane.org"
|
-
|
||||||
];
|
port: 5222
|
||||||
}
|
module: ejabberd_c2s
|
||||||
{
|
shaper: c2s_shaper
|
||||||
modules = "all";
|
starttls: true
|
||||||
name = "admin-addresses";
|
access: c2s_access
|
||||||
urls = [
|
-
|
||||||
"mailto:admin.xmpp@uninsane.org"
|
port: 5223
|
||||||
"xmpp:colin@uninsane.org"
|
module: ejabberd_c2s
|
||||||
];
|
shaper: c2s_shaper
|
||||||
}
|
tls: true
|
||||||
];
|
access: c2s_access
|
||||||
mod_http_upload = {
|
-
|
||||||
host = "upload.xmpp.uninsane.org";
|
port: 5269
|
||||||
hosts = [ "upload.xmpp.uninsane.org" ];
|
module: ejabberd_s2s_in
|
||||||
put_url = "https://@HOST@:5443/upload";
|
shaper: s2s_shaper
|
||||||
dir_mode = "0750";
|
-
|
||||||
file_mode = "0750";
|
port: 5270
|
||||||
rm_on_unregister = false;
|
module: ejabberd_s2s_in
|
||||||
};
|
shaper: s2s_shaper
|
||||||
# allow discoverability of BOSH and websocket endpoints
|
tls: true
|
||||||
# TODO: enable once on ejabberd 22.05 (presently 21.04)
|
-
|
||||||
# mod_host_meta = {};
|
port: 5443
|
||||||
mod_jidprep = {}; # probably not needed: lets clients normalize jids
|
module: ejabberd_http
|
||||||
mod_last = {}; # allow other users to know when i was last online
|
tls: true
|
||||||
mod_mam = {
|
request_handlers:
|
||||||
# Mnesia is limited to 2GB, better to use an SQL backend
|
/admin: ejabberd_web_admin # TODO: ensure this actually works
|
||||||
# For small servers SQLite is a good fit and is very easy
|
/api: mod_http_api # ejabberd API endpoint (to control server)
|
||||||
# to configure. Uncomment this when you have SQL configured:
|
/bosh: mod_bosh
|
||||||
# db_type: sql
|
/upload: mod_http_upload
|
||||||
assume_mam_usage = true;
|
/ws: ejabberd_http_ws
|
||||||
default = "always";
|
# /.well-known/host-meta: mod_host_meta
|
||||||
};
|
# /.well-known/host-meta.json: mod_host_meta
|
||||||
mod_muc = {
|
-
|
||||||
access = [ "allow" ];
|
# STUN+TURN TCP
|
||||||
access_admin = { allow = "admin"; };
|
# note that the full port range should be forwarded ("not NAT'd")
|
||||||
access_create = "muc_create";
|
# `use_turn=true` enables both TURN *and* STUN
|
||||||
access_persistent = "muc_create";
|
port: 3478
|
||||||
access_mam = [ "allow" ];
|
module: ejabberd_stun
|
||||||
history_size = 100; # messages to show new participants
|
transport: tcp
|
||||||
host = "muc.xmpp.uninsane.org";
|
use_turn: true
|
||||||
hosts = [ "muc.xmpp.uninsane.org" ];
|
turn_min_port: 49152
|
||||||
default_room_options = {
|
turn_max_port: 65535
|
||||||
anonymous = false;
|
turn_ipv4_address: %ANATIVE%
|
||||||
lang = "en";
|
-
|
||||||
persistent = true;
|
# STUN+TURN UDP
|
||||||
mam = true;
|
port: 3478
|
||||||
};
|
module: ejabberd_stun
|
||||||
};
|
transport: udp
|
||||||
mod_muc_admin = {};
|
use_turn: true
|
||||||
mod_offline = {
|
turn_min_port: 49152
|
||||||
# store messages for a user when they're offline (TODO: understand multi-client workflow?)
|
turn_max_port: 65535
|
||||||
access_max_user_messages = "max_user_offline_messages";
|
turn_ipv4_address: %ANATIVE%
|
||||||
store_groupchat = true;
|
-
|
||||||
};
|
# STUN+TURN TLS over TCP
|
||||||
mod_ping = {};
|
port: 5349
|
||||||
mod_privacy = {}; # deprecated, but required for `ejabberctl export_piefxis`
|
module: ejabberd_stun
|
||||||
mod_private = {}; # allow local clients to persist arbitrary data on my server
|
transport: tcp
|
||||||
# push notifications to services integrated with e.g. Apple/Android.
|
tls: true
|
||||||
# default is for a maximum amount of PII to be withheld, since these push notifs
|
certfile: /var/lib/acme/uninsane.org/full.pem
|
||||||
# generally traverse 3rd party services. can opt to include message body, etc, though.
|
use_turn: true
|
||||||
mod_push = {};
|
turn_min_port: 49152
|
||||||
# i don't fully understand what this does, but it seems aimed at making push notifs more reliable.
|
turn_max_port: 65535
|
||||||
mod_push_keepalive = {};
|
turn_ipv4_address: %ANATIVE%
|
||||||
mod_roster = {
|
|
||||||
versioning = true;
|
# TODO: enable mod_fail2ban
|
||||||
};
|
# TODO(low): look into mod_http_fileserver for serving macros?
|
||||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-s2s-dialback>
|
modules:
|
||||||
# s2s dialback to verify inbound messages
|
# mod_adhoc: {}
|
||||||
# unclear to what degree the XMPP network requires this
|
# mod_announce:
|
||||||
mod_s2s_dialback = {};
|
# access: admin
|
||||||
mod_shared_roster = {}; # creates groups for @all, @online, and anything manually administered?
|
# allows users to set avatars in vCard
|
||||||
mod_stream_mgmt = {
|
# - <https://docs.ejabberd.im/admin/configuration/modules/#mod-avatar>
|
||||||
# resend undelivered messages if the origin client is offline
|
mod_avatar: {}
|
||||||
resend_on_timeout = "if_offline";
|
mod_caps: {} # for mod_pubsub
|
||||||
};
|
mod_carboncopy: {} # allows multiple clients to receive a user's message
|
||||||
# fallback for when DNS-based STUN discovery is unsupported.
|
# queues messages when recipient is offline, including PEP and presence messages.
|
||||||
# - see: <https://xmpp.org/extensions/xep-0215.html>
|
# compliance test suggests this be enabled
|
||||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-stun-disco>
|
mod_client_state: {}
|
||||||
# people say to just keep this defaulted (i guess ejabberd knows to return its `host` option of uninsane.org?)
|
# mod_conversejs: TODO: enable once on 21.12
|
||||||
mod_stun_disco = {};
|
# allows clients like Dino to discover where to upload files
|
||||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-vcard>
|
mod_disco:
|
||||||
mod_vcard = {
|
server_info:
|
||||||
allow_return_all = true; # all users are discoverable (?)
|
-
|
||||||
host = "vjid.xmpp.uninsane.org";
|
modules: all
|
||||||
hosts = [ "vjid.xmpp.uninsane.org" ];
|
name: abuse-addresses
|
||||||
search = true;
|
urls:
|
||||||
};
|
- "mailto:admin.xmpp@uninsane.org"
|
||||||
mod_vcard_xupdate = {}; # needed for avatars
|
- "xmpp:colin@uninsane.org"
|
||||||
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-pubsub>
|
-
|
||||||
mod_pubsub = {
|
modules: all
|
||||||
#^ needed for avatars
|
name: admin-addresses
|
||||||
access_createnode = "pubsub_createnode_access";
|
urls:
|
||||||
host = "pubsub.xmpp.uninsane.org";
|
- "mailto:admin.xmpp@uninsane.org"
|
||||||
hosts = [ "pubsub.xmpp.uninsane.org" ];
|
- "xmpp:colin@uninsane.org"
|
||||||
ignore_pep_from_offline = false;
|
mod_http_upload:
|
||||||
last_item_cache = true;
|
host: upload.xmpp.uninsane.org
|
||||||
plugins = [
|
hosts:
|
||||||
"pep"
|
- upload.xmpp.uninsane.org
|
||||||
"flat"
|
put_url: "https://@HOST@:5443/upload"
|
||||||
];
|
dir_mode: "0750"
|
||||||
force_node_config = {
|
file_mode: "0750"
|
||||||
# ensure client bookmarks are private
|
rm_on_unregister: false
|
||||||
"storage:bookmarks:" = {
|
# allow discoverability of BOSH and websocket endpoints
|
||||||
"access_model" = "whitelist";
|
# TODO: enable once on ejabberd 22.05 (presently 21.04)
|
||||||
};
|
# mod_host_meta: {}
|
||||||
"urn:xmpp:avatar:data" = {
|
mod_jidprep: {} # probably not needed: lets clients normalize jids
|
||||||
"access_model" = "open";
|
mod_last: {} # allow other users to know when i was last online
|
||||||
};
|
mod_mam:
|
||||||
"urn:xmpp:avatar:metadata" = {
|
# Mnesia is limited to 2GB, better to use an SQL backend
|
||||||
"access_model" = "open";
|
# 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
|
||||||
mod_version = {};
|
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_on_timeout: if_offline # resend undelivered messages if the origin client is 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: {}
|
||||||
|
'';
|
||||||
|
};
|
||||||
sed = "${pkgs.gnused}/bin/sed";
|
sed = "${pkgs.gnused}/bin/sed";
|
||||||
in ''
|
in ''
|
||||||
ip=$(cat '${config.sane.services.dyn-dns.ipPath}')
|
ip=$(cat '${config.sane.services.dyn-dns.ipPath}')
|
||||||
# config is 444 (not 644), so we want to write out-of-place and then atomically move
|
# config is 444 (not 644), so we want to write out-of-place and then atomically move
|
||||||
# TODO: factor this out into `sane-woop` helper?
|
# TODO: factor this out into `sane-woop` helper?
|
||||||
rm -f /var/lib/ejabberd/ejabberd.yaml.new
|
rm -f /var/lib/ejabberd/ejabberd.yaml.new
|
||||||
${sed} "s/%ANATIVE%/$ip/g" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new
|
${sed} "s/%ANATIVE%/$ip/" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new
|
||||||
mv /var/lib/ejabberd/ejabberd.yaml{.new,}
|
mv /var/lib/ejabberd/ejabberd.yaml{.new,}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,6 @@
|
||||||
# - but postfix delegates authorization of that outgoing mail to dovecot, on the server side
|
# - but postfix delegates authorization of that outgoing mail to dovecot, on the server side
|
||||||
#
|
#
|
||||||
# - local clients (i.e. sendmail) interact only with postfix
|
# - 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=185.157.162.178' | 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 185.157.162.178 25`, then try https://canyouseeme.org
|
|
||||||
|
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
{
|
{
|
||||||
sane.ports.ports."143" = {
|
sane.ports.ports."143" = {
|
||||||
protocol = [ "tcp" ];
|
protocol = [ "tcp" ];
|
||||||
visibleTo.doof = true;
|
|
||||||
visibleTo.lan = true;
|
visibleTo.lan = true;
|
||||||
|
visibleTo.wan = true;
|
||||||
description = "colin-imap-imap.uninsane.org";
|
description = "colin-imap-imap.uninsane.org";
|
||||||
};
|
};
|
||||||
sane.ports.ports."993" = {
|
sane.ports.ports."993" = {
|
||||||
protocol = [ "tcp" ];
|
protocol = [ "tcp" ];
|
||||||
visibleTo.doof = true;
|
|
||||||
visibleTo.lan = true;
|
visibleTo.lan = true;
|
||||||
|
visibleTo.wan = true;
|
||||||
description = "colin-imaps-imap.uninsane.org";
|
description = "colin-imaps-imap.uninsane.org";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -127,11 +127,10 @@
|
||||||
services.dovecot2.modules = [
|
services.dovecot2.modules = [
|
||||||
pkgs.dovecot_pigeonhole # enables sieve execution (?)
|
pkgs.dovecot_pigeonhole # enables sieve execution (?)
|
||||||
];
|
];
|
||||||
services.dovecot2.sieve = {
|
services.dovecot2.sieveScripts = {
|
||||||
extensions = [ "fileinto" ];
|
|
||||||
# if any messages fail to pass (or lack) DKIM, move them to Junk
|
# 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
|
# XXX the key name ("after") is only used to order sieve execution/ordering
|
||||||
scripts.after = builtins.toFile "ensuredkim.sieve" ''
|
after = builtins.toFile "ensuredkim.sieve" ''
|
||||||
require "fileinto";
|
require "fileinto";
|
||||||
|
|
||||||
if not header :contains "Authentication-Results" "dkim=pass" {
|
if not header :contains "Authentication-Results" "dkim=pass" {
|
||||||
|
@ -140,6 +139,4 @@
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.dovecot2.serviceConfig.RestartSec = lib.mkForce "15s"; # nixos defaults this to 1s
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# postfix config options: <https://www.postfix.org/postconf.5.html>
|
# postfix config options: <https://www.postfix.org/postconf.5.html>
|
||||||
|
|
||||||
{ config, lib, pkgs, ... }:
|
{ lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
submissionOptions = {
|
submissionOptions = {
|
||||||
|
@ -18,35 +18,31 @@ let
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
# TODO: mode? could be more granular
|
# TODO: mode? could be more granular
|
||||||
{ user = "opendkim"; group = "opendkim"; path = "/var/lib/opendkim"; method = "bind"; }
|
{ user = "opendkim"; group = "opendkim"; path = "/var/lib/opendkim"; }
|
||||||
{ user = "root"; group = "root"; path = "/var/lib/postfix"; method = "bind"; }
|
{ user = "root"; group = "root"; path = "/var/lib/postfix"; }
|
||||||
{ user = "root"; group = "root"; path = "/var/spool/mail"; method = "bind"; }
|
{ user = "root"; group = "root"; path = "/var/spool/mail"; }
|
||||||
# *probably* don't need these dirs:
|
# *probably* don't need these dirs:
|
||||||
# "/var/lib/dhparams" # https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/dhparams.nix
|
# "/var/lib/dhparams" # https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/dhparams.nix
|
||||||
# "/var/lib/dovecot"
|
# "/var/lib/dovecot"
|
||||||
];
|
];
|
||||||
|
|
||||||
# XXX(2023/10/20): opening these ports in the firewall has the OPPOSITE effect as intended.
|
sane.ports.ports."25" = {
|
||||||
# these ports are only routable so long as they AREN'T opened.
|
protocol = [ "tcp" ];
|
||||||
# probably some cursed interaction with network namespaces introduced after 2023/10/10.
|
visibleTo.ovpn = true;
|
||||||
# sane.ports.ports."25" = {
|
description = "colin-smtp-mx.uninsane.org";
|
||||||
# protocol = [ "tcp" ];
|
};
|
||||||
# # XXX visibleTo.lan effectively means "open firewall, but don't configure any NAT/forwarding"
|
sane.ports.ports."465" = {
|
||||||
# visibleTo.lan = true;
|
protocol = [ "tcp" ];
|
||||||
# description = "colin-smtp-mx.uninsane.org";
|
visibleTo.ovpn = true;
|
||||||
# };
|
description = "colin-smtps-mx.uninsane.org";
|
||||||
# sane.ports.ports."465" = {
|
};
|
||||||
# protocol = [ "tcp" ];
|
sane.ports.ports."587" = {
|
||||||
# visibleTo.lan = true;
|
protocol = [ "tcp" ];
|
||||||
# description = "colin-smtps-mx.uninsane.org";
|
visibleTo.ovpn = true;
|
||||||
# };
|
description = "colin-smtps-submission-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
|
# exists only to manage certs for Postfix
|
||||||
services.nginx.virtualHosts."mx.uninsane.org" = {
|
services.nginx.virtualHosts."mx.uninsane.org" = {
|
||||||
|
@ -56,7 +52,8 @@ in
|
||||||
|
|
||||||
sane.dns.zones."uninsane.org".inet = {
|
sane.dns.zones."uninsane.org".inet = {
|
||||||
MX."@" = "10 mx.uninsane.org.";
|
MX."@" = "10 mx.uninsane.org.";
|
||||||
A."mx" = "%AOVPNS%"; #< XXX: RFC's specify that the MX record CANNOT BE A CNAME. TODO: use "%AOVPNS%?
|
# XXX: RFC's specify that the MX record CANNOT BE A CNAME
|
||||||
|
A."mx" = "185.157.162.178";
|
||||||
|
|
||||||
# Sender Policy Framework:
|
# Sender Policy Framework:
|
||||||
# +mx => mail passes if it originated from the MX
|
# +mx => mail passes if it originated from the MX
|
||||||
|
|
|
@ -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" ];
|
|
||||||
};
|
|
||||||
fileSystems."/var/export/pub" = {
|
|
||||||
device = "/var/www/sites/uninsane.org/share";
|
|
||||||
options = [ "rbind" ];
|
|
||||||
};
|
|
||||||
# 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" = {
|
|
||||||
wantedBy = [ "nfs.service" "sftpgo.service" ];
|
|
||||||
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
|
|
||||||
- pub/ read-only: content made to be shared with the www
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.fs."/var/export/playground/README.md" = {
|
|
||||||
wantedBy = [ "nfs.service" "sftpgo.service" ];
|
|
||||||
file.text = ''
|
|
||||||
this directory is intentionally read+write by anyone with access (i.e. on the LAN).
|
|
||||||
- share files
|
|
||||||
- write poetry
|
|
||||||
- be a friendly troll
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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,164 +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.mkPython3Bin {
|
|
||||||
pname = "external_auth_hook";
|
|
||||||
srcRoot = ./.;
|
|
||||||
pyPkgs = [ "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 LAN client can connect (also WAN traffic forwarded from the gateway)
|
|
||||||
address = "10.78.79.51";
|
|
||||||
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 = 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.hostVethIpv4;
|
|
||||||
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 = "${external_auth_hook}/bin/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" ];
|
|
||||||
wants = [ "network-online.target" ];
|
|
||||||
serviceConfig = {
|
|
||||||
ReadWritePaths = [ "/var/export" ];
|
|
||||||
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = "20s";
|
|
||||||
UMask = lib.mkForce "0002";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
#!/usr/bin/env nix-shell
|
|
||||||
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.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
|
|
||||||
]
|
|
||||||
|
|
||||||
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) -> bool:
|
|
||||||
for cred in TRUSTED_CREDS:
|
|
||||||
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,
|
|
||||||
"/pub": PERM_RO,
|
|
||||||
})
|
|
||||||
if isWireguard(ip):
|
|
||||||
# allow any user from wireguard
|
|
||||||
return mkAuthOk(username, permissions = {
|
|
||||||
"/": PERM_RW,
|
|
||||||
"/playground": PERM_RW,
|
|
||||||
"/pub": PERM_RO,
|
|
||||||
})
|
|
||||||
if isLan(ip):
|
|
||||||
if username == "anonymous":
|
|
||||||
# allow anonymous users on the LAN
|
|
||||||
return mkAuthOk("anonymous", permissions = {
|
|
||||||
"/": PERM_RO,
|
|
||||||
"/playground": PERM_RW,
|
|
||||||
"/pub": 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,
|
|
||||||
"/pub": 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
|
|
|
@ -15,8 +15,8 @@
|
||||||
owner = config.users.users.freshrss.name;
|
owner = config.users.users.freshrss.name;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
};
|
};
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "freshrss"; group = "freshrss"; path = "/var/lib/freshrss"; method = "bind"; }
|
{ user = "freshrss"; group = "freshrss"; path = "/var/lib/freshrss"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
services.freshrss.enable = true;
|
services.freshrss.enable = true;
|
||||||
|
|
70
hosts/by-name/servo/services/ftp/default.nix
Normal file
70
hosts/by-name/servo/services/ftp/default.nix
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# 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>
|
||||||
|
#
|
||||||
|
# sftpgo is a FTP server that also supports WebDAV, SFTP, and web clients.
|
||||||
|
|
||||||
|
|
||||||
|
{ lib, pkgs, sane-lib, ... }:
|
||||||
|
let
|
||||||
|
authProgram = pkgs.static-nix-shell.mkBash {
|
||||||
|
pname = "sftpgo_external_auth_hook";
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Client initiates a FTP "control connection" on port 21.
|
||||||
|
# - this handles the client -> server commands, and the server -> client status, but not the actual data
|
||||||
|
# - file data, directory listings, etc need to be transferred on an ephemeral "data port".
|
||||||
|
# - 50000-50100 is a common port range for this.
|
||||||
|
sane.ports.ports = {
|
||||||
|
"21" = {
|
||||||
|
protocol = [ "tcp" ];
|
||||||
|
visibleTo.lan = true;
|
||||||
|
description = "colin-FTP server";
|
||||||
|
};
|
||||||
|
} // (sane-lib.mapToAttrs
|
||||||
|
(port: {
|
||||||
|
name = builtins.toString port;
|
||||||
|
value = {
|
||||||
|
protocol = [ "tcp" ];
|
||||||
|
visibleTo.lan = true;
|
||||||
|
description = "colin-FTP server data port range";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
(lib.range 50000 50100)
|
||||||
|
);
|
||||||
|
|
||||||
|
services.sftpgo = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
ftpd = {
|
||||||
|
bindings = [{
|
||||||
|
address = "10.0.10.5";
|
||||||
|
port = 21;
|
||||||
|
debug = true;
|
||||||
|
}];
|
||||||
|
|
||||||
|
# active mode is susceptible to "bounce attacks", without much benefit over passive mode
|
||||||
|
disable_active_mode = true;
|
||||||
|
hash_support = true;
|
||||||
|
passive_port_range = {
|
||||||
|
start = 50000;
|
||||||
|
end = 50100;
|
||||||
|
};
|
||||||
|
|
||||||
|
banner = ''
|
||||||
|
Welcome, friends, to Colin's read-only FTP server! Also available via NFS on the same host.
|
||||||
|
Please let me know if anything's broken or not as it should be. Otherwise, browse and DL freely :)
|
||||||
|
'';
|
||||||
|
|
||||||
|
};
|
||||||
|
data_provider = {
|
||||||
|
driver = "memory";
|
||||||
|
external_auth_hook = "${authProgram}/bin/sftpgo_external_auth_hook";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
55
hosts/by-name/servo/services/ftp/sftpgo_external_auth_hook
Executable file
55
hosts/by-name/servo/services/ftp/sftpgo_external_auth_hook
Executable file
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#!nix-shell -i bash
|
||||||
|
# vim: set filetype=bash :
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# TODO: don't reuse /var/nfs/export here. formalize this some other way.
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$SFTPGO_AUTHD_USERNAME" = "anonymous" ]; then
|
||||||
|
echo '{'
|
||||||
|
echo ' "status":1,'
|
||||||
|
echo ' "username":"anonymous","expiration_date":0,'
|
||||||
|
echo ' "home_dir":"/var/nfs/export","uid":65534,"gid":65534,"max_sessions":0,"quota_size":0,"quota_files":100000,'
|
||||||
|
echo ' "permissions":{'
|
||||||
|
echo ' "/":["list", "download"]'
|
||||||
|
echo ' },'
|
||||||
|
echo ' "upload_bandwidth":0,"download_bandwidth":0,'
|
||||||
|
echo ' "filters":{"allowed_ip":[],"denied_ip":[]},"public_keys":[]'
|
||||||
|
echo '}'
|
||||||
|
else
|
||||||
|
echo '{"username":""}'
|
||||||
|
fi
|
|
@ -2,9 +2,9 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
# TODO: mode? could be more granular
|
# TODO: mode? could be more granular
|
||||||
{ user = "git"; group = "gitea"; path = "/var/lib/gitea"; method = "bind"; }
|
{ user = "git"; group = "gitea"; path = "/var/lib/gitea"; }
|
||||||
];
|
];
|
||||||
services.gitea.enable = true;
|
services.gitea.enable = true;
|
||||||
services.gitea.user = "git"; # default is 'gitea'
|
services.gitea.user = "git"; # default is 'gitea'
|
||||||
|
@ -13,10 +13,6 @@
|
||||||
services.gitea.appName = "Perfectly Sane Git";
|
services.gitea.appName = "Perfectly Sane Git";
|
||||||
# services.gitea.disableRegistration = true;
|
# services.gitea.disableRegistration = true;
|
||||||
|
|
||||||
services.gitea.database.createDatabase = false; #< silence warning which wants db user and name to be equal
|
|
||||||
# TODO: remove this after merge: <https://github.com/NixOS/nixpkgs/pull/268849>
|
|
||||||
services.gitea.database.socket = "/run/postgresql"; #< would have been set if createDatabase = true
|
|
||||||
|
|
||||||
# gitea doesn't create the git user
|
# gitea doesn't create the git user
|
||||||
users.users.git = {
|
users.users.git = {
|
||||||
description = "Gitea Service";
|
description = "Gitea Service";
|
||||||
|
@ -50,15 +46,9 @@
|
||||||
ENABLE_CAPTCHA = true;
|
ENABLE_CAPTCHA = true;
|
||||||
NOREPLY_ADDRESS = "noreply.anonymous.git@uninsane.org";
|
NOREPLY_ADDRESS = "noreply.anonymous.git@uninsane.org";
|
||||||
};
|
};
|
||||||
session = {
|
session.COOKIE_SECURE = true;
|
||||||
COOKIE_SECURE = true;
|
|
||||||
# keep me logged in for 30 days
|
|
||||||
SESSION_LIFE_TIME = 60 * 60 * 24 * 30;
|
|
||||||
};
|
|
||||||
repository = {
|
repository = {
|
||||||
DEFAULT_BRANCH = "master";
|
DEFAULT_BRANCH = "master";
|
||||||
ENABLE_PUSH_CREATE_USER = true;
|
|
||||||
ENABLE_PUSH_CREATE_ORG = true;
|
|
||||||
};
|
};
|
||||||
other = {
|
other = {
|
||||||
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false;
|
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false;
|
||||||
|
@ -96,8 +86,6 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.openssh.settings.UsePAM = true; #< required for `git` user to authenticate
|
|
||||||
|
|
||||||
# hosted git (web view and for `git <cmd>` use
|
# hosted git (web view and for `git <cmd>` use
|
||||||
# TODO: enable publog?
|
# TODO: enable publog?
|
||||||
services.nginx.virtualHosts."git.uninsane.org" = {
|
services.nginx.virtualHosts."git.uninsane.org" = {
|
||||||
|
@ -108,24 +96,6 @@
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://127.0.0.1:3000";
|
proxyPass = "http://127.0.0.1:3000";
|
||||||
};
|
};
|
||||||
# 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 = "http://127.0.0.1:3000";
|
|
||||||
extraConfig = ''
|
|
||||||
proxy_hide_header Content-Type;
|
|
||||||
default_type text/html;
|
|
||||||
add_header Content-Type text/html;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
locations."~ ^/colin/phone-case-cq/raw/.*.js" = {
|
|
||||||
proxyPass = "http://127.0.0.1:3000";
|
|
||||||
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.dns.zones."uninsane.org".inet.CNAME."git" = "native";
|
||||||
|
@ -133,7 +103,7 @@
|
||||||
sane.ports.ports."22" = {
|
sane.ports.ports."22" = {
|
||||||
protocol = [ "tcp" ];
|
protocol = [ "tcp" ];
|
||||||
visibleTo.lan = true;
|
visibleTo.lan = true;
|
||||||
visibleTo.doof = true;
|
visibleTo.wan = true;
|
||||||
description = "colin-git@git.uninsane.org";
|
description = "colin-git@git.uninsane.org";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
--ignore-panel=HOSTS \
|
--ignore-panel=HOSTS \
|
||||||
--ws-url=wss://sink.uninsane.org:443/ws \
|
--ws-url=wss://sink.uninsane.org:443/ws \
|
||||||
--port=7890 \
|
--port=7890 \
|
||||||
-o /var/lib/goaccess/index.html
|
-o /var/lib/uninsane/sink/index.html
|
||||||
'';
|
'';
|
||||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
|
@ -28,19 +28,17 @@
|
||||||
RestartSec = "10s";
|
RestartSec = "10s";
|
||||||
|
|
||||||
# hardening
|
# hardening
|
||||||
# TODO: run as `goaccess` user and add `goaccess` user to group `nginx`.
|
WorkingDirectory = "/tmp";
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
PrivateDevices = "yes";
|
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
ProtectHome = "read-only";
|
ProtectHome = "read-only";
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @privileged @reboot @resources @setuid @swap @raw-io";
|
||||||
|
ReadOnlyPaths = "/";
|
||||||
|
ReadWritePaths = [ "/proc/self" "/var/lib/uninsane/sink" ];
|
||||||
|
PrivateDevices = "yes";
|
||||||
ProtectKernelModules = "yes";
|
ProtectKernelModules = "yes";
|
||||||
ProtectKernelTunables = "yes";
|
ProtectKernelTunables = "yes";
|
||||||
ProtectSystem = "strict";
|
|
||||||
ReadOnlyPaths = [ "/var/log/nginx" ];
|
|
||||||
ReadWritePaths = [ "/proc/self" "/var/lib/goaccess" ];
|
|
||||||
StateDirectory = "goaccess";
|
|
||||||
SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @privileged @reboot @resources @setuid @swap @raw-io";
|
|
||||||
WorkingDirectory = "/var/lib/goaccess";
|
|
||||||
};
|
};
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
@ -51,7 +49,7 @@
|
||||||
addSSL = true;
|
addSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
# inherit kTLS;
|
# inherit kTLS;
|
||||||
root = "/var/lib/goaccess";
|
root = "/var/lib/uninsane/sink";
|
||||||
|
|
||||||
locations."/ws" = {
|
locations."/ws" = {
|
||||||
proxyPass = "http://127.0.0.1:7890";
|
proxyPass = "http://127.0.0.1:7890";
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
|
|
||||||
lib.mkIf false # i don't actively use ipfs anymore
|
lib.mkIf false # i don't actively use ipfs anymore
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
# TODO: mode? could be more granular
|
# TODO: mode? could be more granular
|
||||||
{ user = "261"; group = "261"; path = "/var/lib/ipfs"; method = "bind"; }
|
{ user = "261"; group = "261"; path = "/var/lib/ipfs"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 4001 ];
|
networking.firewall.allowedTCPPorts = [ 4001 ];
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
# TODO: mode? we only need this to save Indexer creds ==> migrate to config?
|
# TODO: mode? we only need this to save Indexer creds ==> migrate to config?
|
||||||
{ user = "root"; group = "root"; path = "/var/lib/jackett"; method = "bind"; }
|
{ user = "root"; group = "root"; path = "/var/lib/jackett"; }
|
||||||
];
|
];
|
||||||
services.jackett.enable = true;
|
services.jackett.enable = true;
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@
|
||||||
systemd.services.jackett.serviceConfig = {
|
systemd.services.jackett.serviceConfig = {
|
||||||
# run this behind the OVPN static VPN
|
# run this behind the OVPN static VPN
|
||||||
NetworkNamespacePath = "/run/netns/ovpns";
|
NetworkNamespacePath = "/run/netns/ovpns";
|
||||||
ExecStartPre = [ "${lib.getExe pkgs.sane-scripts.ip-check} --no-upnp --expect ${config.sane.netns.ovpns.netnsPubIpv4}" ]; # abort if public IP is not as expected
|
|
||||||
|
|
||||||
# patch jackett to listen on the public interfaces
|
# patch jackett to listen on the public interfaces
|
||||||
# ExecStart = lib.mkForce "${pkgs.jackett}/bin/Jackett --NoUpdates --DataFolder /var/lib/jackett/.config/Jackett --ListenPublic";
|
# ExecStart = lib.mkForce "${pkgs.jackett}/bin/Jackett --NoUpdates --DataFolder /var/lib/jackett/.config/Jackett --ListenPublic";
|
||||||
};
|
};
|
||||||
|
@ -24,7 +22,8 @@
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
# inherit kTLS;
|
# inherit kTLS;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://${config.sane.netns.ovpns.netnsVethIpv4}:9117";
|
# proxyPass = "http://ovpns.uninsane.org:9117";
|
||||||
|
proxyPass = "http://10.0.1.6:9117";
|
||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,8 +40,8 @@
|
||||||
description = "colin-jellyfin-https-lan";
|
description = "colin-jellyfin-https-lan";
|
||||||
};
|
};
|
||||||
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin"; method = "bind"; }
|
{ user = "jellyfin"; group = "jellyfin"; mode = "0700"; path = "/var/lib/jellyfin"; }
|
||||||
];
|
];
|
||||||
sane.fs."/var/lib/jellyfin/config/logging.json" = {
|
sane.fs."/var/lib/jellyfin/config/logging.json" = {
|
||||||
# "Emby.Dlna" logging: <https://jellyfin.org/docs/general/networking/dlna>
|
# "Emby.Dlna" logging: <https://jellyfin.org/docs/general/networking/dlna>
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
# Jellyfin multimedia server
|
# Jellyfin multimedia server
|
||||||
# this is mostly taken from the official jellfin.org docs
|
# this is mostly taken from the official jellfin.org docs
|
||||||
services.nginx.virtualHosts."jelly.uninsane.org" = {
|
services.nginx.virtualHosts."jelly.uninsane.org" = {
|
||||||
forceSSL = true;
|
addSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
# inherit kTLS;
|
# inherit kTLS;
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,9 @@
|
||||||
# how to update wikipedia snapshot:
|
|
||||||
# - browse for later snapshots:
|
|
||||||
# - <https://mirror.accum.se/mirror/wikimedia.org/other/kiwix/zim/wikipedia>
|
|
||||||
# - DL directly, or via rsync (resumable):
|
|
||||||
# - `rsync --progress --append-verify rsync://mirror.accum.se/mirror/wikimedia.org/other/kiwix/zim/wikipedia/wikipedia_en_all_maxi_2022-05.zim .`
|
|
||||||
|
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.ext = [
|
|
||||||
{ user = "colin"; group = "users"; path = "/var/lib/kiwix"; method = "bind"; }
|
|
||||||
];
|
|
||||||
|
|
||||||
sane.services.kiwix-serve = {
|
sane.services.kiwix-serve = {
|
||||||
enable = true;
|
enable = true;
|
||||||
port = 8013;
|
port = 8013;
|
||||||
zimPaths = [ "/var/lib/kiwix/wikipedia_en_all_maxi_2023-11.zim" ];
|
zimPaths = [ "/var/lib/uninsane/www-archive/wikipedia_en_all_maxi_2022-05.zim" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx.virtualHosts."w.uninsane.org" = {
|
services.nginx.virtualHosts."w.uninsane.org" = {
|
||||||
|
|
|
@ -4,15 +4,15 @@ let
|
||||||
inherit (svc-cfg) user group port stateDir;
|
inherit (svc-cfg) user group port stateDir;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ inherit user group; mode = "0700"; path = stateDir; method = "bind"; }
|
{ inherit user group; mode = "0700"; path = stateDir; }
|
||||||
];
|
];
|
||||||
|
|
||||||
services.komga.enable = true;
|
services.komga.enable = true;
|
||||||
services.komga.port = 11319; # chosen at random
|
services.komga.port = 11319; # chosen at random
|
||||||
|
|
||||||
services.nginx.virtualHosts."komga.uninsane.org" = {
|
services.nginx.virtualHosts."komga.uninsane.org" = {
|
||||||
forceSSL = true;
|
addSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
proxyPass = "http://127.0.0.1:${builtins.toString port}";
|
||||||
|
|
|
@ -3,28 +3,13 @@
|
||||||
# - <repo:LemmyNet/lemmy:docker/nginx.conf>
|
# - <repo:LemmyNet/lemmy:docker/nginx.conf>
|
||||||
# - <repo:LemmyNet/lemmy-ansible:templates/nginx.conf>
|
# - <repo:LemmyNet/lemmy-ansible:templates/nginx.conf>
|
||||||
|
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (builtins) toString;
|
inherit (builtins) toString;
|
||||||
inherit (lib) mkForce;
|
inherit (lib) mkForce;
|
||||||
uiPort = 1234; # default ui port is 1234
|
uiPort = 1234; # default ui port is 1234
|
||||||
backendPort = 8536; # default backend port is 8536
|
backendPort = 8536; # default backend port is 8536
|
||||||
#^ i guess the "backend" port is used for federation?
|
# - i guess the "backend" port is used for federation?
|
||||||
pict-rs = pkgs.pict-rs;
|
|
||||||
# pict-rs = pkgs.pict-rs.overrideAttrs (upstream: {
|
|
||||||
# # as of v0.4.2, all non-GIF video is forcibly transcoded.
|
|
||||||
# # that breaks lemmy, because of the request latency.
|
|
||||||
# # and it eats up hella CPU.
|
|
||||||
# # pict-rs is iffy around video altogether: mp4 seems the best supported.
|
|
||||||
# # XXX: this patch no longer applies after 0.5.10 -> 0.5.11 update.
|
|
||||||
# # git log is hard to parse, but *suggests* that video is natively supported
|
|
||||||
# # better than in the 0.4.2 days, e.g. 5fd59fc5b42d31559120dc28bfef4e5002fb509e
|
|
||||||
# # "Change commandline flag to allow disabling video, since it is enabled by default"
|
|
||||||
# postPatch = (upstream.postPatch or "") + ''
|
|
||||||
# substituteInPlace src/validate.rs \
|
|
||||||
# --replace-fail 'if transcode_options.needs_reencode() {' 'if false {'
|
|
||||||
# '';
|
|
||||||
# });
|
|
||||||
in {
|
in {
|
||||||
services.lemmy = {
|
services.lemmy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
@ -71,20 +56,4 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
sane.dns.zones."uninsane.org".inet.CNAME."lemmy" = "native";
|
sane.dns.zones."uninsane.org".inet.CNAME."lemmy" = "native";
|
||||||
|
|
||||||
#v DO NOT REMOVE: defaults to 0.3, instead of latest, so always need to explicitly set this.
|
|
||||||
services.pict-rs.package = pict-rs;
|
|
||||||
|
|
||||||
# pict-rs configuration is applied in this order:
|
|
||||||
# - 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>
|
|
||||||
systemd.services.pict-rs.serviceConfig.ExecStart = lib.mkForce (lib.concatStringsSep " " [
|
|
||||||
"${lib.getBin pict-rs}/bin/pict-rs run"
|
|
||||||
"--media-video-max-frame-count" (builtins.toString (30*60*60))
|
|
||||||
"--media-process-timeout 120"
|
|
||||||
"--media-video-allow-audio" # allow audio
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,6 @@
|
||||||
# docs: <https://nixos.wiki/wiki/Matrix>
|
# docs: <https://nixos.wiki/wiki/Matrix>
|
||||||
# docs: <https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-synapse>
|
# docs: <https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-synapse>
|
||||||
# example config: <https://github.com/matrix-org/synapse/blob/develop/docs/sample_config.yaml>
|
# example config: <https://github.com/matrix-org/synapse/blob/develop/docs/sample_config.yaml>
|
||||||
#
|
|
||||||
# ENABLING PUSH NOTIFICATIONS (with UnifiedPush/ntfy):
|
|
||||||
# - 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, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -20,73 +10,67 @@
|
||||||
./signal.nix
|
./signal.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/matrix-synapse"; method = "bind"; }
|
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/matrix-synapse"; }
|
||||||
];
|
];
|
||||||
services.matrix-synapse.enable = true;
|
services.matrix-synapse.enable = true;
|
||||||
services.matrix-synapse.settings = {
|
# this changes the default log level from INFO to WARN.
|
||||||
# this changes the default log level from INFO to WARN.
|
# maybe there's an easier way?
|
||||||
# maybe there's an easier way?
|
services.matrix-synapse.settings.log_config = ./synapse-log_level.yaml;
|
||||||
log_config = ./synapse-log_level.yaml;
|
services.matrix-synapse.settings.server_name = "uninsane.org";
|
||||||
server_name = "uninsane.org";
|
|
||||||
|
|
||||||
# services.matrix-synapse.enable_registration_captcha = true;
|
# services.matrix-synapse.enable_registration_captcha = true;
|
||||||
# services.matrix-synapse.enable_registration_without_verification = true;
|
# services.matrix-synapse.enable_registration_without_verification = true;
|
||||||
enable_registration = true;
|
services.matrix-synapse.settings.enable_registration = true;
|
||||||
# services.matrix-synapse.registration_shared_secret = "<shared key goes here>";
|
# services.matrix-synapse.registration_shared_secret = "<shared key goes here>";
|
||||||
|
|
||||||
# default for listeners is port = 8448, tls = true, x_forwarded = false.
|
# default for listeners is port = 8448, tls = true, x_forwarded = false.
|
||||||
# we change this because the server is situated behind nginx.
|
# we change this because the server is situated behind nginx.
|
||||||
listeners = [
|
services.matrix-synapse.settings.listeners = [
|
||||||
{
|
{
|
||||||
port = 8008;
|
port = 8008;
|
||||||
bind_addresses = [ "127.0.0.1" ];
|
bind_addresses = [ "127.0.0.1" ];
|
||||||
type = "http";
|
type = "http";
|
||||||
tls = false;
|
tls = false;
|
||||||
x_forwarded = true;
|
x_forwarded = true;
|
||||||
resources = [
|
resources = [
|
||||||
{
|
{
|
||||||
names = [ "client" "federation" ];
|
names = [ "client" "federation" ];
|
||||||
compress = false;
|
compress = false;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
ip_range_whitelist = [
|
services.matrix-synapse.settings.x_forwarded = true; # because we proxy matrix behind nginx
|
||||||
# to communicate with ntfy.uninsane.org push notifs.
|
services.matrix-synapse.settings.max_upload_size = "100M"; # default is "50M"
|
||||||
# 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
|
services.matrix-synapse.settings.admin_contact = "admin.matrix@uninsane.org";
|
||||||
max_upload_size = "100M"; # default is "50M"
|
services.matrix-synapse.settings.registrations_require_3pid = [ "email" ];
|
||||||
|
|
||||||
admin_contact = "admin.matrix@uninsane.org";
|
|
||||||
registrations_require_3pid = [ "email" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.matrix-synapse.extraConfigFiles = [
|
services.matrix-synapse.extraConfigFiles = [
|
||||||
config.sops.secrets."matrix_synapse_secrets.yaml".path
|
config.sops.secrets."matrix_synapse_secrets.yaml".path
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.matrix-synapse.postStart = ''
|
# services.matrix-synapse.extraConfigFiles = [builtins.toFile "matrix-synapse-extra-config" ''
|
||||||
ACCESS_TOKEN=$(${pkgs.coreutils}/bin/cat ${config.sops.secrets.matrix_access_token.path})
|
# admin_contact: "admin.matrix@uninsane.org"
|
||||||
TOPIC=$(${pkgs.coreutils}/bin/cat ${config.sops.secrets.ntfy-sh-topic.path})
|
# registrations_require_3pid:
|
||||||
|
# - email
|
||||||
echo "ensuring ntfy push gateway"
|
# email:
|
||||||
${pkgs.curl}/bin/curl \
|
# smtp_host: "mx.uninsane.org"
|
||||||
--header "Authorization: Bearer $ACCESS_TOKEN" \
|
# smtp_port: 587
|
||||||
--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\" }" \
|
# smtp_user: "matrix-synapse"
|
||||||
localhost:8008/_matrix/client/v3/pushers/set
|
# smtp_pass: "${secrets.matrix-synapse.smtp_pass}"
|
||||||
|
# require_transport_security: true
|
||||||
echo "registered push gateways:"
|
# enable_tls: true
|
||||||
${pkgs.curl}/bin/curl \
|
# notif_from: "%(app)s <notify.matrix@uninsane.org>"
|
||||||
--header "Authorization: Bearer $ACCESS_TOKEN" \
|
# app_name: "Uninsane Matrix"
|
||||||
localhost:8008/_matrix/client/v3/pushers \
|
# enable_notifs: true
|
||||||
| ${pkgs.jq}/bin/jq .
|
# validation_token_lifetime: 96h
|
||||||
'';
|
# invite_client_location: "https://web.matrix.uninsane.org"
|
||||||
|
# subjects:
|
||||||
|
# email_validation: "[%(server_name)s] Validate your email"
|
||||||
|
# ''];
|
||||||
|
|
||||||
# new users may be registered on the CLI:
|
# new users may be registered on the CLI:
|
||||||
# register_new_matrix_user -c /nix/store/8n6kcka37jhmi4qpd2r03aj71pkyh21s-homeserver.yaml http://localhost:8008
|
# register_new_matrix_user -c /nix/store/8n6kcka37jhmi4qpd2r03aj71pkyh21s-homeserver.yaml http://localhost:8008
|
||||||
|
@ -157,9 +141,4 @@
|
||||||
sops.secrets."matrix_synapse_secrets.yaml" = {
|
sops.secrets."matrix_synapse_secrets.yaml" = {
|
||||||
owner = config.users.users.matrix-synapse.name;
|
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 = [ "ntfy-sh" ];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
# - recommended to use mautrix-discord: <https://github.com/NixOS/nixpkgs/pull/200462>
|
# - recommended to use mautrix-discord: <https://github.com/NixOS/nixpkgs/pull/200462>
|
||||||
lib.mkIf false
|
lib.mkIf false
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/mx-puppet-discord"; method = "bind"; }
|
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/mx-puppet-discord"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
services.matrix-synapse.settings.app_service_config_files = [
|
services.matrix-synapse.settings.app_service_config_files = [
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
ircServer = { name, additionalAddresses ? [], ssl ? true, sasl ? true, port ? if ssl then 6697 else 6667 }: let
|
ircServer = { name, additionalAddresses ? [], sasl ? true, port ? 6697 }: let
|
||||||
lowerName = lib.toLower name;
|
lowerName = lib.toLower name;
|
||||||
in {
|
in {
|
||||||
# XXX sasl: appservice doesn't support NickServ identification (only SASL, or PASS if sasl = false)
|
# XXX sasl: appservice doesn't support NickServ identification (only SASL, or PASS if sasl = false)
|
||||||
inherit additionalAddresses name port sasl ssl;
|
inherit name additionalAddresses sasl port;
|
||||||
|
ssl = true;
|
||||||
botConfig = {
|
botConfig = {
|
||||||
# bot has no presence in IRC channel; only real Matrix users
|
# bot has no presence in IRC channel; only real Matrix users
|
||||||
enabled = false;
|
enabled = false;
|
||||||
|
@ -100,9 +101,9 @@ in
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
# TODO: mode?
|
# TODO: mode?
|
||||||
{ user = "matrix-appservice-irc"; group = "matrix-appservice-irc"; path = "/var/lib/matrix-appservice-irc"; method = "bind"; }
|
{ user = "matrix-appservice-irc"; group = "matrix-appservice-irc"; path = "/var/lib/matrix-appservice-irc"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
# XXX: matrix-appservice-irc PreStart tries to chgrp the registration.yml to matrix-synapse,
|
# XXX: matrix-appservice-irc PreStart tries to chgrp the registration.yml to matrix-synapse,
|
||||||
|
@ -140,7 +141,6 @@ in
|
||||||
sasl = false;
|
sasl = false;
|
||||||
# notable channels:
|
# notable channels:
|
||||||
# - #hare
|
# - #hare
|
||||||
# - #mnt-reform
|
|
||||||
};
|
};
|
||||||
"irc.myanonamouse.net" = ircServer {
|
"irc.myanonamouse.net" = ircServer {
|
||||||
name = "MyAnonamouse";
|
name = "MyAnonamouse";
|
||||||
|
@ -155,10 +155,6 @@ in
|
||||||
# - #sxmo-offtopic
|
# - #sxmo-offtopic
|
||||||
};
|
};
|
||||||
"irc.rizon.net" = ircServer { name = "Rizon"; };
|
"irc.rizon.net" = ircServer { name = "Rizon"; };
|
||||||
"wigle.net" = ircServer {
|
|
||||||
name = "WiGLE";
|
|
||||||
ssl = false;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
# config options:
|
# config options:
|
||||||
# - <https://github.com/mautrix/signal/blob/master/mautrix_signal/example-config.yaml>
|
# - <https://github.com/mautrix/signal/blob/master/mautrix_signal/example-config.yaml>
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, 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.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; method = "bind"; }
|
{ user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; }
|
||||||
{ user = "signald"; group = "signald"; path = "/var/lib/signald"; method = "bind"; }
|
{ user = "signald"; group = "signald"; path = "/var/lib/signald"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
# allow synapse to read the registration file
|
# allow synapse to read the registration file
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
|
|
||||||
lib.mkIf false #< i don't actively use navidrome
|
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "navidrome"; group = "navidrome"; path = "/var/lib/navidrome"; method = "bind"; }
|
{ user = "navidrome"; group = "navidrome"; path = "/var/lib/navidrome"; }
|
||||||
];
|
];
|
||||||
services.navidrome.enable = true;
|
services.navidrome.enable = true;
|
||||||
services.navidrome.settings = {
|
services.navidrome.settings = {
|
||||||
# docs: https://www.navidrome.org/docs/usage/configuration-options/
|
# docs: https://www.navidrome.org/docs/usage/configuration-options/
|
||||||
Address = "127.0.0.1";
|
Address = "127.0.0.1";
|
||||||
Port = 4533;
|
Port = 4533;
|
||||||
MusicFolder = "/var/media/Music";
|
MusicFolder = "/var/lib/uninsane/media/Music";
|
||||||
CovertArtPriority = "*.jpg, *.JPG, *.png, *.PNG, embedded";
|
CovertArtPriority = "*.jpg, *.JPG, *.png, *.PNG, embedded";
|
||||||
AutoImportPlaylists = false;
|
AutoImportPlaylists = false;
|
||||||
ScanSchedule = "@every 1h";
|
ScanSchedule = "@every 1h";
|
||||||
|
|
67
hosts/by-name/servo/services/nfs.nix
Normal file
67
hosts/by-name/servo/services/nfs.nix
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# docs:
|
||||||
|
# - <https://nixos.wiki/wiki/NFS>
|
||||||
|
# - <https://wiki.gentoo.org/wiki/Nfs-utils>
|
||||||
|
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
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" ];
|
||||||
|
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;
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# - mountpoint[=/path]: only export the directory if it's a mountpoint. used to avoid exporting failed mounts.
|
||||||
|
#
|
||||||
|
# 10.0.0.0/8 to export (readonly) both to LAN (unencrypted) and wg vpn (encrypted)
|
||||||
|
services.nfs.server.exports = ''
|
||||||
|
/var/nfs/export 10.78.79.0/22(ro,crossmnt,fsid=0,subtree_check) 10.0.10.0/24(rw,no_root_squash,crossmnt,fsid=0,subtree_check)
|
||||||
|
'';
|
||||||
|
|
||||||
|
fileSystems."/var/nfs/export/media" = {
|
||||||
|
# everything in here could be considered publicly readable (based on the viewer's legal jurisdiction)
|
||||||
|
device = "/var/lib/uninsane/media";
|
||||||
|
options = [ "rbind" ];
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
# docs: <https://nixos.wiki/wiki/Nginx>
|
# docs: https://nixos.wiki/wiki/Nginx
|
||||||
# docs: <https://nginx.org/en/docs/>
|
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -17,14 +16,14 @@ in
|
||||||
sane.ports.ports."80" = {
|
sane.ports.ports."80" = {
|
||||||
protocol = [ "tcp" ];
|
protocol = [ "tcp" ];
|
||||||
visibleTo.lan = true;
|
visibleTo.lan = true;
|
||||||
visibleTo.ovpns = true; # so that letsencrypt can procure a cert for the mx record
|
visibleTo.wan = true;
|
||||||
visibleTo.doof = true;
|
visibleTo.ovpn = true; # so that letsencrypt can procure a cert for the mx record
|
||||||
description = "colin-http-uninsane.org";
|
description = "colin-http-uninsane.org";
|
||||||
};
|
};
|
||||||
sane.ports.ports."443" = {
|
sane.ports.ports."443" = {
|
||||||
protocol = [ "tcp" ];
|
protocol = [ "tcp" ];
|
||||||
visibleTo.lan = true;
|
visibleTo.lan = true;
|
||||||
visibleTo.doof = true;
|
visibleTo.wan = true;
|
||||||
description = "colin-https-uninsane.org";
|
description = "colin-https-uninsane.org";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,10 +53,8 @@ in
|
||||||
services.nginx.recommendedOptimisation = true;
|
services.nginx.recommendedOptimisation = true;
|
||||||
|
|
||||||
# web blog/personal site
|
# web blog/personal site
|
||||||
# alternative way to link stuff into the share:
|
|
||||||
# sane.fs."/var/www/sites/uninsane.org/share/Ubunchu".mount.bind = "/var/media/Books/Visual/HiroshiSeo/Ubunchu";
|
|
||||||
# sane.fs."/var/media/Books/Visual/HiroshiSeo/Ubunchu".dir = {};
|
|
||||||
services.nginx.virtualHosts."uninsane.org" = publog {
|
services.nginx.virtualHosts."uninsane.org" = publog {
|
||||||
|
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
|
||||||
# a lot of places hardcode https://uninsane.org,
|
# a lot of places hardcode https://uninsane.org,
|
||||||
# and then when we mix http + non-https, we get CORS violations
|
# and then when we mix http + non-https, we get CORS violations
|
||||||
# and things don't look right. so force SSL.
|
# and things don't look right. so force SSL.
|
||||||
|
@ -67,38 +64,9 @@ in
|
||||||
# for OCSP stapling
|
# for OCSP stapling
|
||||||
sslTrustedCertificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
sslTrustedCertificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||||
|
|
||||||
locations."/" = {
|
# uninsane.org/share/foo => /var/lib/uninsane/root/share/foo.
|
||||||
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
|
# yes, nginx does not strip the prefix when evaluating against the root.
|
||||||
tryFiles = "$uri $uri/ @fallback";
|
locations."/share".root = "/var/lib/uninsane/root";
|
||||||
};
|
|
||||||
|
|
||||||
# unversioned files
|
|
||||||
locations."@fallback" = {
|
|
||||||
root = "/var/www/sites/uninsane.org";
|
|
||||||
};
|
|
||||||
|
|
||||||
# uninsane.org/share/foo => /var/www/sites/uninsane.org/share/foo.
|
|
||||||
# special-cased to enable directory listings
|
|
||||||
locations."/share" = {
|
|
||||||
root = "/var/www/sites/uninsane.org";
|
|
||||||
extraConfig = ''
|
|
||||||
# autoindex => render directory listings
|
|
||||||
autoindex on;
|
|
||||||
# don't follow any symlinks when serving files
|
|
||||||
# otherwise it allows a directory escape
|
|
||||||
disable_symlinks on;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
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;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org
|
# allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org
|
||||||
locations."= /.well-known/matrix/server".extraConfig =
|
locations."= /.well-known/matrix/server".extraConfig =
|
||||||
|
@ -139,19 +107,6 @@ in
|
||||||
# proxyPass = "http://127.0.0.1:4000";
|
# proxyPass = "http://127.0.0.1:4000";
|
||||||
# extraConfig = pleromaExtraConfig;
|
# 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;";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,9 +133,10 @@ in
|
||||||
security.acme.acceptTerms = true;
|
security.acme.acceptTerms = true;
|
||||||
security.acme.defaults.email = "admin.acme@uninsane.org";
|
security.acme.defaults.email = "admin.acme@uninsane.org";
|
||||||
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "acme"; group = "acme"; path = "/var/lib/acme"; method = "bind"; }
|
# TODO: mode?
|
||||||
{ user = "colin"; group = "users"; path = "/var/www/sites"; method = "bind"; }
|
{ user = "acme"; group = "acme"; path = "/var/lib/acme"; }
|
||||||
|
{ user = "colin"; group = "users"; path = "/var/www/sites"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
# let's encrypt default chain looks like:
|
# let's encrypt default chain looks like:
|
||||||
|
|
|
@ -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 /tmp/nixos-prebuild.XXXXXX)
|
|
||||||
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";
|
|
||||||
};
|
|
||||||
}
|
|
21
hosts/by-name/servo/services/nixserve.nix
Normal file
21
hosts/by-name/servo/services/nixserve.nix
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{ config, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
services.nginx.virtualHosts."nixcache.uninsane.org" = {
|
||||||
|
addSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
# inherit kTLS;
|
||||||
|
# serverAliases = [ "nixcache" ];
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
proxy_pass http://localhost:${toString config.services.nix-serve.port};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
sane.dns.zones."uninsane.org".inet.CNAME."nixcache" = "native";
|
||||||
|
|
||||||
|
sane.services.nixserve.enable = true;
|
||||||
|
sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path;
|
||||||
|
}
|
|
@ -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, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./ntfy-waiter.nix
|
|
||||||
./ntfy-sh.nix
|
|
||||||
];
|
|
||||||
sops.secrets."ntfy-sh-topic" = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = config.users.users.ntfy-sh.name;
|
|
||||||
group = config.users.users.ntfy-sh.name;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,92 +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
|
|
||||||
{
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
|
||||||
# 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 = "trace"; # 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})
|
|
||||||
${pkgs.ntfy-sh}/bin/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 "python3.withPackages (ps: [ ])" -p ntfy-sh
|
|
||||||
|
|
||||||
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 = [
|
|
||||||
"${cfg.package}/bin/ntfy-waiter"
|
|
||||||
"--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 = [ "default.target" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options = with lib; {
|
|
||||||
sane.ntfy-waiter.enable = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
sane.ntfy-waiter.verbose = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
sane.ntfy-waiter.package = mkOption {
|
|
||||||
type = types.package;
|
|
||||||
default = pkgs.static-nix-shell.mkPython3Bin {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -5,8 +5,8 @@ let
|
||||||
cfg = config.services.pict-rs;
|
cfg = config.services.pict-rs;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = lib.mkIf cfg.enable [
|
sane.persist.sys.plaintext = lib.mkIf cfg.enable [
|
||||||
{ user = "pict-rs"; group = "pict-rs"; path = cfg.dataDir; method = "bind"; }
|
{ user = "pict-rs"; group = "pict-rs"; path = cfg.dataDir; }
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.pict-rs.serviceConfig = {
|
systemd.services.pict-rs.serviceConfig = {
|
||||||
|
|
|
@ -14,8 +14,8 @@ let
|
||||||
# logLevel = "debug";
|
# logLevel = "debug";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
{ user = "pleroma"; group = "pleroma"; path = "/var/lib/pleroma"; method = "bind"; }
|
{ user = "pleroma"; group = "pleroma"; path = "/var/lib/pleroma"; }
|
||||||
];
|
];
|
||||||
services.pleroma.enable = true;
|
services.pleroma.enable = true;
|
||||||
services.pleroma.secretConfigFile = config.sops.secrets.pleroma_secrets.path;
|
services.pleroma.secretConfigFile = config.sops.secrets.pleroma_secrets.path;
|
||||||
|
@ -25,7 +25,7 @@ in
|
||||||
|
|
||||||
config :pleroma, Pleroma.Web.Endpoint,
|
config :pleroma, Pleroma.Web.Endpoint,
|
||||||
url: [host: "fed.uninsane.org", scheme: "https", port: 443],
|
url: [host: "fed.uninsane.org", scheme: "https", port: 443],
|
||||||
http: [ip: {127, 0, 0, 1}, port: 4040]
|
http: [ip: {127, 0, 0, 1}, port: 4000]
|
||||||
# secret_key_base: "{secrets.pleroma.secret_key_base}",
|
# secret_key_base: "{secrets.pleroma.secret_key_base}",
|
||||||
# signing_salt: "{secrets.pleroma.signing_salt}"
|
# signing_salt: "{secrets.pleroma.signing_salt}"
|
||||||
|
|
||||||
|
@ -63,7 +63,6 @@ in
|
||||||
database: "pleroma",
|
database: "pleroma",
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
pool_size: 10,
|
pool_size: 10,
|
||||||
prepare: :named,
|
|
||||||
parameters: [
|
parameters: [
|
||||||
plan_cache_mode: "force_custom_plan"
|
plan_cache_mode: "force_custom_plan"
|
||||||
]
|
]
|
||||||
|
@ -167,7 +166,7 @@ in
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
# inherit kTLS;
|
# inherit kTLS;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://127.0.0.1:4040";
|
proxyPass = "http://127.0.0.1:4000";
|
||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
# documented: https://git.pleroma.social/pleroma/pleroma/-/blob/develop/installation/pleroma.nginx
|
# documented: https://git.pleroma.social/pleroma/pleroma/-/blob/develop/installation/pleroma.nginx
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
|
|
|
@ -1,39 +1,12 @@
|
||||||
{ pkgs, ... }:
|
{ ... }:
|
||||||
|
|
||||||
let
|
|
||||||
GiB = n: MiB 1024*n;
|
|
||||||
MiB = n: KiB 1024*n;
|
|
||||||
KiB = n: 1024*n;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
sane.persist.sys.byStore.plaintext = [
|
sane.persist.sys.plaintext = [
|
||||||
# TODO: mode?
|
# TODO: mode?
|
||||||
{ user = "postgres"; group = "postgres"; path = "/var/lib/postgresql"; method = "bind"; }
|
{ user = "postgres"; group = "postgres"; path = "/var/lib/postgresql"; }
|
||||||
];
|
];
|
||||||
services.postgresql.enable = true;
|
services.postgresql.enable = true;
|
||||||
|
# services.postgresql.dataDir = "/opt/postgresql/13";
|
||||||
# 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`
|
|
||||||
# - restart dependent services (maybe test one at a time)
|
|
||||||
|
|
||||||
services.postgresql.package = pkgs.postgresql_15;
|
|
||||||
|
|
||||||
|
|
||||||
# XXX colin: for a proper deploy, we'd want to include something for Pleroma here too.
|
# XXX colin: for a proper deploy, we'd want to include something for Pleroma here too.
|
||||||
# services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
|
# services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
|
||||||
# CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD '<password goes here>';
|
# CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD '<password goes here>';
|
||||||
|
@ -44,33 +17,10 @@ in
|
||||||
# LC_CTYPE = "C";
|
# LC_CTYPE = "C";
|
||||||
# '';
|
# '';
|
||||||
|
|
||||||
# perf tuning
|
# TODO: perf tuning
|
||||||
# - for recommended values see: <https://pgtune.leopard.in.ua/>
|
# - 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>
|
# - for official docs (sparse), see: <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
|
||||||
services.postgresql.settings = {
|
# services.postgresql.settings = { ... }
|
||||||
# DB Version: 15
|
|
||||||
# OS Type: linux
|
|
||||||
# DB Type: web
|
|
||||||
# Total Memory (RAM): 32 GB
|
|
||||||
# CPUs num: 12
|
|
||||||
# Data Storage: ssd
|
|
||||||
max_connections = 200;
|
|
||||||
shared_buffers = "8GB";
|
|
||||||
effective_cache_size = "24GB";
|
|
||||||
maintenance_work_mem = "2GB";
|
|
||||||
checkpoint_completion_target = 0.9;
|
|
||||||
wal_buffers = "16MB";
|
|
||||||
default_statistics_target = 100;
|
|
||||||
random_page_cost = 1.1;
|
|
||||||
effective_io_concurrency = 200;
|
|
||||||
work_mem = "10485kB";
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
# daily backups to /var/backup
|
# daily backups to /var/backup
|
||||||
services.postgresqlBackup.enable = true;
|
services.postgresqlBackup.enable = true;
|
||||||
|
|
81
hosts/by-name/servo/services/prosody.nix
Normal file
81
hosts/by-name/servo/services/prosody.nix
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# example configs:
|
||||||
|
# - <https://github.com/kittywitch/nixfiles/blob/main/services/prosody.nix>
|
||||||
|
# create users with:
|
||||||
|
# - `sudo -u prosody prosodyctl adduser colin@uninsane.org`
|
||||||
|
|
||||||
|
{ lib, ... }:
|
||||||
|
|
||||||
|
# XXX disabled: doesn't send messages to nixnet.social (only receives them).
|
||||||
|
# nixnet runs ejabberd, so revisiting that.
|
||||||
|
lib.mkIf false
|
||||||
|
{
|
||||||
|
sane.persist.sys.plaintext = [
|
||||||
|
{ user = "prosody"; group = "prosody"; path = "/var/lib/prosody"; }
|
||||||
|
];
|
||||||
|
sane.ports.ports."5222" = {
|
||||||
|
protocol = [ "tcp" ];
|
||||||
|
visibleTo.lan = true;
|
||||||
|
visibleTo.wan = true;
|
||||||
|
description = "colin-xmpp-client-to-server";
|
||||||
|
};
|
||||||
|
sane.ports.ports."5269" = {
|
||||||
|
protocol = [ "tcp" ];
|
||||||
|
visibleTo.wan = true;
|
||||||
|
description = "colin-xmpp-server-to-server";
|
||||||
|
};
|
||||||
|
sane.ports.ports."5280" = {
|
||||||
|
protocol = [ "tcp" ];
|
||||||
|
visibleTo.lan = true;
|
||||||
|
visibleTo.wan = true;
|
||||||
|
description = "colin-xmpp-bosh";
|
||||||
|
};
|
||||||
|
sane.ports.ports."5281" = {
|
||||||
|
protocol = [ "tcp" ];
|
||||||
|
visibleTo.lan = true;
|
||||||
|
visibleTo.wan = true;
|
||||||
|
description = "colin-xmpp-prosody-https"; # necessary?
|
||||||
|
};
|
||||||
|
|
||||||
|
# provide access to certs
|
||||||
|
users.users.prosody.extraGroups = [ "nginx" ];
|
||||||
|
|
||||||
|
security.acme.certs."uninsane.org".extraDomainNames = [
|
||||||
|
"conference.xmpp.uninsane.org"
|
||||||
|
"upload.xmpp.uninsane.org"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.prosody = {
|
||||||
|
enable = true;
|
||||||
|
admins = [ "colin@uninsane.org" ];
|
||||||
|
# allowRegistration = false;
|
||||||
|
# extraConfig = ''
|
||||||
|
# s2s_require_encryption = true
|
||||||
|
# c2s_require_encryption = true
|
||||||
|
# '';
|
||||||
|
|
||||||
|
extraModules = [ "private" "vcard" "privacy" "compression" "component" "muc" "pep" "adhoc" "lastactivity" "admin_adhoc" "blocklist"];
|
||||||
|
|
||||||
|
ssl.cert = "/var/lib/acme/uninsane.org/fullchain.pem";
|
||||||
|
ssl.key = "/var/lib/acme/uninsane.org/key.pem";
|
||||||
|
|
||||||
|
muc = [
|
||||||
|
{
|
||||||
|
domain = "conference.xmpp.uninsane.org";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
uploadHttp.domain = "upload.xmpp.uninsane.org";
|
||||||
|
|
||||||
|
virtualHosts = {
|
||||||
|
localhost = {
|
||||||
|
domain = "localhost";
|
||||||
|
enabled = true;
|
||||||
|
};
|
||||||
|
"xmpp.uninsane.org" = {
|
||||||
|
domain = "uninsane.org";
|
||||||
|
enabled = true;
|
||||||
|
ssl.cert = "/var/lib/acme/uninsane.org/fullchain.pem";
|
||||||
|
ssl.key = "/var/lib/acme/uninsane.org/key.pem";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,289 +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:
|
|
||||||
# - 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"
|
|
||||||
|
|
||||||
{ lib, pkgs, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
# enables very verbose logging
|
|
||||||
enableDebug = false;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
|
||||||
{ 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
|
|
||||||
];
|
|
||||||
|
|
||||||
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>
|
|
||||||
sane.fs."/etc/prosody/certs/uninsane.org/fullchain.pem" = {
|
|
||||||
symlink.target = "/var/lib/acme/uninsane.org/fullchain.pem";
|
|
||||||
wantedBeforeBy = [ "prosody.service" ];
|
|
||||||
};
|
|
||||||
sane.fs."/etc/prosody/certs/uninsane.org/privkey.pem" = {
|
|
||||||
symlink.target = "/var/lib/acme/uninsane.org/key.pem";
|
|
||||||
wantedBeforeBy = [ "prosody.service" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.prosody = {
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
uploadHttp.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"
|
|
||||||
"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 = assert(io.open(file, "rb"))
|
|
||||||
local content = f:read("*all")
|
|
||||||
f:close()
|
|
||||||
-- remove trailing newline
|
|
||||||
return 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 /etc/prosody/certs: cannot open /etc/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"
|
|
||||||
|
|
||||||
c2s_direct_tls_ports = { 5223 }
|
|
||||||
s2s_direct_tls_ports = { 5270 }
|
|
||||||
|
|
||||||
turn_external_host = "turn.uninsane.org"
|
|
||||||
turn_external_secret = readAll("/var/lib/coturn/shared_secret.bin")
|
|
||||||
-- turn_external_user = "prosody"
|
|
||||||
|
|
||||||
-- legacy mod_turncredentials integration
|
|
||||||
-- turncredentials_host = "turn.uninsane.org"
|
|
||||||
-- turncredentials_secret = readAll("/var/lib/coturn/shared_secret.bin")
|
|
||||||
|
|
||||||
ntfy_binary = "${pkgs.ntfy-sh}/bin/ntfy"
|
|
||||||
ntfy_topic = readAll("/run/secrets/ntfy-sh-topic")
|
|
||||||
|
|
||||||
-- s2s_require_encryption = true
|
|
||||||
-- c2s_require_encryption = true
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -1,79 +0,0 @@
|
||||||
# Soulseek daemon (p2p file sharing with an emphasis on Music)
|
|
||||||
# docs: <https://github.com/slskd/slskd/blob/master/docs/config.md>
|
|
||||||
#
|
|
||||||
# config precedence (higher precedence overrules lower precedence):
|
|
||||||
# - Default Values < Environment Variables < YAML Configuraiton File < Command Line Arguments
|
|
||||||
#
|
|
||||||
# debugging:
|
|
||||||
# - soulseek is just *flaky*. if you see e.g. DNS errors, even though you can't replicate them via `dig` or `getent ahostsv4`, just give it 10 minutes to work out:
|
|
||||||
# - "Soulseek.AddressException: Failed to resolve address 'vps.slsknet.org': Resource temporarily unavailable"
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
|
||||||
{ user = "slskd"; group = "media"; path = "/var/lib/slskd"; method = "bind"; }
|
|
||||||
];
|
|
||||||
sops.secrets."slskd_env" = {
|
|
||||||
owner = config.users.users.slskd.name;
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.slskd.extraGroups = [ "media" ];
|
|
||||||
|
|
||||||
sane.ports.ports."50300" = {
|
|
||||||
protocol = [ "tcp" ];
|
|
||||||
# visibleTo.ovpns = true; #< not needed: it runs in the ovpns namespace
|
|
||||||
description = "colin-soulseek";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.dns.zones."uninsane.org".inet.CNAME."soulseek" = "native";
|
|
||||||
|
|
||||||
services.nginx.virtualHosts."soulseek.uninsane.org" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://${config.sane.netns.ovpns.netnsVethIpv4}:5030";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.slskd.enable = true;
|
|
||||||
services.slskd.domain = null; # i'll manage nginx for it
|
|
||||||
services.slskd.group = "media";
|
|
||||||
# env file, for auth (SLSKD_SLSK_PASSWORD, SLSKD_SLSK_USERNAME)
|
|
||||||
services.slskd.environmentFile = config.sops.secrets.slskd_env.path;
|
|
||||||
services.slskd.settings = {
|
|
||||||
soulseek.diagnostic_level = "Debug"; # one of "None"|"Warning"|"Info"|"Debug"
|
|
||||||
shares.directories = [
|
|
||||||
# folders to share
|
|
||||||
# syntax: <https://github.com/slskd/slskd/blob/master/docs/config.md#directories>
|
|
||||||
# [Alias]/path/on/disk
|
|
||||||
# NOTE: Music library is quick to scan; videos take a solid 10min to scan.
|
|
||||||
# TODO: re-enable the other libraries
|
|
||||||
# "[Audioooks]/var/media/Books/Audiobooks"
|
|
||||||
# "[Books]/var/media/Books/Books"
|
|
||||||
# "[Manga]/var/media/Books/Visual"
|
|
||||||
# "[games]/var/media/games"
|
|
||||||
"[Music]/var/media/Music"
|
|
||||||
# "[Film]/var/media/Videos/Film"
|
|
||||||
# "[Shows]/var/media/Videos/Shows"
|
|
||||||
];
|
|
||||||
# directories.downloads = "..." # TODO
|
|
||||||
# directories.incomplete = "..." # TODO
|
|
||||||
# what unit is this? kbps??
|
|
||||||
global.upload.speed_limit = 32000;
|
|
||||||
web.logging = true;
|
|
||||||
# debug = true;
|
|
||||||
flags.no_logo = true; # don't show logo at start
|
|
||||||
# flags.volatile = true; # store searches and active transfers in RAM (completed transfers still go to disk). rec for btrfs/zfs
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.slskd.serviceConfig = {
|
|
||||||
# run this behind the OVPN static VPN
|
|
||||||
NetworkNamespacePath = "/run/netns/ovpns";
|
|
||||||
ExecStartPre = [ "${lib.getExe pkgs.sane-scripts.ip-check} --no-upnp --expect ${config.sane.netns.ovpns.netnsPubIpv4}" ]; # abort if public IP is not as expected
|
|
||||||
|
|
||||||
Restart = lib.mkForce "always"; # exits "success" when it fails to connect to soulseek server
|
|
||||||
RestartSec = "60s";
|
|
||||||
};
|
|
||||||
}
|
|
80
hosts/by-name/servo/services/transmission.nix
Normal file
80
hosts/by-name/servo/services/transmission.nix
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
sane.persist.sys.plaintext = [
|
||||||
|
# TODO: mode? we need this specifically for the stats tracking in .config/
|
||||||
|
{ user = "transmission"; group = "transmission"; path = "/var/lib/transmission"; }
|
||||||
|
];
|
||||||
|
services.transmission.enable = true;
|
||||||
|
services.transmission.settings = {
|
||||||
|
rpc-bind-address = "0.0.0.0";
|
||||||
|
#rpc-host-whitelist = "bt.uninsane.org";
|
||||||
|
#rpc-whitelist = "*.*.*.*";
|
||||||
|
rpc-authentication-required = true;
|
||||||
|
rpc-username = "colin";
|
||||||
|
# salted pw. to regenerate, set this plaintext, run nixos-rebuild, and then find the salted pw in:
|
||||||
|
# /var/lib/transmission/.config/transmission-daemon/settings.json
|
||||||
|
rpc-password = "{503fc8928344f495efb8e1f955111ca5c862ce0656SzQnQ5";
|
||||||
|
rpc-whitelist-enabled = false;
|
||||||
|
|
||||||
|
# download-dir = "/opt/uninsane/media/";
|
||||||
|
# hopefully, make the downloads world-readable
|
||||||
|
umask = 0;
|
||||||
|
|
||||||
|
# force peer connections to be encrypted
|
||||||
|
encryption = 2;
|
||||||
|
|
||||||
|
# units in kBps
|
||||||
|
speed-limit-down = 3000;
|
||||||
|
speed-limit-down-enabled = true;
|
||||||
|
speed-limit-up = 600;
|
||||||
|
speed-limit-up-enabled = true;
|
||||||
|
|
||||||
|
# see: https://git.zknt.org/mirror/transmission/commit/cfce6e2e3a9b9d31a9dafedd0bdc8bf2cdb6e876?lang=bg-BG
|
||||||
|
anti-brute-force-enabled = false;
|
||||||
|
|
||||||
|
download-dir = "/var/lib/uninsane/media";
|
||||||
|
incomplete-dir = "/var/lib/uninsane/media/incomplete";
|
||||||
|
|
||||||
|
};
|
||||||
|
# transmission will by default not allow the world to read its files.
|
||||||
|
services.transmission.downloadDirPermissions = "775";
|
||||||
|
|
||||||
|
systemd.services.transmission.after = [ "wireguard-wg-ovpns.service" ];
|
||||||
|
systemd.services.transmission.partOf = [ "wireguard-wg-ovpns.service" ];
|
||||||
|
systemd.services.transmission.serviceConfig = {
|
||||||
|
# run this behind the OVPN static VPN
|
||||||
|
NetworkNamespacePath = "/run/netns/ovpns";
|
||||||
|
LogLevelMax = "warning";
|
||||||
|
};
|
||||||
|
|
||||||
|
# service to automatically backup torrents i add to transmission
|
||||||
|
systemd.services.backup-torrents = {
|
||||||
|
description = "archive torrents to storage not owned by transmission";
|
||||||
|
script = ''
|
||||||
|
${pkgs.rsync}/bin/rsync -arv /var/lib/transmission/.config/transmission-daemon/torrents/ /var/backup/torrents/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
systemd.timers.backup-torrents = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnStartupSec = "11min";
|
||||||
|
OnUnitActiveSec = "240min";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# transmission web client
|
||||||
|
services.nginx.virtualHosts."bt.uninsane.org" = {
|
||||||
|
# basicAuth is literally cleartext user/pw, so FORCE this to happen over SSL
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
# inherit kTLS;
|
||||||
|
locations."/" = {
|
||||||
|
# proxyPass = "http://ovpns.uninsane.org:9091";
|
||||||
|
proxyPass = "http://10.0.1.6:9091";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sane.dns.zones."uninsane.org".inet.CNAME."bt" = "native";
|
||||||
|
}
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
# 2023/09/06: nixpkgs `transmission` defaults to old 3.00
|
|
||||||
# 2024/02/15: some torrent trackers whitelist clients; everyone is still on 3.00 for some reason :|
|
|
||||||
# some do this via peer-id (e.g. baka); others via user-agent (e.g. MAM).
|
|
||||||
# peer-id format is essentially the same between 3.00 and 4.x (just swap the MAJOR/MINOR/PATCH numbers).
|
|
||||||
# user-agent format has changed. `Transmission/3.00` (old) v.s. `TRANSMISSION/MAJ.MIN.PATCH` (new).
|
|
||||||
realTransmission = pkgs.transmission_4;
|
|
||||||
realVersion = {
|
|
||||||
major = lib.versions.major realTransmission.version;
|
|
||||||
minor = lib.versions.minor realTransmission.version;
|
|
||||||
patch = lib.versions.patch realTransmission.version;
|
|
||||||
};
|
|
||||||
package = realTransmission.overrideAttrs (upstream: {
|
|
||||||
# `cmakeFlags = [ "-DTR_VERSION_MAJOR=3" ]`, etc, doesn't seem to take effect.
|
|
||||||
postPatch = (upstream.postPatch or "") + ''
|
|
||||||
substituteInPlace CMakeLists.txt \
|
|
||||||
--replace-fail 'TR_VERSION_MAJOR "${realVersion.major}"' 'TR_VERSION_MAJOR "3"' \
|
|
||||||
--replace-fail 'TR_VERSION_MINOR "${realVersion.minor}"' 'TR_VERSION_MINOR "0"' \
|
|
||||||
--replace-fail 'TR_VERSION_PATCH "${realVersion.patch}"' 'TR_VERSION_PATCH "0"' \
|
|
||||||
--replace-fail 'set(TR_USER_AGENT_PREFIX "''${TR_SEMVER}")' 'set(TR_USER_AGENT_PREFIX "3.00")'
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
download-dir = "/var/media/torrents"; #< keep in sync with consts embedded in `torrent-done`
|
|
||||||
torrent-done = pkgs.static-nix-shell.mkBash {
|
|
||||||
pname = "torrent-done";
|
|
||||||
srcRoot = ./.;
|
|
||||||
pkgs = [
|
|
||||||
"acl"
|
|
||||||
"coreutils"
|
|
||||||
"findutils"
|
|
||||||
"rsync"
|
|
||||||
"util-linux"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
sane.persist.sys.byStore.plaintext = [
|
|
||||||
# TODO: mode? we need this specifically for the stats tracking in .config/
|
|
||||||
{ user = "transmission"; group = config.users.users.transmission.group; path = "/var/lib/transmission"; method = "bind"; }
|
|
||||||
];
|
|
||||||
users.users.transmission.extraGroups = [ "media" ];
|
|
||||||
|
|
||||||
services.transmission.enable = true;
|
|
||||||
services.transmission.package = package;
|
|
||||||
#v setting `group` this way doesn't tell transmission to `chown` the files it creates
|
|
||||||
# it's a nixpkgs setting which just runs the transmission daemon as this group
|
|
||||||
services.transmission.group = "media";
|
|
||||||
|
|
||||||
# transmission will by default not allow the world to read its files.
|
|
||||||
services.transmission.downloadDirPermissions = "775";
|
|
||||||
services.transmission.extraFlags = [
|
|
||||||
# "--log-level=debug"
|
|
||||||
];
|
|
||||||
|
|
||||||
services.transmission.settings = {
|
|
||||||
# DOCUMENTATION/options list: <https://github.com/transmission/transmission/blob/main/docs/Editing-Configuration-Files.md#options>
|
|
||||||
|
|
||||||
# message-level = 3; #< enable for debug logging. 0-3, default is 2.
|
|
||||||
# ovpns.netnsVethIpv4 => allow rpc only from the root servo ns. it'll tunnel things to the net, if need be.
|
|
||||||
rpc-bind-address = config.sane.netns.ovpns.netnsVethIpv4;
|
|
||||||
#rpc-host-whitelist = "bt.uninsane.org";
|
|
||||||
#rpc-whitelist = "*.*.*.*";
|
|
||||||
rpc-authentication-required = true;
|
|
||||||
rpc-username = "colin";
|
|
||||||
# salted pw. to regenerate, set this plaintext, run nixos-rebuild, and then find the salted pw in:
|
|
||||||
# /var/lib/transmission/.config/transmission-daemon/settings.json
|
|
||||||
rpc-password = "{503fc8928344f495efb8e1f955111ca5c862ce0656SzQnQ5";
|
|
||||||
rpc-whitelist-enabled = false;
|
|
||||||
|
|
||||||
# force behind ovpns in case the NetworkNamespace fails somehow
|
|
||||||
bind-address-ipv4 = config.sane.netns.ovpns.netnsPubIpv4;
|
|
||||||
port-forwarding-enabled = false;
|
|
||||||
|
|
||||||
# hopefully, make the downloads world-readable
|
|
||||||
# umask = 0; #< default is 2: i.e. deny writes from world
|
|
||||||
|
|
||||||
# force peer connections to be encrypted
|
|
||||||
encryption = 2;
|
|
||||||
|
|
||||||
# units in kBps
|
|
||||||
speed-limit-down = 12000;
|
|
||||||
speed-limit-down-enabled = true;
|
|
||||||
speed-limit-up = 800;
|
|
||||||
speed-limit-up-enabled = true;
|
|
||||||
|
|
||||||
# see: https://git.zknt.org/mirror/transmission/commit/cfce6e2e3a9b9d31a9dafedd0bdc8bf2cdb6e876?lang=bg-BG
|
|
||||||
anti-brute-force-enabled = false;
|
|
||||||
|
|
||||||
inherit download-dir;
|
|
||||||
incomplete-dir = "${download-dir}/incomplete";
|
|
||||||
# transmission regularly fails to move stuff from the incomplete dir to the main one, so disable:
|
|
||||||
incomplete-dir-enabled = false;
|
|
||||||
|
|
||||||
# env vars available in script:
|
|
||||||
# - TR_APP_VERSION - Transmission's short version string, e.g. `4.0.0`
|
|
||||||
# - TR_TIME_LOCALTIME
|
|
||||||
# - TR_TORRENT_BYTES_DOWNLOADED - Number of bytes that were downloaded for this torrent
|
|
||||||
# - TR_TORRENT_DIR - Location of the downloaded data
|
|
||||||
# - TR_TORRENT_HASH - The torrent's info hash
|
|
||||||
# - TR_TORRENT_ID
|
|
||||||
# - TR_TORRENT_LABELS - A comma-delimited list of the torrent's labels
|
|
||||||
# - TR_TORRENT_NAME - Name of torrent (not filename)
|
|
||||||
# - TR_TORRENT_TRACKERS - A comma-delimited list of the torrent's trackers' announce URLs
|
|
||||||
script-torrent-done-enabled = true;
|
|
||||||
script-torrent-done-filename = "${torrent-done}/bin/torrent-done";
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.transmission.after = [ "wireguard-wg-ovpns.service" ];
|
|
||||||
systemd.services.transmission.partOf = [ "wireguard-wg-ovpns.service" ];
|
|
||||||
systemd.services.transmission.serviceConfig = {
|
|
||||||
# run this behind the OVPN static VPN
|
|
||||||
NetworkNamespacePath = "/run/netns/ovpns";
|
|
||||||
ExecStartPre = [ "${lib.getExe pkgs.sane-scripts.ip-check} --no-upnp --expect ${config.sane.netns.ovpns.netnsPubIpv4}" ]; # abort if public IP is not as expected
|
|
||||||
|
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = "30s";
|
|
||||||
BindPaths = [ "/var/media" ]; #< so it can move completed torrents into the media library
|
|
||||||
};
|
|
||||||
|
|
||||||
# service to automatically backup torrents i add to transmission
|
|
||||||
systemd.services.backup-torrents = {
|
|
||||||
description = "archive torrents to storage not owned by transmission";
|
|
||||||
script = ''
|
|
||||||
${pkgs.rsync}/bin/rsync -arv /var/lib/transmission/.config/transmission-daemon/torrents/ /var/backup/torrents/
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
systemd.timers.backup-torrents = {
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
timerConfig = {
|
|
||||||
OnStartupSec = "11min";
|
|
||||||
OnUnitActiveSec = "240min";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# transmission web client
|
|
||||||
services.nginx.virtualHosts."bt.uninsane.org" = {
|
|
||||||
# basicAuth is literally cleartext user/pw, so FORCE this to happen over SSL
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
# inherit kTLS;
|
|
||||||
locations."/" = {
|
|
||||||
# proxyPass = "http://ovpns.uninsane.org:9091";
|
|
||||||
proxyPass = "http://${config.sane.netns.ovpns.netnsVethIpv4}:9091";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.dns.zones."uninsane.org".inet.CNAME."bt" = "native";
|
|
||||||
sane.ports.ports."51413" = {
|
|
||||||
protocol = [ "tcp" "udp" ];
|
|
||||||
# visibleTo.ovpns = true; #< not needed: it runs in the ovpns namespace
|
|
||||||
description = "colin-bittorrent";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
#!/usr/bin/env nix-shell
|
|
||||||
#!nix-shell -i bash -p acl -p bash -p coreutils -p findutils -p rsync -p util-linux
|
|
||||||
|
|
||||||
# transmission invokes this with no args, and the following env vars:
|
|
||||||
# - TR_TORRENT_DIR: full path to the folder i told transmission to download it to.
|
|
||||||
# e.g. /var/media/torrents/Videos/Film/Jason.Bourne-2016
|
|
||||||
# optionally:
|
|
||||||
# - TR_DRY_RUN=1
|
|
||||||
# - TR_DEBUG=1
|
|
||||||
# - TR_NO_HARDLINK=1
|
|
||||||
|
|
||||||
DOWNLOAD_DIR=/var/media/torrents
|
|
||||||
|
|
||||||
destructive() {
|
|
||||||
if [ -n "${TR_DRY_RUN-}" ]; then
|
|
||||||
echo "[dry-run] $*"
|
|
||||||
else
|
|
||||||
debug "$@"
|
|
||||||
"$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
debug() {
|
|
||||||
if [ -n "${TR_DEBUG-}" ]; then
|
|
||||||
echo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$TR_TORRENT_DIR" =~ ^.*freeleech.*$ ]]; then
|
|
||||||
# freeleech torrents have no place in my permanent library
|
|
||||||
echo "freeleech: nothing to do"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if ! [[ "$TR_TORRENT_DIR" =~ ^$DOWNLOAD_DIR/.*$ ]]; then
|
|
||||||
echo "unexpected torrent dir, aborting: $TR_TORRENT_DIR"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
REL_DIR="${TR_TORRENT_DIR#$DOWNLOAD_DIR/}"
|
|
||||||
MEDIA_DIR="/var/media/$REL_DIR"
|
|
||||||
|
|
||||||
destructive mkdir -p "$(dirname "$MEDIA_DIR")"
|
|
||||||
destructive rsync -arv "$TR_TORRENT_DIR/" "$MEDIA_DIR/"
|
|
||||||
# make the media rwx by anyone in the group
|
|
||||||
destructive find "$MEDIA_DIR" -type d -exec setfacl --recursive --modify d:g::rwx,o::rx {} \;
|
|
||||||
destructive find "$MEDIA_DIR" -type d -exec chmod g+rw,a+rx {} \;
|
|
||||||
|
|
||||||
# if there's a single directory inside the media dir, then inline that
|
|
||||||
subdirs=("$MEDIA_DIR"/*)
|
|
||||||
debug "top-level items in torrent dir:" "${subdirs[@]}"
|
|
||||||
if [ ${#subdirs[@]} -eq 1 ]; then
|
|
||||||
dirname="${subdirs[0]}"
|
|
||||||
debug "exactly one top-level item, checking if directory: $dirname"
|
|
||||||
if [ -d "$dirname" ]; then
|
|
||||||
destructive mv "$dirname"/* "$MEDIA_DIR/" && destructive rmdir "$dirname"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# remove noisy files:
|
|
||||||
destructive find "$MEDIA_DIR/" -type f \(\
|
|
||||||
-iname '.*downloaded.?from.*' \
|
|
||||||
-o -iname 'source.txt' \
|
|
||||||
-o -iname 'upcoming.?releases.*' \
|
|
||||||
-o -iname 'www.YTS.*.jpg' \
|
|
||||||
-o -iname 'WWW.YIFY*.COM.jpg' \
|
|
||||||
-o -iname 'YIFY*.com.txt' \
|
|
||||||
-o -iname 'YTS*.com.txt' \
|
|
||||||
\) -exec rm {} \;
|
|
||||||
|
|
||||||
if ! [ -n "${TR_NO_HARDLINK}" ]; then
|
|
||||||
# dedupe the whole media library.
|
|
||||||
# yeah, a bit excessive: move this to a cron job if that's problematic
|
|
||||||
# or make it run with only 1/N probability, etc.
|
|
||||||
destructive hardlink /var/media --reflink=always --ignore-time --verbose
|
|
||||||
fi
|
|
|
@ -1,17 +1,23 @@
|
||||||
# TODO: split this file apart into smaller files to make it easier to understand
|
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
|
||||||
dyn-dns = config.sane.services.dyn-dns;
|
|
||||||
nativeAddrs = lib.mapAttrs (_name: builtins.head) config.sane.dns.zones."uninsane.org".inet.A;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
|
services.trust-dns.enable = true;
|
||||||
|
|
||||||
|
services.trust-dns.settings.listen_addrs_ipv4 = [
|
||||||
|
# specify each address explicitly, instead of using "*".
|
||||||
|
# this ensures responses are sent from the address at which the request was received.
|
||||||
|
config.sane.hosts.by-name."servo".lan-ip
|
||||||
|
"10.0.1.5"
|
||||||
|
];
|
||||||
|
# don't bind to IPv6 until i explicitly test that stack
|
||||||
|
services.trust-dns.settings.listen_addrs_ipv6 = [];
|
||||||
|
services.trust-dns.quiet = true;
|
||||||
|
# services.trust-dns.debug = true;
|
||||||
|
|
||||||
sane.ports.ports."53" = {
|
sane.ports.ports."53" = {
|
||||||
protocol = [ "udp" "tcp" ];
|
protocol = [ "udp" "tcp" ];
|
||||||
visibleTo.lan = true;
|
visibleTo.lan = true;
|
||||||
# visibleTo.wan = true;
|
visibleTo.wan = true;
|
||||||
visibleTo.ovpns = true;
|
|
||||||
visibleTo.doof = true;
|
|
||||||
description = "colin-dns-hosting";
|
description = "colin-dns-hosting";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,28 +34,26 @@ in
|
||||||
sane.dns.zones."uninsane.org".inet = {
|
sane.dns.zones."uninsane.org".inet = {
|
||||||
SOA."@" = ''
|
SOA."@" = ''
|
||||||
ns1.uninsane.org. admin-dns.uninsane.org. (
|
ns1.uninsane.org. admin-dns.uninsane.org. (
|
||||||
2023092101 ; Serial
|
2022122101 ; Serial
|
||||||
4h ; Refresh
|
4h ; Refresh
|
||||||
30m ; Retry
|
30m ; Retry
|
||||||
7d ; Expire
|
7d ; Expire
|
||||||
5m) ; Negative response TTL
|
5m) ; Negative response TTL
|
||||||
'';
|
'';
|
||||||
TXT."rev" = "2023092101";
|
TXT."rev" = "2023052901";
|
||||||
|
|
||||||
CNAME."native" = "%CNAMENATIVE%";
|
CNAME."native" = "%CNAMENATIVE%";
|
||||||
A."@" = "%ANATIVE%";
|
A."@" = "%ANATIVE%";
|
||||||
A."servo.wan" = "%AWAN%";
|
A."wan" = "%AWAN%";
|
||||||
A."servo.doof" = "%ADOOF%";
|
|
||||||
A."servo.lan" = config.sane.hosts.by-name."servo".lan-ip;
|
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
|
# 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.
|
# 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.
|
# so, org. can specify ns2/ns3 as being to the VPN, with no mention of ns1. we provide ns1 here.
|
||||||
A."ns1" = "%ANATIVE%";
|
A."ns1" = "%ANATIVE%";
|
||||||
A."ns2" = "%ADOOF%";
|
A."ns2" = "185.157.162.178";
|
||||||
A."ns3" = "%AOVPNS%";
|
A."ns3" = "185.157.162.178";
|
||||||
A."ovpns" = "%AOVPNS%";
|
A."ovpns" = "185.157.162.178";
|
||||||
NS."@" = [
|
NS."@" = [
|
||||||
"ns1.uninsane.org."
|
"ns1.uninsane.org."
|
||||||
"ns2.uninsane.org."
|
"ns2.uninsane.org."
|
||||||
|
@ -59,109 +63,76 @@ in
|
||||||
|
|
||||||
services.trust-dns.settings.zones = [ "uninsane.org" ];
|
services.trust-dns.settings.zones = [ "uninsane.org" ];
|
||||||
|
|
||||||
|
services.trust-dns.package =
|
||||||
|
let
|
||||||
|
sed = "${pkgs.gnused}/bin/sed";
|
||||||
|
zone-dir = "/var/lib/trust-dns";
|
||||||
|
zone-wan = "${zone-dir}/wan/uninsane.org.zone";
|
||||||
|
zone-lan = "${zone-dir}/lan/uninsane.org.zone";
|
||||||
|
zone-template = pkgs.writeText "uninsane.org.zone.in" config.sane.dns.zones."uninsane.org".rendered;
|
||||||
|
in pkgs.writeShellScriptBin "named" ''
|
||||||
|
# compute wan/lan values
|
||||||
|
mkdir -p ${zone-dir}/{ovpn,wan,lan}
|
||||||
|
wan=$(cat '${config.sane.services.dyn-dns.ipPath}')
|
||||||
|
lan=${config.sane.hosts.by-name."servo".lan-ip}
|
||||||
|
|
||||||
networking.nat.enable = true; #< TODO: try removing this?
|
# create specializations that resolve native.uninsane.org to different CNAMEs
|
||||||
# networking.nat.extraCommands = ''
|
${sed} s/%AWAN%/$wan/ ${zone-template} \
|
||||||
# # redirect incoming DNS requests from LAN addresses
|
| ${sed} s/%CNAMENATIVE%/wan/ \
|
||||||
# # to the LAN-specialized DNS service
|
| ${sed} s/%ANATIVE%/$wan/ \
|
||||||
# # N.B.: use the `nixos-*` chains instead of e.g. PREROUTING
|
> ${zone-wan}
|
||||||
# # because they get cleanly reset across activations or `systemctl restart firewall`
|
${sed} s/%AWAN%/$wan/ ${zone-template} \
|
||||||
# # instead of accumulating cruft
|
| ${sed} s/%CNAMENATIVE%/servo.lan/ \
|
||||||
# iptables -t nat -A nixos-nat-pre -p udp --dport 53 \
|
| ${sed} s/%ANATIVE%/$lan/ \
|
||||||
# -m iprange --src-range 10.78.76.0-10.78.79.255 \
|
> ${zone-lan}
|
||||||
# -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";
|
|
||||||
# };
|
|
||||||
|
|
||||||
|
# launch the different interfaces, separately
|
||||||
|
${pkgs.trust-dns}/bin/named --port 53 --zonedir ${zone-dir}/wan/ $@ &
|
||||||
|
WANPID=$!
|
||||||
|
${pkgs.trust-dns}/bin/named --port 1053 --zonedir ${zone-dir}/lan/ $@ &
|
||||||
|
LANPID=$!
|
||||||
|
|
||||||
sane.services.trust-dns.enable = true;
|
# wait until any of the processes exits, then kill them all and exit error
|
||||||
sane.services.trust-dns.instances = let
|
while kill -0 $WANPID $LANPID ; do
|
||||||
mkSubstitutions = flavor: {
|
sleep 5
|
||||||
"%ADOOF%" = config.sane.netns.doof.netnsPubIpv4;
|
done
|
||||||
"%ANATIVE%" = nativeAddrs."servo.${flavor}";
|
kill $WANPID $LANPID
|
||||||
"%AOVPNS%" = config.sane.netns.ovpns.netnsPubIpv4;
|
exit 1
|
||||||
"%AWAN%" = "$(cat '${dyn-dns.ipPath}')";
|
'';
|
||||||
"%CNAMENATIVE%" = "servo.${flavor}";
|
|
||||||
};
|
systemd.services.trust-dns.serviceConfig = {
|
||||||
in
|
DynamicUser = lib.mkForce false;
|
||||||
{
|
User = "trust-dns";
|
||||||
doof = {
|
Group = "trust-dns";
|
||||||
substitutions = mkSubstitutions "doof";
|
};
|
||||||
listenAddrsIpv4 = [
|
users.groups.trust-dns = {};
|
||||||
config.sane.netns.doof.hostVethIpv4
|
users.users.trust-dns = {
|
||||||
config.sane.netns.ovpns.hostVethIpv4
|
group = "trust-dns";
|
||||||
];
|
isSystemUser = true;
|
||||||
};
|
|
||||||
hn = {
|
|
||||||
substitutions = mkSubstitutions "hn";
|
|
||||||
listenAddrsIpv4 = [ nativeAddrs."servo.hn" ];
|
|
||||||
};
|
|
||||||
lan = {
|
|
||||||
substitutions = mkSubstitutions "lan";
|
|
||||||
listenAddrsIpv4 = [ nativeAddrs."servo.lan" ];
|
|
||||||
# port = 1053;
|
|
||||||
};
|
|
||||||
# wan = {
|
|
||||||
# substitutions = mkSubstitutions "wan";
|
|
||||||
# listenAddrsIpv4 = [
|
|
||||||
# nativeAddrs."servo.lan"
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
# hn-resolver = {
|
|
||||||
# # don't need %AWAN% here because we forward to the hn instance.
|
|
||||||
# listenAddrsIpv4 = [ nativeAddrs."servo.hn" ];
|
|
||||||
# extraConfig = {
|
|
||||||
# zones = [
|
|
||||||
# {
|
|
||||||
# zone = "uninsane.org";
|
|
||||||
# zone_type = "Forward";
|
|
||||||
# stores = {
|
|
||||||
# type = "forward";
|
|
||||||
# name_servers = [
|
|
||||||
# {
|
|
||||||
# socket_addr = "${nativeAddrs."servo.hn"}:1053";
|
|
||||||
# protocol = "udp";
|
|
||||||
# trust_nx_responses = true;
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# # forward the root zone to the local DNS resolver
|
|
||||||
# zone = ".";
|
|
||||||
# zone_type = "Forward";
|
|
||||||
# stores = {
|
|
||||||
# type = "forward";
|
|
||||||
# name_servers = [
|
|
||||||
# {
|
|
||||||
# socket_addr = "127.0.0.53:53";
|
|
||||||
# protocol = "udp";
|
|
||||||
# trust_nx_responses = true;
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sane.services.dyn-dns.restartOnChange = [
|
sane.services.dyn-dns.restartOnChange = [ "trust-dns.service" ];
|
||||||
"trust-dns-doof.service"
|
|
||||||
"trust-dns-hn.service"
|
networking.nat.enable = true;
|
||||||
"trust-dns-lan.service"
|
networking.nat.extraCommands = ''
|
||||||
# "trust-dns-wan.service"
|
# redirect incoming DNS requests from LAN addresses
|
||||||
# "trust-dns-hn-resolver.service" # doesn't need restart because it doesn't know about WAN IP
|
# 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?
|
||||||
|
protocol = [ "udp" "tcp" ];
|
||||||
|
visibleTo.lan = true;
|
||||||
|
description = "colin-redirected-dns-for-lan-namespace";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
{ lib, pkgs, ... }:
|
|
||||||
{
|
|
||||||
boot.initrd.supportedFilesystems = [ "ext4" "btrfs" "ext2" "ext3" "vfat" ];
|
|
||||||
# useful emergency utils
|
|
||||||
boot.initrd.extraUtilsCommands = ''
|
|
||||||
copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfstune
|
|
||||||
copy_bin_and_libs ${pkgs.util-linux}/bin/{cfdisk,lsblk,lscpu}
|
|
||||||
copy_bin_and_libs ${pkgs.gptfdisk}/bin/{cgdisk,gdisk}
|
|
||||||
copy_bin_and_libs ${pkgs.smartmontools}/bin/smartctl
|
|
||||||
copy_bin_and_libs ${pkgs.e2fsprogs}/bin/resize2fs
|
|
||||||
'' + lib.optionalString pkgs.stdenv.hostPlatform.isx86_64 ''
|
|
||||||
copy_bin_and_libs ${pkgs.nvme-cli}/bin/nvme # doesn't cross compile
|
|
||||||
'';
|
|
||||||
boot.kernelParams = [
|
|
||||||
"boot.shell_on_fail"
|
|
||||||
#v experimental full pre-emption for hopefully better call/audio latency on moby.
|
|
||||||
# also toggleable at runtime via /sys/kernel/debug/sched/preempt
|
|
||||||
# defaults to preempt=voluntary
|
|
||||||
# "preempt=full"
|
|
||||||
];
|
|
||||||
# other kernelParams:
|
|
||||||
# "boot.trace"
|
|
||||||
# "systemd.log_level=debug"
|
|
||||||
# "systemd.log_target=console"
|
|
||||||
|
|
||||||
# moby has to run recent kernels (defined elsewhere).
|
|
||||||
# meanwhile, kernel variation plays some minor role in things like sandboxing (landlock) and capabilities.
|
|
||||||
# simpler to keep near the latest kernel on all devices,
|
|
||||||
# and also makes certain that any weird system-level bugs i see aren't likely to be stale kernel bugs.
|
|
||||||
# servo needs zfs though, which doesn't support every kernel.
|
|
||||||
boot.kernelPackages = lib.mkDefault pkgs.zfs.latestCompatibleLinuxPackages;
|
|
||||||
|
|
||||||
# hack in the `boot.shell_on_fail` arg since that doesn't always seem to work.
|
|
||||||
boot.initrd.preFailCommands = "allowShell=1";
|
|
||||||
|
|
||||||
# default: 4 (warn). 7 is debug
|
|
||||||
boot.consoleLogLevel = 7;
|
|
||||||
|
|
||||||
boot.loader.grub.enable = lib.mkDefault false;
|
|
||||||
boot.loader.generic-extlinux-compatible.enable = lib.mkDefault true;
|
|
||||||
|
|
||||||
hardware.enableAllFirmware = true; # firmware with licenses that don't allow for redistribution. fuck lawyers, fuck IP, give me the goddamn firmware.
|
|
||||||
# hardware.enableRedistributableFirmware = true; # proprietary but free-to-distribute firmware (extraneous to `enableAllFirmware` option)
|
|
||||||
|
|
||||||
# default is 252274, which is too low particularly for servo.
|
|
||||||
# manifests as spurious "No space left on device" when trying to install watches,
|
|
||||||
# e.g. in dyn-dns by `systemctl start dyn-dns-watcher.path`.
|
|
||||||
# see: <https://askubuntu.com/questions/828779/failed-to-add-run-systemd-ask-password-to-directory-watch-no-space-left-on-dev>
|
|
||||||
boot.kernel.sysctl."fs.inotify.max_user_watches" = 1048576;
|
|
||||||
}
|
|
|
@ -1,52 +1,92 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ lib, pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./boot.nix
|
|
||||||
./feeds.nix
|
./feeds.nix
|
||||||
./fs.nix
|
./fs.nix
|
||||||
|
./hardware.nix
|
||||||
./home
|
./home
|
||||||
./hosts.nix
|
|
||||||
./ids.nix
|
./ids.nix
|
||||||
./machine-id.nix
|
./machine-id.nix
|
||||||
./net
|
./net.nix
|
||||||
./nix.nix
|
./nix-path
|
||||||
./persist.nix
|
./persist.nix
|
||||||
./polyunfill.nix
|
|
||||||
./programs
|
./programs
|
||||||
./quirks.nix
|
|
||||||
./secrets.nix
|
./secrets.nix
|
||||||
./ssh.nix
|
./ssh.nix
|
||||||
./systemd.nix
|
|
||||||
./users
|
./users
|
||||||
|
./vpn.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
|
|
||||||
# this affects where nixos modules look for stateful data which might have been migrated across releases.
|
|
||||||
system.stateVersion = "21.11";
|
|
||||||
|
|
||||||
sane.nixcache.enable-trusted-keys = true;
|
sane.nixcache.enable-trusted-keys = true;
|
||||||
sane.nixcache.enable = lib.mkDefault true;
|
sane.nixcache.enable = lib.mkDefault true;
|
||||||
sane.persist.enable = lib.mkDefault true;
|
sane.persist.enable = lib.mkDefault true;
|
||||||
sane.root-on-tmpfs = lib.mkDefault true;
|
|
||||||
sane.programs.sysadminUtils.enableFor.system = lib.mkDefault true;
|
sane.programs.sysadminUtils.enableFor.system = lib.mkDefault true;
|
||||||
sane.programs.consoleUtils.enableFor.user.colin = lib.mkDefault true;
|
sane.programs.consoleUtils.enableFor.user.colin = lib.mkDefault true;
|
||||||
|
|
||||||
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
nixpkgs.config.allowBroken = true; # NIXPKGS_ALLOW_BROKEN
|
||||||
|
|
||||||
# time.timeZone = "America/Los_Angeles";
|
# time.timeZone = "America/Los_Angeles";
|
||||||
time.timeZone = "Etc/UTC"; # DST is too confusing for me => use a stable timezone
|
time.timeZone = "Etc/UTC"; # DST is too confusing for me => use a stable timezone
|
||||||
|
|
||||||
|
# allow `nix flake ...` command
|
||||||
|
# TODO: is this still required?
|
||||||
|
nix.extraOptions = ''
|
||||||
|
experimental-features = nix-command flakes
|
||||||
|
'';
|
||||||
|
# hardlinks identical files in the nix store to save 25-35% disk space.
|
||||||
|
# unclear _when_ this occurs. it's not a service.
|
||||||
|
# does the daemon continually scan the nix store?
|
||||||
|
# does the builder use some content-addressed db to efficiently dedupe?
|
||||||
|
nix.settings.auto-optimise-store = true;
|
||||||
|
|
||||||
|
systemd.services.nix-daemon.serviceConfig = {
|
||||||
|
# the nix-daemon manages nix builders
|
||||||
|
# kill nix-daemon subprocesses when systemd-oomd detects an out-of-memory condition
|
||||||
|
# see:
|
||||||
|
# - nixos PR that enabled systemd-oomd: <https://github.com/NixOS/nixpkgs/pull/169613>
|
||||||
|
# - systemd's docs on these properties: <https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#ManagedOOMSwap=auto%7Ckill>
|
||||||
|
#
|
||||||
|
# systemd's docs warn that without swap, systemd-oomd might not be able to react quick enough to save the system.
|
||||||
|
# see `man oomd.conf` for further tunables that may help.
|
||||||
|
#
|
||||||
|
# alternatively, apply this more broadly with `systemd.oomd.enableSystemSlice = true` or `enableRootSlice`
|
||||||
|
# TODO: also apply this to the guest user's slice (user-1100.slice)
|
||||||
|
# TODO: also apply this to distccd
|
||||||
|
ManagedOOMMemoryPressure = "kill";
|
||||||
|
ManagedOOMSwap = "kill";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
system.activationScripts.nixClosureDiff = {
|
system.activationScripts.nixClosureDiff = {
|
||||||
supportsDryActivation = true;
|
supportsDryActivation = true;
|
||||||
text = ''
|
text = ''
|
||||||
# show which packages changed versions or are new/removed in this upgrade
|
# show which packages changed versions or are new/removed in this upgrade
|
||||||
# source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix>
|
# source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix>
|
||||||
# modified to not error on boot (when /run/current-system doesn't exist)
|
${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
|
||||||
if [ -d /run/current-system ]; then
|
|
||||||
${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
|
|
||||||
fi
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# disable non-required packages like nano, perl, rsync, strace
|
||||||
|
environment.defaultPackages = [];
|
||||||
|
|
||||||
|
# dconf docs: <https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/desktop_migration_and_administration_guide/profiles>
|
||||||
|
# this lets programs temporarily write user-level dconf settings (aka gsettings).
|
||||||
|
# they're written to ~/.config/dconf/user, unless `DCONF_PROFILE` is set to something other than the default of /etc/dconf/profile/user
|
||||||
|
# find keys/values with `dconf dump /`
|
||||||
|
programs.dconf.enable = true;
|
||||||
|
programs.dconf.packages = [
|
||||||
|
(pkgs.writeTextFile {
|
||||||
|
name = "dconf-user-profile";
|
||||||
|
destination = "/etc/dconf/profile/user";
|
||||||
|
text = ''
|
||||||
|
user-db:user
|
||||||
|
system-db:site
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
];
|
||||||
|
# sane.programs.glib.enableFor.user.colin = true; # for `gsettings`
|
||||||
|
|
||||||
# link debug symbols into /run/current-system/sw/lib/debug
|
# link debug symbols into /run/current-system/sw/lib/debug
|
||||||
# hopefully picked up by gdb automatically?
|
# hopefully picked up by gdb automatically?
|
||||||
environment.enableDebugInfo = true;
|
environment.enableDebugInfo = true;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
# where to find good stuff?
|
# where to find good stuff?
|
||||||
# - universal search/directory: <https://podcastindex.org>
|
|
||||||
# - podcasts w/ a community: <https://lemmyverse.net/communities?query=podcast>
|
|
||||||
# - podcast rec thread: <https://lemmy.ml/post/1565858>
|
# - podcast rec thread: <https://lemmy.ml/post/1565858>
|
||||||
#
|
#
|
||||||
# candidates:
|
# candidates:
|
||||||
|
@ -51,8 +49,6 @@ let
|
||||||
else
|
else
|
||||||
"infrequent"
|
"infrequent"
|
||||||
));
|
));
|
||||||
} // lib.optionalAttrs (lib.hasPrefix "https://www.youtube.com/" raw.url) {
|
|
||||||
format = "video";
|
|
||||||
} // lib.optionalAttrs (raw.is_podcast or false) {
|
} // lib.optionalAttrs (raw.is_podcast or false) {
|
||||||
format = "podcast";
|
format = "podcast";
|
||||||
} // lib.optionalAttrs (raw.title or "" != "") {
|
} // lib.optionalAttrs (raw.title or "" != "") {
|
||||||
|
@ -60,205 +56,201 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
podcasts = [
|
podcasts = [
|
||||||
(fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech) # ACQ2 - more "Acquired" episodes
|
|
||||||
(fromDb "allinchamathjason.libsyn.com" // pol)
|
|
||||||
(fromDb "anchor.fm/s/34c7232c/podcast/rss" // tech) # Civboot -- https://anchor.fm/civboot
|
|
||||||
(fromDb "anchor.fm/s/2da69154/podcast/rss" // tech) # POD OF JAKE -- https://podofjake.com/
|
|
||||||
(fromDb "cast.postmarketos.org" // tech)
|
|
||||||
(fromDb "congressionaldish.libsyn.com" // pol) # Jennifer Briney
|
|
||||||
(fromDb "craphound.com" // pol) # Cory Doctorow -- both podcast & text entries
|
|
||||||
(fromDb "darknetdiaries.com" // tech)
|
|
||||||
(fromDb "feed.podbean.com/matrixlive/feed.xml" // tech) # Matrix (chat) Live
|
|
||||||
(fromDb "feeds.99percentinvisible.org/99percentinvisible" // pol) # 99% Invisible -- also available here: <https://feeds.simplecast.com/BqbsxVfO>
|
|
||||||
(fromDb "feeds.feedburner.com/80000HoursPodcast" // rat)
|
|
||||||
(fromDb "feeds.feedburner.com/dancarlin/history" // rat)
|
|
||||||
(fromDb "feeds.feedburner.com/radiolab" // pol) # Radiolab -- also available here, but ONLY OVER HTTP: <http://feeds.wnyc.org/radiolab>
|
|
||||||
(fromDb "feeds.megaphone.fm/behindthebastards" // pol) # also Maggie Killjoy
|
|
||||||
(fromDb "feeds.megaphone.fm/recodedecode" // tech) # The Verge - Decoder
|
|
||||||
(fromDb "feeds.simplecast.com/54nAGcIl" // pol) # The Daily
|
|
||||||
(fromDb "feeds.simplecast.com/82FI35Px" // pol) # Ezra Klein Show
|
|
||||||
(fromDb "feeds.simplecast.com/wgl4xEgL" // rat) # Econ Talk
|
|
||||||
(fromDb "feeds.simplecast.com/xKJ93w_w" // uncat) # Atlas Obscura
|
|
||||||
(fromDb "feeds.transistor.fm/acquired" // tech)
|
|
||||||
(fromDb "fulltimenix.com" // tech)
|
|
||||||
(fromDb "futureofcoding.org/episodes" // tech)
|
|
||||||
(fromDb "hackerpublicradio.org" // tech)
|
|
||||||
(fromDb "lexfridman.com/podcast" // rat)
|
(fromDb "lexfridman.com/podcast" // rat)
|
||||||
(fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies
|
## Astral Codex Ten
|
||||||
(fromDb "microarch.club" // tech)
|
(fromDb "sscpodcast.libsyn.com" // rat)
|
||||||
(fromDb "mintcast.org" // tech)
|
## Less Wrong Curated
|
||||||
(fromDb "omegataupodcast.net" // tech) # 3/4 German; 1/4 eps are English
|
(fromDb "feeds.libsyn.com/421877" // rat)
|
||||||
(fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol) # Maggie Killjoy -- referenced by Cory Doctorow
|
## Econ Talk
|
||||||
(fromDb "omny.fm/shows/money-stuff-the-podcast") # Matt Levine
|
(fromDb "feeds.simplecast.com/wgl4xEgL" // rat)
|
||||||
(fromDb "omny.fm/shows/the-dollop-with-dave-anthony-and-gareth-reynolds") # The Dollop history/comedy
|
## Cory Doctorow -- both podcast & text entries
|
||||||
(fromDb "originstories.libsyn.com" // uncat)
|
(fromDb "craphound.com" // pol)
|
||||||
(fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol)
|
## Maggie Killjoy -- referenced by Cory Doctorow
|
||||||
(fromDb "politicalorphanage.libsyn.com" // pol)
|
(fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol)
|
||||||
(fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast
|
(fromDb "congressionaldish.libsyn.com" // pol)
|
||||||
(fromDb "rss.acast.com/deconstructed") # The Intercept - Deconstructed
|
|
||||||
(fromDb "rss.acast.com/ft-tech-tonic" // tech)
|
|
||||||
(fromDb "rss.acast.com/intercepted-with-jeremy-scahill") # The Intercept - Intercepted
|
|
||||||
(fromDb "rss.art19.com/60-minutes" // pol)
|
|
||||||
(fromDb "rss.art19.com/the-portal" // rat) # Eric Weinstein
|
|
||||||
(fromDb "seattlenice.buzzsprout.com" // pol)
|
|
||||||
(fromDb "srslywrong.com" // pol)
|
|
||||||
(fromDb "sharkbytes.transistor.fm" // tech) # Wireshark Podcast o_0
|
|
||||||
(fromDb "sscpodcast.libsyn.com" // rat) # Astral Codex Ten
|
|
||||||
(fromDb "talesfromthebridge.buzzsprout.com" // tech) # Sci-Fi? has Peter Watts; author of No Moods, Ads or Cutesy Fucking Icons (rifters.com)
|
|
||||||
(fromDb "theamphour.com" // tech)
|
|
||||||
(fromDb "techtalesshow.com" // tech) # Corbin Davenport
|
|
||||||
(fromDb "techwontsave.us" // pol) # rec by Cory Doctorow
|
|
||||||
(fromDb "wakingup.libsyn.com" // pol) # Sam Harris
|
|
||||||
(fromDb "werenotwrong.fireside.fm" // pol)
|
|
||||||
(mkPod "https://sfconservancy.org/casts/the-corresponding-source/feeds/ogg/" // tech)
|
|
||||||
|
|
||||||
# (fromDb "feeds.libsyn.com/421877" // rat) # Less Wrong Curated
|
|
||||||
# (fromDb "feeds.megaphone.fm/hubermanlab" // uncat) # Daniel Huberman on sleep
|
|
||||||
# (fromDb "feeds.simplecast.com/l2i9YnTd" // tech // pol) # Hard Fork (NYtimes tech)
|
|
||||||
# (fromDb "podcast.thelinuxexp.com" // tech) # low-brow linux/foss PR announcements
|
|
||||||
# (fromDb "rss.art19.com/your-welcome" // pol) # Michael Malice - Your Welcome -- also available here: <https://origin.podcastone.com/podcast?categoryID2=2232>
|
|
||||||
# (fromDb "rss.prod.firstlook.media/deconstructed/podcast.rss" // pol) #< possible URL rot
|
|
||||||
# (fromDb "rss.prod.firstlook.media/intercepted/podcast.rss" // pol) #< possible URL rot
|
|
||||||
# (fromDb "trashfuturepodcast.podbean.com" // pol) # rec by Cory Doctorow, but way rambly
|
|
||||||
# (mkPod "https://anchor.fm/s/21bc734/podcast/rss" // pol // infrequent) # Emerge: making sense of what's next -- <https://www.whatisemerging.com/emergepodcast>
|
|
||||||
# (mkPod "https://audioboom.com/channels/5097784.rss" // tech) # Lateral with Tom Scott
|
|
||||||
# (mkPod "https://feeds.megaphone.fm/RUNMED9919162779" // pol // infrequent) # The Witch Trials of J.K. Rowling: <https://www.thefp.com/witchtrials>
|
|
||||||
# (mkPod "https://podcasts.la.utexas.edu/this-is-democracy/feed/podcast/" // pol // weekly)
|
# (mkPod "https://podcasts.la.utexas.edu/this-is-democracy/feed/podcast/" // pol // weekly)
|
||||||
|
## Civboot -- https://anchor.fm/civboot
|
||||||
|
(fromDb "anchor.fm/s/34c7232c/podcast/rss" // tech)
|
||||||
|
## Emerge: making sense of what's next -- <https://www.whatisemerging.com/emergepodcast>
|
||||||
|
(mkPod "https://anchor.fm/s/21bc734/podcast/rss" // pol // infrequent)
|
||||||
|
(fromDb "feeds.feedburner.com/80000HoursPodcast" // rat)
|
||||||
|
## Daniel Huberman on sleep
|
||||||
|
(fromDb "feeds.megaphone.fm/hubermanlab" // uncat)
|
||||||
|
## Multidisciplinary Association for Psychedelic Studies
|
||||||
|
(fromDb "mapspodcast.libsyn.com" // uncat)
|
||||||
|
(fromDb "allinchamathjason.libsyn.com" // pol)
|
||||||
|
(fromDb "feeds.transistor.fm/acquired" // tech)
|
||||||
|
## ACQ2 - more "Acquired" episodes
|
||||||
|
(fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech)
|
||||||
|
# The Intercept - Deconstructed
|
||||||
|
(fromDb "rss.acast.com/deconstructed")
|
||||||
|
# (fromDb "rss.prod.firstlook.media/deconstructed/podcast.rss" // pol) #< possible URL rot
|
||||||
|
## The Daily
|
||||||
|
(mkPod "https://feeds.simplecast.com/54nAGcIl" // pol // daily)
|
||||||
|
# The Intercept - Intercepted
|
||||||
|
(fromDb "rss.acast.com/intercepted-with-jeremy-scahill")
|
||||||
|
# (fromDb "rss.prod.firstlook.media/intercepted/podcast.rss" // pol) #< possible URL rot
|
||||||
|
(fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol)
|
||||||
|
## Eric Weinstein
|
||||||
|
(fromDb "rss.art19.com/the-portal" // rat)
|
||||||
|
(fromDb "darknetdiaries.com" // tech)
|
||||||
|
## Radiolab -- also available here, but ONLY OVER HTTP: <http://feeds.wnyc.org/radiolab>
|
||||||
|
(fromDb "feeds.feedburner.com/radiolab" // pol)
|
||||||
|
## Sam Harris
|
||||||
|
(fromDb "wakingup.libsyn.com" // pol)
|
||||||
|
## 99% Invisible -- also available here: <https://feeds.simplecast.com/BqbsxVfO>
|
||||||
|
(fromDb "feeds.99percentinvisible.org/99percentinvisible" // pol)
|
||||||
|
(fromDb "rss.acast.com/ft-tech-tonic" // tech)
|
||||||
|
(fromDb "feeds.feedburner.com/dancarlin/history" // rat)
|
||||||
|
(fromDb "rss.art19.com/60-minutes" // pol)
|
||||||
|
## The Verge - Decoder
|
||||||
|
(fromDb "feeds.megaphone.fm/recodedecode" // tech)
|
||||||
|
## Matrix (chat) Live
|
||||||
|
(fromDb "feed.podbean.com/matrixlive/feed.xml" // tech)
|
||||||
|
(fromDb "cast.postmarketos.org" // tech)
|
||||||
|
(fromDb "podcast.thelinuxexp.com" // tech)
|
||||||
|
## Michael Malice - Your Welcome -- also available here: <https://origin.podcastone.com/podcast?categoryID2=2232>
|
||||||
|
(fromDb "rss.art19.com/your-welcome" // pol)
|
||||||
|
(fromDb "seattlenice.buzzsprout.com" // pol)
|
||||||
|
## Sci-Fi? has Peter Watts; author of No Moods, Ads or Cutesy Fucking Icons (rifters.com)
|
||||||
|
(fromDb "talesfromthebridge.buzzsprout.com" // tech)
|
||||||
|
## UnNamed Reverse Engineering Podcast
|
||||||
|
(fromDb "reverseengineering.libsyn.com/rss" // tech)
|
||||||
|
## The Witch Trials of J.K. Rowling
|
||||||
|
## - <https://www.thefp.com/witchtrials>
|
||||||
|
(mkPod "https://feeds.megaphone.fm/RUNMED9919162779" // pol // infrequent)
|
||||||
|
## Atlas Obscura
|
||||||
|
(fromDb "feeds.simplecast.com/xKJ93w_w" // uncat)
|
||||||
|
## Ezra Klein Show
|
||||||
|
(fromDb "feeds.simplecast.com/82FI35Px" // pol)
|
||||||
|
## Wireshark Podcast o_0
|
||||||
|
(fromDb "sharkbytes.transistor.fm" // tech)
|
||||||
];
|
];
|
||||||
|
|
||||||
texts = [
|
texts = [
|
||||||
(fromDb "acoup.blog/feed") # history, states. author: <https://historians.social/@bretdevereaux/following>
|
# AGGREGATORS (> 1 post/day)
|
||||||
(fromDb "amosbbatto.wordpress.com" // tech)
|
|
||||||
(fromDb "anish.lakhwara.com" // tech)
|
|
||||||
(fromDb "apenwarr.ca/log/rss.php" // tech) # CEO of tailscale
|
|
||||||
(fromDb "applieddivinitystudies.com" // rat)
|
|
||||||
(fromDb "artemis.sh" // tech)
|
|
||||||
(fromDb "ascii.textfiles.com" // tech) # Jason Scott
|
|
||||||
(fromDb "austinvernon.site" // tech)
|
|
||||||
(fromDb "buttondown.email" // tech)
|
|
||||||
(fromDb "ben-evans.com/benedictevans" // pol)
|
|
||||||
(fromDb "bitbashing.io" // tech)
|
|
||||||
(fromDb "bitsaboutmoney.com" // uncat)
|
|
||||||
(fromDb "blog.danieljanus.pl" // tech)
|
|
||||||
(fromDb "blog.dshr.org" // pol) # David Rosenthal
|
|
||||||
(fromDb "blog.jmp.chat" // tech)
|
|
||||||
(fromDb "blog.rust-lang.org" // tech)
|
|
||||||
(fromDb "blog.thalheim.io" // tech) # Mic92
|
|
||||||
(fromDb "bunniestudios.com" // tech) # Bunnie Juang
|
|
||||||
(fromDb "capitolhillseattle.com" // pol)
|
|
||||||
(fromDb "edwardsnowden.substack.com" // pol // text)
|
|
||||||
(fromDb "fasterthanli.me" // tech)
|
|
||||||
(fromDb "gwern.net" // rat)
|
|
||||||
(fromDb "hardcoresoftware.learningbyshipping.com" // tech) # Steven Sinofsky
|
|
||||||
(fromDb "harihareswara.net" // tech // pol) # rec by Cory Doctorow
|
|
||||||
(fromDb "ianthehenry.com" // tech)
|
|
||||||
(fromDb "idiomdrottning.org" // uncat)
|
|
||||||
(fromDb "interconnected.org/home/feed" // rat) # Matt Webb -- engineering-ish, but dreamy
|
|
||||||
(fromDb "jeffgeerling.com" // tech)
|
|
||||||
(fromDb "jefftk.com" // tech)
|
|
||||||
(fromDb "jwz.org/blog" // tech // pol) # DNA lounge guy, loooong-time blogger
|
|
||||||
(fromDb "kill-the-newsletter.com/feeds/joh91bv7am2pnznv.xml" // pol) # Matt Levine - Money Stuff
|
|
||||||
(fromDb "kosmosghost.github.io/index.xml" // tech)
|
|
||||||
(fromDb "linmob.net" // tech)
|
|
||||||
(fromDb "lwn.net" // tech)
|
(fromDb "lwn.net" // tech)
|
||||||
(fromDb "lynalden.com" // pol)
|
|
||||||
(fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow
|
|
||||||
(fromDb "mg.lol" // tech)
|
|
||||||
(fromDb "mindingourway.com" // rat)
|
|
||||||
(fromDb "morningbrew.com/feed" // pol)
|
|
||||||
(fromDb "nixpkgs.news" // tech)
|
|
||||||
(fromDb "overcomingbias.com" // rat) # Robin Hanson
|
|
||||||
(fromDb "palladiummag.com" // uncat)
|
|
||||||
(fromDb "philosopher.coach" // rat) # Peter Saint-Andre -- side project of stpeter.im
|
|
||||||
(fromDb "pomeroyb.com" // tech)
|
|
||||||
(fromDb "postmarketos.org/blog" // tech)
|
|
||||||
(fromDb "preposterousuniverse.com" // rat) # Sean Carroll
|
|
||||||
(fromDb "project-insanity.org" // tech) # shared blog by a few NixOS devs, notably onny
|
|
||||||
(fromDb "putanumonit.com" // rat) # mostly dating topics. not advice, or humor, but looking through a social lens
|
|
||||||
(fromDb "richardcarrier.info" // rat)
|
|
||||||
(fromDb "rifters.com/crawl" // uncat) # No Moods, Ads or Cutesy Fucking Icons
|
|
||||||
(fromDb "righto.com" // tech) # Ken Shirriff
|
|
||||||
(fromDb "rootsofprogress.org" // rat) # Jason Crawford
|
|
||||||
(fromDb "samuel.dionne-riel.com" // tech) # SamuelDR
|
|
||||||
(fromDb "sagacioussuricata.com" // tech) # ian (Sanctuary)
|
|
||||||
(fromDb "semiaccurate.com" // tech)
|
|
||||||
(fromDb "sideways-view.com" // rat) # Paul Christiano
|
|
||||||
(fromDb "slatecave.net" // tech)
|
|
||||||
(fromDb "slimemoldtimemold.com" // rat)
|
|
||||||
(fromDb "spectrum.ieee.org" // tech)
|
|
||||||
(fromDb "stpeter.im/atom.xml" // pol)
|
|
||||||
(fromDb "thediff.co" // pol) # Byrne Hobart
|
|
||||||
(fromDb "thisweek.gnome.org" // tech)
|
|
||||||
(fromDb "tuxphones.com" // tech)
|
|
||||||
(fromDb "uninsane.org" // tech)
|
|
||||||
(fromDb "unintendedconsequenc.es" // rat)
|
|
||||||
(fromDb "vitalik.eth.limo" // tech) # Vitalik Buterin
|
|
||||||
(fromDb "weekinethereumnews.com" // tech)
|
|
||||||
(fromDb "willow.phantoma.online") # wizard@xyzzy.link
|
|
||||||
(fromDb "xn--gckvb8fzb.com" // tech)
|
|
||||||
(fromDb "xorvoid.com" // tech)
|
|
||||||
(mkSubstack "astralcodexten" // rat // daily) # Scott Alexander
|
|
||||||
(mkSubstack "eliqian" // rat // weekly)
|
|
||||||
(mkSubstack "oversharing" // pol // daily)
|
|
||||||
(mkSubstack "samkriss" // humor // infrequent)
|
|
||||||
(mkText "http://benjaminrosshoffman.com/feed" // pol // weekly)
|
|
||||||
(mkText "http://boginjr.com/feed" // tech // infrequent)
|
|
||||||
(mkText "https://forum.merveilles.town/rss.xml" // pol // infrequent) #quality RSS list here: <https://forum.merveilles.town/thread/57/share-your-rss-feeds%21-6/>
|
|
||||||
(mkText "https://jvns.ca/atom.xml" // tech // weekly) # Julia Evans
|
|
||||||
(mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent)
|
|
||||||
(mkText "https://nixos.org/blog/announcements-rss.xml" // tech // infrequent) # more nixos stuff here, but unclear how to subscribe: <https://nixos.org/blog/categories.html>
|
|
||||||
(mkText "https://nixos.org/blog/stories-rss.xml" // tech // weekly)
|
|
||||||
(mkText "https://solar.lowtechmagazine.com/posts/index.xml" // tech // weekly)
|
|
||||||
(mkText "https://www.stratechery.com/rss" // pol // weekly) # Ben Thompson
|
|
||||||
|
|
||||||
# (fromDb "balajis.com" // pol) # Balaji
|
|
||||||
# (fromDb "drewdevault.com" // tech)
|
|
||||||
# (fromDb "econlib.org" // pol)
|
|
||||||
# (fromDb "lesswrong.com" // rat)
|
# (fromDb "lesswrong.com" // rat)
|
||||||
# (fromDb "profectusmag.com" // pol) # some conservative/libertarian think tank
|
# (fromDb "econlib.org" // pol)
|
||||||
# (fromDb "thesideview.co" // uncat) # spiritual journal; RSS items are stubs
|
|
||||||
# (fromDb "theregister.com" // tech)
|
# AGGREGATORS (< 1 post/day)
|
||||||
# (fromDb "vitalik.ca" // tech) # moved to vitalik.eth.limo
|
(fromDb "palladiummag.com" // uncat)
|
||||||
# (fromDb "webcurious.co.uk" // uncat) # link aggregator; defunct?
|
(fromDb "profectusmag.com" // uncat)
|
||||||
# (mkSubstack "doomberg" // tech // weekly) # articles are all pay-walled
|
(fromDb "semiaccurate.com" // tech)
|
||||||
# (mkText "https://github.com/Kaiteki-Fedi/Kaiteki/commits/master.atom" // tech // infrequent)
|
(mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent)
|
||||||
|
(fromDb "tuxphones.com" // tech)
|
||||||
|
(fromDb "spectrum.ieee.org" // tech)
|
||||||
|
(fromDb "theregister.com" // tech)
|
||||||
|
(fromDb "thisweek.gnome.org" // tech)
|
||||||
|
# more nixos stuff here, but unclear how to subscribe: <https://nixos.org/blog/categories.html>
|
||||||
|
(mkText "https://nixos.org/blog/announcements-rss.xml" // tech // infrequent)
|
||||||
|
(mkText "https://nixos.org/blog/stories-rss.xml" // tech // weekly)
|
||||||
|
## n.b.: quality RSS list here: <https://forum.merveilles.town/thread/57/share-your-rss-feeds%21-6/>
|
||||||
|
(mkText "https://forum.merveilles.town/rss.xml" // pol // infrequent)
|
||||||
|
|
||||||
|
## No Moods, Ads or Cutesy Fucking Icons
|
||||||
|
(fromDb "rifters.com/crawl" // uncat)
|
||||||
|
|
||||||
|
# DEVELOPERS
|
||||||
|
(fromDb "blog.jmp.chat" // tech)
|
||||||
|
(fromDb "uninsane.org" // tech)
|
||||||
|
(fromDb "ascii.textfiles.com" // tech) # Jason Scott
|
||||||
|
(fromDb "xn--gckvb8fzb.com" // tech)
|
||||||
|
(fromDb "mg.lol" // tech)
|
||||||
|
# (fromDb "drewdevault.com" // tech)
|
||||||
|
## Ken Shirriff
|
||||||
|
(fromDb "righto.com" // tech)
|
||||||
|
## shared blog by a few NixOS devs, notably onny
|
||||||
|
(fromDb "project-insanity.org" // tech)
|
||||||
|
## Vitalik Buterin
|
||||||
|
(fromDb "vitalik.ca" // tech)
|
||||||
|
## ian (Sanctuary)
|
||||||
|
(fromDb "sagacioussuricata.com" // tech)
|
||||||
|
## Bunnie Juang
|
||||||
|
(fromDb "bunniestudios.com" // tech)
|
||||||
|
(fromDb "blog.danieljanus.pl" // tech)
|
||||||
|
(fromDb "ianthehenry.com" // tech)
|
||||||
|
(fromDb "bitbashing.io" // tech)
|
||||||
|
(fromDb "idiomdrottning.org" // uncat)
|
||||||
|
(mkText "http://boginjr.com/feed" // tech // infrequent)
|
||||||
|
(mkText "https://anish.lakhwara.com/home.html" // tech // weekly)
|
||||||
|
(fromDb "jefftk.com" // tech)
|
||||||
|
(fromDb "pomeroyb.com" // tech)
|
||||||
# (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly)
|
# (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly)
|
||||||
# (mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly) # Matt Levine (preview/paywalled)
|
|
||||||
];
|
|
||||||
|
|
||||||
videos = [
|
# TECH PROJECTS
|
||||||
(fromDb "youtube.com/@Channel5YouTube" // pol)
|
(fromDb "blog.rust-lang.org" // tech)
|
||||||
(fromDb "youtube.com/@ColdFusion")
|
|
||||||
(fromDb "youtube.com/@ContraPoints" // pol)
|
|
||||||
(fromDb "youtube.com/@Exurb1a")
|
|
||||||
(fromDb "youtube.com/@hbomberguy")
|
|
||||||
(fromDb "youtube.com/@JackStauber")
|
|
||||||
(fromDb "youtube.com/@NativLang")
|
|
||||||
(fromDb "youtube.com/@PolyMatter")
|
|
||||||
(fromDb "youtube.com/@TechnologyConnections" // tech)
|
|
||||||
(fromDb "youtube.com/@TheB1M")
|
|
||||||
(fromDb "youtube.com/@TomScottGo")
|
|
||||||
(fromDb "youtube.com/@Vihart")
|
|
||||||
(fromDb "youtube.com/@Vox")
|
|
||||||
# (fromDb "youtube.com/@Vsauce") # they're all like 1-minute long videos now? what happened @Vsauce?
|
|
||||||
|
|
||||||
# (fromDb "youtube.com/@rossmanngroup" // pol // tech) # Louis Rossmann
|
# (TECH; POL) COMMENTATORS
|
||||||
|
## Matt Webb -- engineering-ish, but dreamy
|
||||||
|
(fromDb "interconnected.org/home/feed" // rat)
|
||||||
|
(fromDb "edwardsnowden.substack.com" // pol // text)
|
||||||
|
## Julia Evans
|
||||||
|
(mkText "https://jvns.ca/atom.xml" // tech // weekly)
|
||||||
|
(mkText "http://benjaminrosshoffman.com/feed" // pol // weekly)
|
||||||
|
## Ben Thompson
|
||||||
|
(mkText "https://www.stratechery.com/rss" // pol // weekly)
|
||||||
|
## Balaji
|
||||||
|
(fromDb "balajis.com" // pol)
|
||||||
|
(fromDb "ben-evans.com/benedictevans" // pol)
|
||||||
|
(fromDb "lynalden.com" // pol)
|
||||||
|
(fromDb "austinvernon.site" // tech)
|
||||||
|
(mkSubstack "oversharing" // pol // daily)
|
||||||
|
(mkSubstack "byrnehobart" // pol // infrequent)
|
||||||
|
# (mkSubstack "doomberg" // tech // weekly) # articles are all pay-walled
|
||||||
|
## David Rosenthal
|
||||||
|
(fromDb "blog.dshr.org" // pol)
|
||||||
|
## Matt Levine
|
||||||
|
(mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly)
|
||||||
|
(fromDb "stpeter.im/atom.xml" // pol)
|
||||||
|
## Peter Saint-Andre -- side project of stpeter.im
|
||||||
|
(fromDb "philosopher.coach" // rat)
|
||||||
|
(fromDb "morningbrew.com/feed" // pol)
|
||||||
|
|
||||||
|
# RATIONALITY/PHILOSOPHY/ETC
|
||||||
|
(mkSubstack "samkriss" // humor // infrequent)
|
||||||
|
(fromDb "unintendedconsequenc.es" // rat)
|
||||||
|
(fromDb "applieddivinitystudies.com" // rat)
|
||||||
|
(fromDb "slimemoldtimemold.com" // rat)
|
||||||
|
(fromDb "richardcarrier.info" // rat)
|
||||||
|
(fromDb "gwern.net" // rat)
|
||||||
|
## Jason Crawford
|
||||||
|
(fromDb "rootsofprogress.org" // rat)
|
||||||
|
## Robin Hanson
|
||||||
|
(fromDb "overcomingbias.com" // rat)
|
||||||
|
## Scott Alexander
|
||||||
|
(mkSubstack "astralcodexten" // rat // daily)
|
||||||
|
## Paul Christiano
|
||||||
|
(fromDb "sideways-view.com" // rat)
|
||||||
|
## Sean Carroll
|
||||||
|
(fromDb "preposterousuniverse.com" // rat)
|
||||||
|
(mkSubstack "eliqian" // rat // weekly)
|
||||||
|
(mkText "https://acoup.blog/feed" // rat // weekly)
|
||||||
|
(fromDb "mindingourway.com" // rat)
|
||||||
|
|
||||||
|
## mostly dating topics. not advice, or humor, but looking through a social lens
|
||||||
|
(fromDb "putanumonit.com" // rat)
|
||||||
|
|
||||||
|
# LOCAL
|
||||||
|
(fromDb "capitolhillseattle.com" // pol)
|
||||||
|
|
||||||
|
# CODE
|
||||||
|
# (mkText "https://github.com/Kaiteki-Fedi/Kaiteki/commits/master.atom" // tech // infrequent)
|
||||||
];
|
];
|
||||||
|
|
||||||
images = [
|
images = [
|
||||||
(fromDb "catandgirl.com" // img // humor)
|
|
||||||
(fromDb "davidrevoy.com" // img // art)
|
|
||||||
(fromDb "grumpy.website" // img // humor)
|
|
||||||
(fromDb "miniature-calendar.com" // img // art // daily)
|
|
||||||
(fromDb "pbfcomics.com" // img // humor)
|
|
||||||
(fromDb "poorlydrawnlines.com/feed" // img // humor)
|
|
||||||
(fromDb "smbc-comics.com" // img // humor)
|
(fromDb "smbc-comics.com" // img // humor)
|
||||||
(fromDb "turnoff.us" // img // humor)
|
|
||||||
(fromDb "xkcd.com" // img // humor)
|
(fromDb "xkcd.com" // img // humor)
|
||||||
|
(fromDb "pbfcomics.com" // img // humor)
|
||||||
|
# (mkImg "http://dilbert.com/feed" // humor // daily)
|
||||||
|
(fromDb "poorlydrawnlines.com/feed" // img // humor)
|
||||||
|
|
||||||
|
# ART
|
||||||
|
(fromDb "miniature-calendar.com" // img // art // daily)
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
sane.feeds = texts ++ images ++ podcasts ++ videos;
|
sane.feeds = texts ++ images ++ podcasts;
|
||||||
|
|
||||||
assertions = builtins.map
|
assertions = builtins.map
|
||||||
(p: {
|
(p: {
|
||||||
|
|
|
@ -1,237 +1,136 @@
|
||||||
# docs
|
# docs
|
||||||
# - x-systemd options: <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
|
# - x-systemd options: <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
|
||||||
# - fuse options: `man mount.fuse`
|
|
||||||
|
|
||||||
{ config, lib, pkgs, sane-lib, utils, ... }:
|
{ pkgs, sane-lib, ... }:
|
||||||
|
|
||||||
let
|
let fsOpts = rec {
|
||||||
fsOpts = rec {
|
common = [
|
||||||
common = [
|
"_netdev"
|
||||||
"_netdev"
|
"noatime"
|
||||||
"noatime"
|
"user" # allow any user with access to the device to mount the fs
|
||||||
# user: allow any user with access to the device to mount the fs.
|
"x-systemd.requires=network-online.target"
|
||||||
# note that this requires a suid `mount` binary; see: <https://zameermanji.com/blog/2022/8/5/using-fuse-without-root-on-linux/>
|
"x-systemd.after=network-online.target"
|
||||||
"user"
|
"x-systemd.mount-timeout=10s" # how long to wait for mount **and** how long to wait for unmount
|
||||||
"x-systemd.requires=network-online.target"
|
];
|
||||||
"x-systemd.after=network-online.target"
|
auto = [ "x-systemd.automount" ];
|
||||||
"x-systemd.mount-timeout=10s" # how long to wait for mount **and** how long to wait for unmount
|
noauto = [ "noauto" ]; # don't mount as part of remote-fs.target
|
||||||
];
|
wg = [
|
||||||
# x-systemd.automount: mount the fs automatically *on first access*.
|
"x-systemd.requires=wireguard-wg-home.service"
|
||||||
# creates a `path-to-mount.automount` systemd unit.
|
"x-systemd.after=wireguard-wg-home.service"
|
||||||
automount = [ "x-systemd.automount" ];
|
];
|
||||||
# noauto: don't mount as part of remote-fs.target.
|
|
||||||
# N.B.: `remote-fs.target` is a dependency of multi-user.target, itself of graphical.target.
|
|
||||||
# hence, omitting `noauto` can slow down boots.
|
|
||||||
noauto = [ "noauto" ];
|
|
||||||
# lazyMount: defer mounting until first access from userspace.
|
|
||||||
# see: `man systemd.automount`, `man automount`, `man autofs`
|
|
||||||
lazyMount = noauto ++ automount;
|
|
||||||
wg = [
|
|
||||||
"x-systemd.requires=wireguard-wg-home.service"
|
|
||||||
"x-systemd.after=wireguard-wg-home.service"
|
|
||||||
];
|
|
||||||
|
|
||||||
fuse = [
|
ssh = common ++ [
|
||||||
"allow_other" # allow users other than the one who mounts it to access it. needed, if systemd is the one mounting this fs (as root)
|
"identityfile=/home/colin/.ssh/id_ed25519"
|
||||||
# allow_root: allow root to access files on this fs (if mounted by non-root, else it can always access them).
|
"allow_other"
|
||||||
# N.B.: if both allow_root and allow_other are specified, then only allow_root takes effect.
|
"default_permissions"
|
||||||
# "allow_root"
|
];
|
||||||
# default_permissions: enforce local permissions check. CRUCIAL if using `allow_other`.
|
sshColin = ssh ++ [
|
||||||
# w/o this, permissions mode of sshfs is like:
|
"transform_symlinks"
|
||||||
# - sshfs runs all remote commands as the remote user.
|
"idmap=user"
|
||||||
# - if a local user has local permissions to the sshfs mount, then their file ops are sent blindly across the tunnel.
|
"uid=1000"
|
||||||
# - `allow_other` allows *any* local user to access the mount, and hence any local user can now freely become the remote mapped user.
|
"gid=100"
|
||||||
# with default_permissions, sshfs doesn't tunnel file ops from users until checking that said user could perform said op on an equivalent local fs.
|
];
|
||||||
"default_permissions"
|
sshRoot = ssh ++ [
|
||||||
];
|
# we don't transform_symlinks because that breaks the validity of remote /nix stores
|
||||||
fuseColin = fuse ++ [
|
"sftp_server=/run/wrappers/bin/sudo\\040/run/current-system/sw/libexec/sftp-server"
|
||||||
"uid=1000"
|
];
|
||||||
"gid=100"
|
# in the event of hunt NFS mounts, consider:
|
||||||
];
|
# - <https://unix.stackexchange.com/questions/31979/stop-broken-nfs-mounts-from-locking-a-directory>
|
||||||
|
|
||||||
ssh = common ++ fuse ++ [
|
# NFS options: <https://linux.die.net/man/5/nfs>
|
||||||
"identityfile=/home/colin/.ssh/id_ed25519"
|
# actimeo=n = how long (in seconds) to cache file/dir attributes (default: 3-60s)
|
||||||
# i *think* idmap=user means that `colin` on `localhost` and `colin` on the remote are actually treated as the same user, even if their uid/gid differs?
|
# bg = retry failed mounts in the background
|
||||||
# i.e., local colin's id is translated to/from remote colin's id on every operation?
|
# retry=n = for how many minutes `mount` will retry NFS mount operation
|
||||||
"idmap=user"
|
# soft = on "major timeout", report I/O error to userspace
|
||||||
];
|
# retrans=n = how many times to retry a NFS request before giving userspace a "server not responding" error (default: 3)
|
||||||
sshColin = ssh ++ fuseColin ++ [
|
# timeo=n = number of *deciseconds* to wait for a response before retrying it (default: 600)
|
||||||
# follow_symlinks: remote files which are symlinks are presented to the local system as ordinary files (as the target of the symlink).
|
# note: client uses a linear backup, so the second request will have double this timeout, then triple, etc.
|
||||||
# if the symlink target does not exist, the presentation is unspecified.
|
nfs = common ++ [
|
||||||
# symlinks which point outside the mount ARE followed. so this is more capable than `transform_symlinks`
|
# "actimeo=10"
|
||||||
"follow_symlinks"
|
"bg"
|
||||||
# symlinks on the remote fs which are absolute paths are presented to the local system as relative symlinks pointing to the expected data on the remote fs.
|
"retrans=4"
|
||||||
# only symlinks which would point inside the mountpoint are translated.
|
"retry=0"
|
||||||
"transform_symlinks"
|
"soft"
|
||||||
];
|
"timeo=15"
|
||||||
# sshRoot = ssh ++ [
|
"nofail" # don't fail remote-fs.target when this mount fails (not an option for sshfs else would be common)
|
||||||
# # we don't transform_symlinks because that breaks the validity of remote /nix stores
|
];
|
||||||
# "sftp_server=/run/wrappers/bin/sudo\\040/run/current-system/sw/libexec/sftp-server"
|
};
|
||||||
# ];
|
|
||||||
# in the event of hunt NFS mounts, consider:
|
|
||||||
# - <https://unix.stackexchange.com/questions/31979/stop-broken-nfs-mounts-from-locking-a-directory>
|
|
||||||
|
|
||||||
# NFS options: <https://linux.die.net/man/5/nfs>
|
|
||||||
# actimeo=n = how long (in seconds) to cache file/dir attributes (default: 3-60s)
|
|
||||||
# bg = retry failed mounts in the background
|
|
||||||
# retry=n = for how many minutes `mount` will retry NFS mount operation
|
|
||||||
# intr = allow Ctrl+C to abort I/O (it will error with `EINTR`)
|
|
||||||
# soft = on "major timeout", report I/O error to userspace
|
|
||||||
# softreval = on "major timeout", service the request using known-stale cache results instead of erroring -- if such cache data exists
|
|
||||||
# retrans=n = how many times to retry a NFS request before giving userspace a "server not responding" error (default: 3)
|
|
||||||
# timeo=n = number of *deciseconds* to wait for a response before retrying it (default: 600)
|
|
||||||
# note: client uses a linear backup, so the second request will have double this timeout, then triple, etc.
|
|
||||||
# proto=udp = encapsulate protocol ops inside UDP packets instead of a TCP session.
|
|
||||||
# requires `nfsvers=3` and a kernel compiled with `NFS_DISABLE_UDP_SUPPORT=n`.
|
|
||||||
# UDP might be preferable to TCP because the latter is liable to hang for ~100s (kernel TCP timeout) after a link drop.
|
|
||||||
# however, even UDP has issues with `umount` hanging.
|
|
||||||
#
|
|
||||||
# N.B.: don't change these without first testing the behavior of sandboxed apps on a flaky network.
|
|
||||||
nfs = common ++ [
|
|
||||||
# "actimeo=5"
|
|
||||||
# "bg"
|
|
||||||
"retrans=1"
|
|
||||||
"retry=0"
|
|
||||||
# "intr"
|
|
||||||
"soft"
|
|
||||||
"softreval"
|
|
||||||
"timeo=30"
|
|
||||||
"nofail" # don't fail remote-fs.target when this mount fails (not an option for sshfs else would be common)
|
|
||||||
# "proto=udp" # default kernel config doesn't support NFS over UDP: <https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1964093> (see comment 11).
|
|
||||||
# "nfsvers=3" # NFSv4+ doesn't support UDP at *all*. it's ok to omit nfsvers -- server + client will negotiate v3 based on udp requirement. but omitting causes confusing mount errors when the server is *offline*, because the client defaults to v4 and thinks the udp option is a config error.
|
|
||||||
# "x-systemd.idle-timeout=10" # auto-unmount after this much inactivity
|
|
||||||
];
|
|
||||||
|
|
||||||
# manually perform a ftp mount via e.g.
|
|
||||||
# curlftpfs -o ftpfs_debug=2,user=anonymous:anonymous,connect_timeout=10 -f -s ftp://servo-hn /mnt/my-ftp
|
|
||||||
ftp = common ++ fuseColin ++ [
|
|
||||||
# "ftpfs_debug=2"
|
|
||||||
"user=colin:ipauth"
|
|
||||||
# connect_timeout=10: casting shows to T.V. fails partway through about half the time
|
|
||||||
"connect_timeout=20"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
remoteHome = host: {
|
|
||||||
sane.programs.sshfs-fuse.enableFor.system = true;
|
|
||||||
fileSystems."/mnt/${host}/home" = {
|
|
||||||
device = "colin@${host}:/home/colin";
|
|
||||||
fsType = "fuse.sshfs";
|
|
||||||
options = fsOpts.sshColin ++ fsOpts.lazyMount;
|
|
||||||
noCheck = true;
|
|
||||||
};
|
|
||||||
sane.fs."/mnt/${host}/home" = sane-lib.fs.wanted {
|
|
||||||
dir.acl.user = "colin";
|
|
||||||
dir.acl.group = "users";
|
|
||||||
dir.acl.mode = "0700";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
remoteServo = subdir: {
|
|
||||||
sane.programs.curlftpfs.enableFor.system = true;
|
|
||||||
sane.fs."/mnt/servo/${subdir}" = sane-lib.fs.wanted {
|
|
||||||
dir.acl.user = "colin";
|
|
||||||
dir.acl.group = "users";
|
|
||||||
dir.acl.mode = "0750";
|
|
||||||
};
|
|
||||||
fileSystems."/mnt/servo/${subdir}" = {
|
|
||||||
device = "ftp://servo-hn:/${subdir}";
|
|
||||||
noCheck = true;
|
|
||||||
fsType = "fuse.curlftpfs";
|
|
||||||
options = fsOpts.ftp ++ fsOpts.noauto ++ fsOpts.wg;
|
|
||||||
# fsType = "nfs";
|
|
||||||
# options = fsOpts.nfs ++ fsOpts.lazyMount ++ fsOpts.wg;
|
|
||||||
};
|
|
||||||
systemd.services."automount-servo-${utils.escapeSystemdPath subdir}" = let
|
|
||||||
fs = config.fileSystems."/mnt/servo/${subdir}";
|
|
||||||
in {
|
|
||||||
# this is a *flaky* network mount, especially on moby.
|
|
||||||
# if done as a normal autofs mount, access will eternally block when network is dropped.
|
|
||||||
# notably, this would block *any* sandboxed app which allows media access, whether they actually try to use that media or not.
|
|
||||||
# a practical solution is this: mount as a service -- instead of autofs -- and unmount on timeout error, in a restart loop.
|
|
||||||
# until the ftp handshake succeeds, nothing is actually mounted to the vfs, so this doesn't slow down any I/O when network is down.
|
|
||||||
description = "automount /mnt/servo/${subdir} in a fault-tolerant and non-blocking manner";
|
|
||||||
after = [ "network-online.target" ];
|
|
||||||
requires = [ "network-online.target" ];
|
|
||||||
wantedBy = [ "default.target" ];
|
|
||||||
|
|
||||||
serviceConfig.Type = "simple";
|
|
||||||
serviceConfig.ExecStart = lib.escapeShellArgs [
|
|
||||||
"/usr/bin/env"
|
|
||||||
"PATH=/run/current-system/sw/bin"
|
|
||||||
"mount.${fs.fsType}"
|
|
||||||
"-f" # foreground (i.e. don't daemonize)
|
|
||||||
"-s" # single-threaded (TODO: it's probably ok to disable this?)
|
|
||||||
"-o"
|
|
||||||
(lib.concatStringsSep "," (lib.filter (o: !lib.hasPrefix "x-systemd." o) fs.options))
|
|
||||||
fs.device
|
|
||||||
"/mnt/servo/${subdir}"
|
|
||||||
];
|
|
||||||
# not sure if this configures a linear, or exponential backoff.
|
|
||||||
# but the first restart will be after `RestartSec`, and the n'th restart (n = RestartSteps) will be RestartMaxDelaySec after the n-1'th exit.
|
|
||||||
serviceConfig.Restart = "always";
|
|
||||||
serviceConfig.RestartSec = "10s";
|
|
||||||
serviceConfig.RestartMaxDelaySec = "120s";
|
|
||||||
serviceConfig.RestartSteps = "5";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
lib.mkMerge [
|
{
|
||||||
{
|
# some services which use private directories error if the parent (/var/lib/private) isn't 700.
|
||||||
# some services which use private directories error if the parent (/var/lib/private) isn't 700.
|
sane.fs."/var/lib/private".dir.acl.mode = "0700";
|
||||||
sane.fs."/var/lib/private".dir.acl.mode = "0700";
|
|
||||||
|
|
||||||
# in-memory compressed RAM
|
# in-memory compressed RAM
|
||||||
# defaults to compressing at most 50% size of RAM
|
# defaults to compressing at most 50% size of RAM
|
||||||
# claimed compression ratio is about 2:1
|
# claimed compression ratio is about 2:1
|
||||||
# - but on moby w/ zstd default i see 4-7:1 (ratio lowers as it fills)
|
# - but on moby w/ zstd default i see 4-7:1 (ratio lowers as it fills)
|
||||||
# note that idle overhead is about 0.05% of capacity (e.g. 2B per 4kB page)
|
# note that idle overhead is about 0.05% of capacity (e.g. 2B per 4kB page)
|
||||||
# docs: <https://www.kernel.org/doc/Documentation/blockdev/zram.txt>
|
# docs: <https://www.kernel.org/doc/Documentation/blockdev/zram.txt>
|
||||||
#
|
#
|
||||||
# to query effectiveness:
|
# to query effectiveness:
|
||||||
# `cat /sys/block/zram0/mm_stat`. whitespace separated fields:
|
# `cat /sys/block/zram0/mm_stat`. whitespace separated fields:
|
||||||
# - *orig_data_size* (bytes)
|
# - *orig_data_size* (bytes)
|
||||||
# - *compr_data_size* (bytes)
|
# - *compr_data_size* (bytes)
|
||||||
# - mem_used_total (bytes)
|
# - mem_used_total (bytes)
|
||||||
# - mem_limit (bytes)
|
# - mem_limit (bytes)
|
||||||
# - mem_used_max (bytes)
|
# - mem_used_max (bytes)
|
||||||
# - *same_pages* (pages which are e.g. all zeros (consumes no additional mem))
|
# - *same_pages* (pages which are e.g. all zeros (consumes no additional mem))
|
||||||
# - *pages_compacted* (pages which have been freed thanks to compression)
|
# - *pages_compacted* (pages which have been freed thanks to compression)
|
||||||
# - huge_pages (incompressible)
|
# - huge_pages (incompressible)
|
||||||
#
|
#
|
||||||
# see also:
|
# see also:
|
||||||
# - `man zramctl`
|
# - `man zramctl`
|
||||||
zramSwap.enable = true;
|
zramSwap.enable = true;
|
||||||
# how much ram can be swapped into the zram device.
|
# how much ram can be swapped into the zram device.
|
||||||
# this shouldn't be higher than the observed compression ratio.
|
# this shouldn't be higher than the observed compression ratio.
|
||||||
# the default is 50% (why?)
|
# the default is 50% (why?)
|
||||||
# 100% should be "guaranteed" safe so long as the data is even *slightly* compressible.
|
# 100% should be "guaranteed" safe so long as the data is even *slightly* compressible.
|
||||||
# but it decreases working memory under the heaviest of loads by however much space the compressed memory occupies (e.g. 50% if 2:1; 25% if 4:1)
|
# but it decreases working memory under the heaviest of loads by however much space the compressed memory occupies (e.g. 50% if 2:1; 25% if 4:1)
|
||||||
zramSwap.memoryPercent = 100;
|
zramSwap.memoryPercent = 100;
|
||||||
|
|
||||||
# environment.pathsToLink = [
|
# fileSystems."/mnt/servo-nfs" = {
|
||||||
# # needed to achieve superuser access for user-mounted filesystems (see sshRoot above)
|
# device = "servo-hn:/";
|
||||||
# # we can only link whole directories here, even though we're only interested in pkgs.openssh
|
# noCheck = true;
|
||||||
# "/libexec"
|
# fsType = "nfs";
|
||||||
# ];
|
# options = fsOpts.nfs ++ fsOpts.auto ++ fsOpts.wg;
|
||||||
|
# };
|
||||||
|
fileSystems."/mnt/servo-nfs/media" = {
|
||||||
|
device = "servo-hn:/media";
|
||||||
|
noCheck = true;
|
||||||
|
fsType = "nfs";
|
||||||
|
options = fsOpts.nfs ++ fsOpts.auto ++ fsOpts.wg;
|
||||||
|
};
|
||||||
|
# fileSystems."/mnt/servo-media-nfs" = {
|
||||||
|
# device = "servo-hn:/media";
|
||||||
|
# noCheck = true;
|
||||||
|
# fsType = "nfs";
|
||||||
|
# options = fsOpts.common ++ fsOpts.auto;
|
||||||
|
# };
|
||||||
|
sane.fs."/mnt/servo-media" = sane-lib.fs.wantedSymlinkTo "/mnt/servo-nfs/media";
|
||||||
|
|
||||||
programs.fuse.userAllowOther = true; #< necessary for `allow_other` or `allow_root` options.
|
fileSystems."/mnt/desko-home" = {
|
||||||
}
|
device = "colin@desko:/home/colin";
|
||||||
|
fsType = "fuse.sshfs";
|
||||||
|
options = fsOpts.sshColin ++ fsOpts.noauto;
|
||||||
|
noCheck = true;
|
||||||
|
};
|
||||||
|
sane.fs."/mnt/desko-home" = sane-lib.fs.wantedDir;
|
||||||
|
fileSystems."/mnt/desko-root" = {
|
||||||
|
device = "colin@desko:/";
|
||||||
|
fsType = "fuse.sshfs";
|
||||||
|
options = fsOpts.sshRoot ++ fsOpts.noauto;
|
||||||
|
noCheck = true;
|
||||||
|
};
|
||||||
|
sane.fs."/mnt/desko-root" = sane-lib.fs.wantedDir;
|
||||||
|
|
||||||
(remoteHome "crappy")
|
environment.pathsToLink = [
|
||||||
(remoteHome "desko")
|
# needed to achieve superuser access for user-mounted filesystems (see optionsRoot above)
|
||||||
(remoteHome "lappy")
|
# we can only link whole directories here, even though we're only interested in pkgs.openssh
|
||||||
(remoteHome "moby")
|
"/libexec"
|
||||||
# this granularity of servo media mounts is necessary to support sandboxing:
|
];
|
||||||
# for flaky mounts, we can only bind the mountpoint itself into the sandbox,
|
|
||||||
# so it's either this or unconditionally bind all of media/.
|
environment.systemPackages = [
|
||||||
(remoteServo "media/archive")
|
pkgs.sshfs-fuse
|
||||||
(remoteServo "media/Books")
|
];
|
||||||
(remoteServo "media/collections")
|
}
|
||||||
# (remoteServo "media/datasets")
|
|
||||||
(remoteServo "media/games")
|
|
||||||
(remoteServo "media/Music")
|
|
||||||
(remoteServo "media/Pictures/macros")
|
|
||||||
(remoteServo "media/torrents")
|
|
||||||
(remoteServo "media/Videos")
|
|
||||||
(remoteServo "playground")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
44
hosts/common/hardware.nix
Normal file
44
hosts/common/hardware.nix
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{ lib, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
boot.initrd.supportedFilesystems = [ "ext4" "btrfs" "ext2" "ext3" "vfat" ];
|
||||||
|
# useful emergency utils
|
||||||
|
boot.initrd.extraUtilsCommands = ''
|
||||||
|
copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfstune
|
||||||
|
'';
|
||||||
|
boot.kernelParams = [ "boot.shell_on_fail" ];
|
||||||
|
# other kernelParams:
|
||||||
|
# "boot.trace"
|
||||||
|
# "systemd.log_level=debug"
|
||||||
|
# "systemd.log_target=console"
|
||||||
|
|
||||||
|
# hack in the `boot.shell_on_fail` arg since that doesn't always seem to work.
|
||||||
|
boot.initrd.preFailCommands = "allowShell=1";
|
||||||
|
|
||||||
|
# default: 4 (warn). 7 is debug
|
||||||
|
boot.consoleLogLevel = 7;
|
||||||
|
|
||||||
|
boot.loader.grub.enable = lib.mkDefault false;
|
||||||
|
boot.loader.generic-extlinux-compatible.enable = lib.mkDefault true;
|
||||||
|
|
||||||
|
# non-free firmware
|
||||||
|
hardware.enableRedistributableFirmware = true;
|
||||||
|
|
||||||
|
# powertop will default to putting USB devices -- including HID -- to sleep after TWO SECONDS
|
||||||
|
powerManagement.powertop.enable = false;
|
||||||
|
|
||||||
|
services.logind.extraConfig = ''
|
||||||
|
# don’t shutdown when power button is short-pressed
|
||||||
|
HandlePowerKey=ignore
|
||||||
|
'';
|
||||||
|
|
||||||
|
# services.snapper.configs = {
|
||||||
|
# root = {
|
||||||
|
# subvolume = "/";
|
||||||
|
# extraConfig = {
|
||||||
|
# ALLOW_USERS = "colin";
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# services.snapper.snapshotInterval = "daily";
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./fs.nix
|
./keyring
|
||||||
./mime.nix
|
./mime.nix
|
||||||
./ssh.nix
|
./ssh.nix
|
||||||
./xdg-dirs.nix
|
./xdg-dirs.nix
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
{ config, lib, ... }:
|
|
||||||
{
|
|
||||||
sane.user.persist.byStore.plaintext = [
|
|
||||||
"archive"
|
|
||||||
"dev"
|
|
||||||
# TODO: records should be private
|
|
||||||
"records"
|
|
||||||
"ref"
|
|
||||||
"tmp"
|
|
||||||
"use"
|
|
||||||
"Books/local"
|
|
||||||
"Music"
|
|
||||||
"Pictures/albums"
|
|
||||||
"Pictures/cat"
|
|
||||||
"Pictures/from"
|
|
||||||
"Pictures/Screenshots" #< XXX: something is case-sensitive about this?
|
|
||||||
"Pictures/Photos"
|
|
||||||
"Videos/local"
|
|
||||||
|
|
||||||
# these are persisted simply to save on RAM.
|
|
||||||
# ~/.cache/nix can become several GB.
|
|
||||||
# mesa_shader_cache is < 10 MB.
|
|
||||||
# TODO: integrate with sane.programs.sandbox?
|
|
||||||
".cache/mesa_shader_cache"
|
|
||||||
".cache/nix"
|
|
||||||
];
|
|
||||||
sane.user.persist.byStore.private = [
|
|
||||||
"knowledge"
|
|
||||||
];
|
|
||||||
|
|
||||||
# convenience
|
|
||||||
sane.user.fs = let
|
|
||||||
persistEnabled = config.sane.persist.enable;
|
|
||||||
in {
|
|
||||||
".persist/private" = lib.mkIf persistEnabled { symlink.target = config.sane.persist.stores.private.origin; };
|
|
||||||
".persist/plaintext" = lib.mkIf persistEnabled { symlink.target = config.sane.persist.stores.plaintext.origin; };
|
|
||||||
".persist/ephemeral" = lib.mkIf persistEnabled { symlink.target = config.sane.persist.stores.cryptClearOnBoot.origin; };
|
|
||||||
|
|
||||||
"nixos".symlink.target = "dev/nixos";
|
|
||||||
|
|
||||||
"Books/servo".symlink.target = "/mnt/servo/media/Books";
|
|
||||||
"Videos/servo".symlink.target = "/mnt/servo/media/Videos";
|
|
||||||
"Pictures/servo-macros".symlink.target = "/mnt/servo/media/Pictures/macros";
|
|
||||||
};
|
|
||||||
}
|
|
17
hosts/common/home/keyring/default.nix
Normal file
17
hosts/common/home/keyring/default.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{ config, pkgs, sane-lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
init-keyring = pkgs.static-nix-shell.mkBash {
|
||||||
|
pname = "init-keyring";
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
sane.user.persist.private = [ ".local/share/keyrings" ];
|
||||||
|
|
||||||
|
sane.user.fs."private/.local/share/keyrings/default" = {
|
||||||
|
generated.command = [ "${init-keyring}/bin/init-keyring" ];
|
||||||
|
wantedBy = [ config.sane.fs."/home/colin/private".unit ];
|
||||||
|
wantedBeforeBy = [ ]; # don't created this as part of `multi-user.target`
|
||||||
|
};
|
||||||
|
}
|
21
hosts/common/home/keyring/init-keyring
Executable file
21
hosts/common/home/keyring/init-keyring
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#!nix-shell -i bash
|
||||||
|
# initializes the default libsecret keyring (used by gnome-keyring) if not already initialized.
|
||||||
|
# this initializes it to be plaintext/unencrypted.
|
||||||
|
|
||||||
|
ringdir=/home/colin/private/.local/share/keyrings
|
||||||
|
if test -f "$ringdir/default"
|
||||||
|
then
|
||||||
|
echo 'keyring already initialized: not doing anything'
|
||||||
|
else
|
||||||
|
keyring="$ringdir/Default_keyring.keyring"
|
||||||
|
|
||||||
|
echo 'initializing default user keyring:' "$keyring.new"
|
||||||
|
echo '[keyring]' > "$keyring.new"
|
||||||
|
echo 'display-name=Default keyring' >> "$keyring.new"
|
||||||
|
echo 'lock-on-idle=false' >> "$keyring.new"
|
||||||
|
echo 'lock-after=false' >> "$keyring.new"
|
||||||
|
chown colin:users "$keyring.new"
|
||||||
|
# closest to an atomic update we can achieve
|
||||||
|
mv "$keyring.new" "$keyring" && echo -n "Default_keyring" > "$ringdir/default"
|
||||||
|
fi
|
|
@ -1,94 +1,28 @@
|
||||||
# TODO: move into modules/users.nix
|
{ config, lib, ...}:
|
||||||
{ config, lib, pkgs, ...}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
# [ ProgramConfig ]
|
|
||||||
enabledPrograms = builtins.filter
|
|
||||||
(p: p.enabled)
|
|
||||||
(builtins.attrValues config.sane.programs);
|
|
||||||
|
|
||||||
# [ ProgramConfig ]
|
|
||||||
enabledProgramsWithPackage = builtins.filter (p: p.package != null) enabledPrograms;
|
|
||||||
|
|
||||||
# [ { "<mime-type>" = { prority, desktop } ]
|
|
||||||
enabledWeightedMimes = builtins.map weightedMimes enabledPrograms;
|
|
||||||
|
|
||||||
# ProgramConfig -> { "<mime-type>" = { priority, desktop }; }
|
# ProgramConfig -> { "<mime-type>" = { priority, desktop }; }
|
||||||
weightedMimes = prog: builtins.mapAttrs
|
weightedMimes = prog: builtins.mapAttrs (_key: desktop: { priority = prog.mime.priority; desktop = desktop; }) prog.mime.associations;
|
||||||
(_key: desktop: {
|
|
||||||
priority = prog.mime.priority; desktop = desktop;
|
|
||||||
})
|
|
||||||
prog.mime.associations;
|
|
||||||
|
|
||||||
# [ { "<mime-type>" = { priority, desktop } ]; } ] -> { "<mime-type>" = [ { priority, desktop } ... ]; }
|
# [ { "<mime-type>" = { priority, desktop } ]; } ] -> { "<mime-type>" = [ { priority, desktop } ... ]; }
|
||||||
mergeMimes = mimes: lib.foldAttrs (item: acc: [item] ++ acc) [] mimes;
|
mergeMimes = mimes: lib.foldAttrs (item: acc: [item] ++ acc) [] mimes;
|
||||||
|
|
||||||
# [ { priority, desktop } ... ] -> Self
|
# [ { priority, desktop } ... ] -> Self
|
||||||
sortOneMimeType = associations: builtins.sort
|
sortOneMimeType = associations: builtins.sort (l: r: assert l.priority != r.priority; l.priority < r.priority) associations;
|
||||||
(l: r: lib.throwIf
|
|
||||||
(l.priority == r.priority)
|
|
||||||
"${l.desktop} and ${r.desktop} share a preferred mime type with identical priority ${builtins.toString l.priority} (and so the desired association is ambiguous)"
|
|
||||||
(l.priority < r.priority)
|
|
||||||
)
|
|
||||||
associations;
|
|
||||||
sortMimes = mimes: builtins.mapAttrs (_k: sortOneMimeType) mimes;
|
sortMimes = mimes: builtins.mapAttrs (_k: sortOneMimeType) mimes;
|
||||||
# { "<mime-type>"} = [ { priority, desktop } ... ]; } -> { "<mime-type>" = [ "<desktop>" ... ]; }
|
removePriorities = mimes: builtins.mapAttrs (_k: associations: builtins.map (a: a.desktop) associations) mimes;
|
||||||
removePriorities = mimes: builtins.mapAttrs
|
|
||||||
(_k: associations: builtins.map (a: a.desktop) associations)
|
|
||||||
mimes;
|
|
||||||
# { "<mime-type>" = [ "<desktop>" ... ]; } -> { "<mime-type>" = "<desktop1>;<desktop2>;..."; }
|
|
||||||
formatDesktopLists = mimes: builtins.mapAttrs
|
|
||||||
(_k: desktops: lib.concatStringsSep ";" desktops)
|
|
||||||
mimes;
|
|
||||||
|
|
||||||
mimeappsListPkg = pkgs.writeTextDir "share/applications/mimeapps.list" (
|
|
||||||
lib.generators.toINI { } {
|
|
||||||
"Default Applications" = formatDesktopLists (removePriorities (sortMimes (mergeMimes enabledWeightedMimes)));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
localShareApplicationsPkg = (pkgs.symlinkJoin {
|
|
||||||
name = "user-local-share-applications";
|
|
||||||
paths = builtins.map
|
|
||||||
(p: builtins.toString p.package)
|
|
||||||
(enabledProgramsWithPackage ++ [ { package=mimeappsListPkg; } ]);
|
|
||||||
}).overrideAttrs (orig: {
|
|
||||||
# like normal symlinkJoin, but don't error if the path doesn't exist
|
|
||||||
buildCommand = ''
|
|
||||||
mkdir -p $out/share/applications
|
|
||||||
for i in $(cat $pathsPath); do
|
|
||||||
if [ -e "$i/share/applications" ]; then
|
|
||||||
${pkgs.buildPackages.xorg.lndir}/bin/lndir -silent $i/share/applications $out/share/applications
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
runHook postBuild
|
|
||||||
'';
|
|
||||||
postBuild = ''
|
|
||||||
# rebuild `mimeinfo.cache`, used by file openers to show the list of *all* apps, not just the user's defaults.
|
|
||||||
${pkgs.buildPackages.desktop-file-utils}/bin/update-desktop-database $out/share/applications
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
# [ ProgramConfig ]
|
||||||
|
enabledPrograms = builtins.filter (p: p.enabled) (builtins.attrValues config.sane.programs);
|
||||||
|
# [ { "<mime-type>" = { prority, desktop } ]
|
||||||
|
enabledWeightedMimes = builtins.map weightedMimes enabledPrograms;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# the xdg mime type for a file can be found with:
|
# the xdg mime type for a file can be found with:
|
||||||
# - `xdg-mime query filetype path/to/thing.ext`
|
# - `xdg-mime query filetype path/to/thing.ext`
|
||||||
# the default handler for a mime type can be found with:
|
# the default handler for a mime type can be found with:
|
||||||
# - `xdg-mime query default <mimetype>` (e.g. x-scheme-handler/http)
|
# - `xdg-mime query default <mimetype>` (e.g. x-scheme-handler/http)
|
||||||
# the nix-configured handler can be found `nix-repl > :lf . > hostConfigs.desko.xdg.mime.defaultApplications`
|
|
||||||
#
|
|
||||||
# glib/gio is queried via glib.bin output:
|
|
||||||
# - `gio mime x-scheme-handler/https`
|
|
||||||
# - `gio open <path_or_url>`
|
|
||||||
# - `gio launch </path/to/app.desktop>`
|
|
||||||
#
|
#
|
||||||
# we can have single associations or a list of associations.
|
# we can have single associations or a list of associations.
|
||||||
# there's also options to *remove* [non-default] associations from specific apps
|
# there's also options to *remove* [non-default] associations from specific apps
|
||||||
# N.B.: don't use nixos' `xdg.mime` option becaue that caues `/share/applications` to be linked into the whole system,
|
xdg.mime.enable = true;
|
||||||
# which limits what i can do around sandboxing. getting the default associations to live in ~/ makes it easier to expose
|
xdg.mime.defaultApplications = removePriorities (sortMimes (mergeMimes enabledWeightedMimes));
|
||||||
# the associations to apps selectively.
|
|
||||||
# xdg.mime.enable = true;
|
|
||||||
# xdg.mime.defaultApplications = removePriorities (sortMimes (mergeMimes enabledWeightedMimes));
|
|
||||||
|
|
||||||
sane.user.fs.".local/share/applications".symlink.target = "${localShareApplicationsPkg}/share/applications";
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# ssh key is stored in private storage
|
# ssh key is stored in private storage
|
||||||
sane.user.persist.byStore.private = [
|
sane.user.persist.private = [
|
||||||
{ type = "file"; path = ".ssh/id_ed25519"; }
|
{ type = "file"; path = ".ssh/id_ed25519"; }
|
||||||
];
|
];
|
||||||
sane.user.fs.".ssh/id_ed25519.pub" = lib.mkIf (user-pubkey != null) {
|
sane.user.fs.".ssh/id_ed25519.pub" = lib.mkIf (user-pubkey != null) {
|
||||||
|
|
|
@ -3,18 +3,13 @@
|
||||||
{
|
{
|
||||||
# XDG defines things like ~/Desktop, ~/Downloads, etc.
|
# XDG defines things like ~/Desktop, ~/Downloads, etc.
|
||||||
# these clutter the home, so i mostly don't use them.
|
# these clutter the home, so i mostly don't use them.
|
||||||
# note that several of these are not actually standardized anywhere.
|
|
||||||
# some are even non-conventional, like:
|
|
||||||
# - XDG_PHOTOS_DIR: only works because i patch e.g. megapixels
|
|
||||||
sane.user.fs.".config/user-dirs.dirs".symlink.text = ''
|
sane.user.fs.".config/user-dirs.dirs".symlink.text = ''
|
||||||
XDG_DESKTOP_DIR="$HOME/.xdg/Desktop"
|
XDG_DESKTOP_DIR="$HOME/.xdg/Desktop"
|
||||||
XDG_DOCUMENTS_DIR="$HOME/dev"
|
XDG_DOCUMENTS_DIR="$HOME/dev"
|
||||||
XDG_DOWNLOAD_DIR="$HOME/tmp"
|
XDG_DOWNLOAD_DIR="$HOME/tmp"
|
||||||
XDG_MUSIC_DIR="$HOME/Music"
|
XDG_MUSIC_DIR="$HOME/Music"
|
||||||
XDG_PHOTOS_DIR="$HOME/Pictures/Photos"
|
|
||||||
XDG_PICTURES_DIR="$HOME/Pictures"
|
XDG_PICTURES_DIR="$HOME/Pictures"
|
||||||
XDG_PUBLICSHARE_DIR="$HOME/.xdg/Public"
|
XDG_PUBLICSHARE_DIR="$HOME/.xdg/Public"
|
||||||
XDG_SCREENSHOTS_DIR="$HOME/Pictures/Screenshots"
|
|
||||||
XDG_TEMPLATES_DIR="$HOME/.xdg/Templates"
|
XDG_TEMPLATES_DIR="$HOME/.xdg/Templates"
|
||||||
XDG_VIDEOS_DIR="$HOME/Videos"
|
XDG_VIDEOS_DIR="$HOME/Videos"
|
||||||
'';
|
'';
|
||||||
|
@ -22,6 +17,4 @@
|
||||||
# prevent `xdg-user-dirs-update` from overriding/updating our config
|
# prevent `xdg-user-dirs-update` from overriding/updating our config
|
||||||
# see <https://manpages.ubuntu.com/manpages/bionic/man5/user-dirs.conf.5.html>
|
# see <https://manpages.ubuntu.com/manpages/bionic/man5/user-dirs.conf.5.html>
|
||||||
sane.user.fs.".config/user-dirs.conf".symlink.text = "enabled=False";
|
sane.user.fs.".config/user-dirs.conf".symlink.text = "enabled=False";
|
||||||
|
|
||||||
sane.user.fs.".config/environment.d/30-user-dirs.conf".symlink.target = "../user-dirs.dirs";
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
{ lib, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
# TODO: this should be populated per-host
|
|
||||||
sane.hosts.by-name."crappy" = {
|
|
||||||
ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMIvSQAGKqmymXIL4La9B00LPxBIqWAr5AsJxk3UQeY5";
|
|
||||||
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMN0cpRAloCBOE5/2wuzgik35iNDv5KLceWMCVaa7DIQ";
|
|
||||||
# wg-home.pubkey = "TODO";
|
|
||||||
# wg-home.ip = "10.0.10.55";
|
|
||||||
lan-ip = "10.78.79.55";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.hosts.by-name."desko" = {
|
|
||||||
ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPU5GlsSfbaarMvDA20bxpSZGWviEzXGD8gtrIowc1pX";
|
|
||||||
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFw9NoRaYrM6LbDd3aFBc4yyBlxGQn8HjeHd/dZ3CfHk";
|
|
||||||
wg-home.pubkey = "17PMZssYi0D4t2d0vbmhjBKe1sGsE8kT8/dod0Q2CXc=";
|
|
||||||
wg-home.ip = "10.0.10.22";
|
|
||||||
lan-ip = "10.78.79.52";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.hosts.by-name."lappy" = {
|
|
||||||
ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDpmFdNSVPRol5hkbbCivRhyeENzb9HVyf9KutGLP2Zu";
|
|
||||||
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSJnqmVl9/SYQ0btvGb0REwwWY8wkdkGXQZfn/1geEc";
|
|
||||||
wg-home.pubkey = "FTUWGw2p4/cEcrrIE86PWVnqctbv8OYpw8Gt3+dC/lk=";
|
|
||||||
wg-home.ip = "10.0.10.20";
|
|
||||||
lan-ip = "10.78.79.53";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.hosts.by-name."moby" = {
|
|
||||||
# ssh.authorized = lib.mkDefault false; # moby's too easy to hijack: don't let it ssh places
|
|
||||||
ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrR+gePnl0nV/vy7I5BzrGeyVL+9eOuXHU1yNE3uCwU";
|
|
||||||
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1N/IT3nQYUD+dBlU1sTEEVMxfOyMkrrDeyHcYgnJvw";
|
|
||||||
wg-home.pubkey = "I7XIR1hm8bIzAtcAvbhWOwIAabGkuEvbWH/3kyIB1yA=";
|
|
||||||
wg-home.ip = "10.0.10.48";
|
|
||||||
lan-ip = "10.78.79.54";
|
|
||||||
};
|
|
||||||
|
|
||||||
sane.hosts.by-name."servo" = {
|
|
||||||
ssh.authorized = lib.mkDefault false; # servo presents too many services to the internet: easy atack vector
|
|
||||||
ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPS1qFzKurAdB9blkWomq8gI1g0T3sTs9LsmFOj5VtqX";
|
|
||||||
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOfdSmFkrVT6DhpgvFeQKm3Fh9VKZ9DbLYOPOJWYQ0E8";
|
|
||||||
wg-home.pubkey = "roAw+IUFVtdpCcqa4khB385Qcv9l5JAB//730tyK4Wk=";
|
|
||||||
wg-home.ip = "10.0.10.5";
|
|
||||||
wg-home.endpoint = "uninsane.org:51820";
|
|
||||||
lan-ip = "10.78.79.51";
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -4,9 +4,6 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
# partially supported in nixpkgs <repo:nixos/nixpkgs:nixos/modules/misc/ids.nix>
|
|
||||||
sane.ids.networkmanager.uid = 57; #< nixpkgs unofficially reserves this, to match networkmanager's gid
|
|
||||||
|
|
||||||
# legacy servo users, some are inconvenient to migrate
|
# legacy servo users, some are inconvenient to migrate
|
||||||
sane.ids.dhcpcd.gid = 991;
|
sane.ids.dhcpcd.gid = 991;
|
||||||
sane.ids.dhcpcd.uid = 992;
|
sane.ids.dhcpcd.uid = 992;
|
||||||
|
@ -21,7 +18,7 @@
|
||||||
sane.ids.matrix-appservice-irc.uid = 993;
|
sane.ids.matrix-appservice-irc.uid = 993;
|
||||||
sane.ids.matrix-appservice-irc.gid = 992;
|
sane.ids.matrix-appservice-irc.gid = 992;
|
||||||
|
|
||||||
# greetd (legacy)
|
# greetd (used by sway)
|
||||||
sane.ids.greeter.uid = 999;
|
sane.ids.greeter.uid = 999;
|
||||||
sane.ids.greeter.gid = 999;
|
sane.ids.greeter.gid = 999;
|
||||||
|
|
||||||
|
@ -47,21 +44,6 @@
|
||||||
sane.ids.sftpgo.gid = 2410;
|
sane.ids.sftpgo.gid = 2410;
|
||||||
sane.ids.trust-dns.uid = 2411;
|
sane.ids.trust-dns.uid = 2411;
|
||||||
sane.ids.trust-dns.gid = 2411;
|
sane.ids.trust-dns.gid = 2411;
|
||||||
sane.ids.export.gid = 2412;
|
|
||||||
sane.ids.nfsuser.uid = 2413;
|
|
||||||
sane.ids.media.gid = 2414;
|
|
||||||
sane.ids.ntfy-sh.uid = 2415;
|
|
||||||
sane.ids.ntfy-sh.gid = 2415;
|
|
||||||
sane.ids.monero.uid = 2416;
|
|
||||||
sane.ids.monero.gid = 2416;
|
|
||||||
sane.ids.slskd.uid = 2417;
|
|
||||||
sane.ids.slskd.gid = 2417;
|
|
||||||
sane.ids.bitcoind-mainnet.uid = 2418;
|
|
||||||
sane.ids.bitcoind-mainnet.gid = 2418;
|
|
||||||
sane.ids.clightning.uid = 2419;
|
|
||||||
sane.ids.clightning.gid = 2419;
|
|
||||||
sane.ids.nix-serve.uid = 2420;
|
|
||||||
sane.ids.nix-serve.gid = 2420;
|
|
||||||
|
|
||||||
sane.ids.colin.uid = 1000;
|
sane.ids.colin.uid = 1000;
|
||||||
sane.ids.guest.uid = 1100;
|
sane.ids.guest.uid = 1100;
|
||||||
|
@ -76,12 +58,9 @@
|
||||||
sane.ids.systemd-oom.uid = 2005;
|
sane.ids.systemd-oom.uid = 2005;
|
||||||
sane.ids.systemd-oom.gid = 2005;
|
sane.ids.systemd-oom.gid = 2005;
|
||||||
sane.ids.wireshark.gid = 2006;
|
sane.ids.wireshark.gid = 2006;
|
||||||
sane.ids.nixremote.uid = 2007;
|
|
||||||
sane.ids.nixremote.gid = 2007;
|
|
||||||
|
|
||||||
# found on graphical hosts
|
# found on graphical hosts
|
||||||
sane.ids.nm-iodine.uid = 2101; # desko/moby/lappy
|
sane.ids.nm-iodine.uid = 2101; # desko/moby/lappy
|
||||||
sane.ids.seat.gid = 2102;
|
|
||||||
|
|
||||||
# found on desko host
|
# found on desko host
|
||||||
# from services.usbmuxd
|
# from services.usbmuxd
|
||||||
|
@ -102,8 +81,4 @@
|
||||||
sane.ids.rtkit.gid = 2307;
|
sane.ids.rtkit.gid = 2307;
|
||||||
# phosh
|
# phosh
|
||||||
sane.ids.feedbackd.gid = 2308;
|
sane.ids.feedbackd.gid = 2308;
|
||||||
|
|
||||||
# new moby users
|
|
||||||
sane.ids.eg25-control.uid = 2309;
|
|
||||||
sane.ids.eg25-control.gid = 2309;
|
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user