Compare commits

..

1 Commits

Author SHA1 Message Date
a17ad5fbf2 moby: build/deploy kaiteki 2022-10-09 04:17:04 -07:00
690 changed files with 8281 additions and 132219 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,2 @@
/keep
result
result-*
/secrets/local.nix
/working

View File

@@ -8,7 +8,7 @@ keys:
- &host_servo age1tzlyex2z6t88tg9h82943e39shxhmqeyr7ywhlwpdjmyqsndv3qq27x0rf
- &host_moby age18vq5ktwgeaysucvw9t67drqmg5zd5c5k3le34yqxckkfj7wqdqgsd4ejmt
creation_rules:
- path_regex: secrets/common*
- path_regex: secrets/universal*
key_groups:
- age:
- *user_desko_colin
@@ -23,25 +23,11 @@ creation_rules:
key_groups:
- age:
- *user_desko_colin
- *user_lappy_colin
- *user_servo_colin
- *host_servo
- path_regex: secrets/desko*
- path_regex: secrets/desko.yaml$
key_groups:
- age:
- *user_desko_colin
- *user_lappy_colin
- *host_desko
- path_regex: secrets/lappy*
key_groups:
- age:
- *user_lappy_colin
- *user_desko_colin
- *host_lappy
- path_regex: secrets/moby*
key_groups:
- age:
- *user_desko_colin
- *user_lappy_colin
- *user_moby_colin
- *host_moby

123
README.md
View File

@@ -1,123 +0,0 @@
## What's Here
this is the top-level repo from which i configure/deploy all my NixOS machines:
- desktop
- laptop
- server
- mobile phone (Pinephone)
everything outside of <./hosts/> and <./secrets/> is intended for export, to be importable for use by 3rd parties.
the only hard dependency for my exported pkgs/modules should be [nixpkgs][nixpkgs].
building <./hosts/> will require [sops][sops].
you might specifically be interested in these files (elaborated further in #key-points-of-interest):
- [`sxmo-utils-latest`](./pkgs/additional/sxmo-utils/default.nix)
- [example SXMO deployment](./hosts/modules/gui/sxmo/default.nix)
- [my implementation of impermanence](./modules/persist/default.nix)
- my way of deploying dotfiles/configuring programs per-user:
- <./modules/fs/default.nix>
- <./modules/programs.nix>
- <./modules/users.nix>
[nixpkgs]: https://github.com/NixOS/nixpkgs
[sops]: https://github.com/Mic92/sops-nix
[uninsane-org]: https://uninsane.org
## Using This Repo In Your Own Config
this should be a pretty "standard" flake. just reference it, and import either
- `nixosModules.sane` (for the modules)
- `overlays.pkgs` (for the packages)
or follow the instructions [here][NUR] to use it via the Nix User Repositories.
[NUR]: https://nur.nix-community.org/
## Layout
- `doc/`
- instructions for tasks i find myself doing semi-occasionally in this repo.
- `hosts/`
- 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,
you won't likely be depending on anything in this directory.
- `integrations/`
- code intended for consumption by external tools (e.g. the Nix User Repos)
- `modules/`
- config which is gated behind `enable` flags, in similar style to nixpkgs'
`nixos/` directory.
- if you depend on this repo, it's most likely for something in this directory.
- `nixpatches/`
- literally, diffs i apply atop upstream nixpkgs before performing further eval.
- `overlays/`
- exposed via the `overlays` output in `flake.nix`.
- predominantly a list of `callPackage` directives.
- `pkgs/`
- derivations for things not yet packaged in nixpkgs.
- derivations for things from nixpkgs which i need to `override` for some reason.
- inline code for wholly custom packages (e.g. `pkgs/additional/sane-scripts/` for CLI tools
that are highly specific to my setup).
- `scripts/`
- scripts which aren't reachable on a deployed system, but may aid manual deployments
- `secrets/`
- encrypted keys, API tokens, anything which one or more of my machines needs
read access to but shouldn't be world-readable.
- not much to see here
- `templates/`
- exposed via the `templates` output in `flake.nix`.
- used to instantiate short-lived environments.
- used to auto-fill the boiler-plate portions of new packages.
## Key Points of Interest
i.e. you might find value in using these in your own config:
- `modules/fs/`
- use this to statically define leafs and nodes anywhere in the filesystem,
not just inside `/nix/store`.
- e.g. specify that `/var/www` should be:
- owned by a specific user/group
- set to a specific mode
- symlinked to some other path
- populated with some statically-defined data
- populated according to some script
- created as a dependency of some service (e.g. `nginx`)
- values defined here are applied neither at evaluation time _nor_ at activation time.
- rather, they become systemd services.
- systemd manages dependencies
- e.g. link `/var/www -> /mnt/my-drive/www` only _after_ `/mnt/my-drive/www` appears)
- this is akin to using [Home Manager's][home-manager] file API -- the part which lets you
statically define `~/.config` files -- just with a different philosophy.
- `modules/persist/`
- my alternative to the Impermanence module.
- this builds atop `modules/fs/` to achieve things stock impermanence can't:
- persist things to encrypted storage which is unlocked at login time (pam_mount).
- "persist" cache directories -- to free up RAM -- but auto-wipe them on mount
and encrypt them to ephemeral keys so they're unreadable post shutdown/unmount.
- `modules/programs.nix`
- like nixpkgs' `programs` options, but allows both system-wide or per-user deployment.
- allows `fs` and `persist` config values to be gated behind program deployment:
- e.g. `/home/<user>/.mozilla/firefox` is persisted only for users who
`sane.programs.firefox.enableFor.user."<user>" = true;`
- `modules/users.nix`
- convenience layer atop the above modules so that you can just write
`fs.".config/git"` instead of `fs."/home/colin/.config/git"`
some things in here could easily find broader use. if you would find benefit in
them being factored out of my config, message me and we could work to make that happen.
[home-manager]: https://github.com/nix-community/home-manager
## Mirrors
this repo exists in a few known locations:
- primary: <https://git.uninsane.org/colin/nix-files>
- mirror: <https://github.com/nix-community/nur-combined/tree/master/repos/colinsane>
## Contact
if you want to contact me for questions, or collaborate to split something useful into a shared repo, etc,
you can reach me via any method listed [here](https://uninsane.org/about).
patches, for this repo or any other i host, will be warmly welcomed in any manner you see fit:
`git send-email`, DM'ing the patch over Matrix/Lemmy/ActivityPub/etc, even a literal PR where you
link me to your own clone.

125
TODO.md
View File

@@ -1,113 +1,16 @@
## BUGS
- why i need to manually restart `wireguard-wg-ovpns` on servo periodically
- else DNS fails
- ringer (i.e. dino incoming call) doesn't prevent moby from sleeping
## REFACTORING:
### sops/secrets
- attach secrets to the thing they're used by (sane.programs)
- rework secrets to leverage `sane.fs`
- remove sops activation script as it's covered by my systemd sane.fs impl
### roles
- allow any host to take the role of `uninsane.org`
- will make it easier to test new services?
### upstreaming
- split out a sxmo module usable by NUR consumers
- bump nodejs version in lemmy-ui
- add updateScripts to all my packages in nixpkgs
- fix lightdm-mobile-greeter for newer libhandy
- port zecwallet-lite to a from-source build
- REVIEW/integrate jellyfin dataDir config: <https://github.com/NixOS/nixpkgs/pull/233617>
- remove `libsForQt5.callPackage` broadly: <https://github.com/NixOS/nixpkgs/issues/180841>
#### upstreaming to non-nixpkgs repos
- gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844>
- sxmo: add new app entries
# features/tweaks
- emoji picker application
- find a Masto/Pleroma app which works on mobile
- remove hardcoded uid/gids outside of allocations.nix (used in impermanence code -- replace with username/groupname)
## IMPROVEMENTS:
### security/resilience
- matrix/ntfy: automatically add the ntfy.uninsane.org push URL as part of synapse launch
- ntfy: use a more secure topic
- validate duplicity backups!
- encrypt more ~ dirs (~/archives, ~/records, ..?)
- best to do this after i know for sure i have good backups
- have `sane.programs` be wrapped such that they run in a cgroup?
- at least, only give them access to the portion of the fs they *need*.
- Android takes approach of giving each app its own user: could hack that in here.
- **systemd-run** takes a command and runs it in a temporary scope (cgroup)
- presumably uses the same options as systemd services
- see e.g. <https://github.com/NixOS/nixpkgs/issues/113903#issuecomment-857296349>
- flatpak does this, somehow
- apparmor? SElinux? (desktop) "portals"?
- see Spectrum OS; Alyssa Ross; etc
- bubblewrap-based sandboxing: <https://github.com/nixpak/nixpak>
- canaries for important services
- e.g. daily email checks; daily backup checks
- integrate `nix check` into Gitea actions?
### user experience
#### moby
- fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html>
- install apps:
- display QR codes for WiFi endpoints: <https://linuxphoneapps.org/apps/noappid.wisperwind.wifi2qr/>
- shopping list: <https://linuxphoneapps.org/apps/ro.hume.cosmin.shoppinglist/>
- offline Wikipedia
- 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
- extend width to 100% of portrait mode
- moby: tune GPS
- run only geoclue, and not gpsd, to save power?
- tune QGPS setting in eg25-control, for less jitter?
- direct mepo to prefer gpsd, with fallback to geoclue, for better accuracy?
- configure geoclue to do some smoothing?
- manually do smoothing, as some layer between mepo and geoclue/gpsd?
- moby: show battery state on ssh login
- moby: improve gPodder launch time
- sxmo: port to swaybar like i use on desktop
- users in #sxmo claim it's way better perf
- sxmo: fix youtube scripts (package youtube-cli)
- moby: theme GTK apps (i.e. non-adwaita styles)
- combine multiple icon themes to get one which has the full icon set?
- get adwaita-icon-theme to ship everything even when cross-compiled?
- especially, make the menubar collapsible
- try Gradience tool specifically for theming adwaita? <https://linuxphoneapps.org/apps/com.github.gradienceteam.gradience/>
- phog: remove the gnome-shell runtime dependency to save hella closure size
#### non-moby
- neovim: set up language server (lsp; rnix-lsp; nvim-lspconfig)
- Helix: make copy-to-system clipboard be the default
- firefox/librewolf: persist history
- just not cookies or tabs
- package Nix/NixOS docs for Zeal
- install [doc-browser](https://github.com/qwfy/doc-browser)
- this supports both dash (zeal) *and* the datasets from <https://devdocs.io> (which includes nix!)
- install [devhelp](https://wiki.gnome.org/Apps/Devhelp) (gnome)
- have xdg-open parse `<repo:...> URIs (or adjust them so that it _can_ parse)
- 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)
- email: fix so that local mail doesn't go to junk
- git sendmail flow adds the DKIM signatures, but gets delivered locally w/o having the sig checked, so goes into Junk
- could change junk filter from "no DKIM success" to explicit "DKIM failed"
### perf
- add `pkgs.impure-cached.<foo>` package set to build things with ccache enabled
- every package here can be auto-generated, and marked with some env var so that it doesn't pollute the pure package set
- would be super handy for package prototyping!
- 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:
- migrate MAME cabinet to nix
- boot it from PXE from servo?
- deploy to new server, and use it as a remote builder
- enable IPv6
- package lemonade lemmy app: <https://linuxphoneapps.org/apps/ml.mdwalters.lemonade/>
# speed up cross compiling
- <https://nixos.wiki/wiki/Cross_Compiling>
- <https://nixos.wiki/wiki/NixOS_on_ARM>
```nix
overlays = [{ ... }: {
nixpkgs.crossSystem.system = "aarch64-linux";
}];
```
- <https://github.com/nix-community/aarch64-build-box>
- apply for access to the community arm build box

View File

@@ -1,9 +0,0 @@
# limited, non-flake interface to this repo.
# this file exposes the same view into `pkgs` which the flake would see when evaluated.
#
# the primary purpose of this file is so i can run `updateScript`s which expect
# the root to be `default.nix`
{ pkgs ? import <nixpkgs> {} }:
pkgs.appendOverlays [
(import ./overlays/all.nix)
]

View File

@@ -1,13 +0,0 @@
to ship `pkgs.foo` on some host, either:
- add it as an entry in `suggestedPrograms` to the appropriate category in `hosts/common/programs/assorted.nix`, or
- `sane.programs.foo.enableFor.user.colin = true` in `hosts/by-name/myhost/default.nix`
if the program needs customization (persistence, configs, secrets):
- add a file for it at `hosts/common/programs/<foo>.nix`
- set the options, `sane.programs.foo.{fs,persist}`
if it's unclear what fs paths a program uses:
- run one of these commands, launch the program, run it again, and `diff`:
- `du -x --apparent-size ~`
- `find ~ -xdev`
- or, inspect the whole tmpfs root with `ncdu -x /`

157
flake.lock generated
View File

@@ -1,93 +1,140 @@
{
"nodes": {
"flake-utils": {
"home-manager": {
"inputs": {
"systems": "systems"
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1687709756,
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
"lastModified": 1656169755,
"narHash": "sha256-Nlnm4jeQWEGjYrE6hxi/7HYHjBSZ/E0RtjCYifnNsWk=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "4a3d01fb53f52ac83194081272795aa4612c2381",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "nix-community",
"ref": "release-22.05",
"repo": "home-manager",
"type": "github"
}
},
"impermanence": {
"locked": {
"lastModified": 1661933071,
"narHash": "sha256-RFgfzldpbCvS+H2qwH+EvNejvqs+NhPVD5j1I7HQQPY=",
"owner": "nix-community",
"repo": "impermanence",
"rev": "def994adbdfc28974e87b0e4c949e776207d5557",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "impermanence",
"type": "github"
}
},
"mobile-nixos": {
"flake": false,
"locked": {
"lastModified": 1694749521,
"narHash": "sha256-MiVokKlpcJmfoGuWAMeW1En7gZ5hk0rCQArYm6P9XCc=",
"lastModified": 1664852186,
"narHash": "sha256-t0FhmTf3qRs8ScR8H9Rq7FAxptNELLSpxZG2ALL1HnE=",
"owner": "nixos",
"repo": "mobile-nixos",
"rev": "d25d3b87e7f300d8066e31d792337d9cd7ecd23b",
"rev": "ca872f1a617674c4045e880aab8a45037e73700b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "d25d3b87e7f300d8066e31d792337d9cd7ecd23b",
"repo": "mobile-nixos",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1665081174,
"narHash": "sha256-6hsmzdhdy8Kbvl5e0xZNE83pW3fKQvNiobJkM6KQrgA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "598f83ebeb2235435189cf84d844b8b73e858e0f",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs-22_05": {
"locked": {
"lastModified": 1665279158,
"narHash": "sha256-TpbWNzoJ5RaZ302dzvjY2o//WxtOJuYT3CnDj5N69Hs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3783bcfb8ec54e0de26feccfc6cc36b8e202ed5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1698544399,
"narHash": "sha256-vhRmPyEyoPkrXF2iykBsWHA05MIaOSmMRLMF7Hul6+s=",
"lastModified": 1665132027,
"narHash": "sha256-zoHPqSQSENt96zTk6Mt1AP+dMNqQDshXKQ4I6MfjP80=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d87c5d8c41c9b3b39592563242f3a448b5cc4bc9",
"rev": "9ecc270f02b09b2f6a76b98488554dd842797357",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.05",
"repo": "nixpkgs",
"type": "github"
"id": "nixpkgs",
"ref": "nixos-22.05",
"type": "indirect"
}
},
"nixpkgs-unpatched": {
"nixpkgs_2": {
"locked": {
"lastModified": 1698611440,
"narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=",
"owner": "nixos",
"lastModified": 1665197809,
"narHash": "sha256-dRUzv/zNYV2EYtnxFG31pPBk0nErT+MBTu6ZJHm1o2A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735",
"rev": "7b06206fa24198912cea58de690aa4943f238fbf",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"home-manager": "home-manager",
"impermanence": "impermanence",
"mobile-nixos": "mobile-nixos",
"nixpkgs-unpatched": "nixpkgs-unpatched",
"sops-nix": "sops-nix",
"uninsane-dot-org": "uninsane-dot-org"
"nixpkgs": "nixpkgs",
"nixpkgs-stable": "nixpkgs-stable",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs-unpatched"
],
"nixpkgs-stable": "nixpkgs-stable"
"nixpkgs": "nixpkgs_2",
"nixpkgs-22_05": "nixpkgs-22_05"
},
"locked": {
"lastModified": 1698548647,
"narHash": "sha256-7c03OjBGqnwDW0FBaBc+NjfEBxMkza+dxZGJPyIzfFE=",
"lastModified": 1665289655,
"narHash": "sha256-j1Q9mNBhbzeJykhObiXwEGres9qvP4vH7gxdJ+ihkLI=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "632c3161a6cc24142c8e3f5529f5d81042571165",
"rev": "0ce0449e6404c4ff9d1b7bd657794ae5ca54deb3",
"type": "github"
},
"original": {
@@ -95,42 +142,6 @@
"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": 1698634059,
"narHash": "sha256-+Oyv6vDyCtBzab/5cTG0nUrHD9gj7KgGfD4D1Rn4fCk=",
"ref": "refs/heads/master",
"rev": "2419750ca98fc04af42c91e50c49a29c68d465d2",
"revCount": 210,
"type": "git",
"url": "https://git.uninsane.org/colin/uninsane"
},
"original": {
"type": "git",
"url": "https://git.uninsane.org/colin/uninsane"
}
}
},
"root": "root",

535
flake.nix
View File

@@ -1,176 +1,71 @@
# FLAKE FEEDBACK:
# - if flake inputs are meant to be human-readable, a human should be able to easily track them down given the URL.
# - this is not the case with registry URLs, like `nixpkgs/nixos-22.11`.
# - this is marginally the case with schemes like `github:nixos/nixpkgs`.
# - given the *existing* `git+https://` scheme, i propose expressing github URLs similarly:
# - `github+https://github.com/nixos/nixpkgs/tree/nixos-22.11`
# - this would allow for the same optimizations as today's `github:nixos/nixpkgs`, but without obscuring the source.
# a code reader could view the source being referenced simply by clicking the https:// portion of that URI.
# - need some way to apply local patches to inputs.
#
#
# DEVELOPMENT DOCS:
# - Flake docs: <https://nixos.wiki/wiki/Flakes>
# - Flake RFC: <https://github.com/tweag/rfcs/blob/flakes/rfcs/0049-flakes.md>
# - Discussion: <https://github.com/NixOS/rfcs/pull/49>
# docs:
# - <https://nixos.wiki/wiki/Flakes>
# - <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";
nixpkgs-stable.url = "nixpkgs/nixos-22.05";
nixpkgs.url = "nixpkgs/nixos-unstable";
mobile-nixos = {
# <https://github.com/nixos/mobile-nixos>
# only used for building disk images, not relevant after deployment
# TODO: replace with something else. commit `0f3ac0bef1aea70254a3bae35e3cc2561623f4c1`
# replaces the imageBuilder with a "new implementation from celun" and wildly breaks my use.
# pinning to d25d3b... is equivalent to holding at 2023-09-15
url = "github:nixos/mobile-nixos?ref=d25d3b87e7f300d8066e31d792337d9cd7ecd23b";
url = "github:nixos/mobile-nixos";
flake = false;
};
sops-nix = {
# <https://github.com/Mic92/sops-nix>
# used to distribute secrets to my hosts
url = "github:Mic92/sops-nix";
# inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "nixpkgs-unpatched";
};
uninsane-dot-org = {
# provides the package to deploy <https://uninsane.org>, used only when building the servo host
url = "git+https://git.uninsane.org/colin/uninsane";
# inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "nixpkgs-unpatched";
home-manager = {
url = "github:nix-community/home-manager/release-22.05";
inputs.nixpkgs.follows = "nixpkgs";
};
# TODO: set these up to follow our nixpkgs?
sops-nix.url = "github:Mic92/sops-nix";
impermanence.url = "github:nix-community/impermanence";
};
outputs = {
self,
nixpkgs-unpatched,
mobile-nixos,
sops-nix,
uninsane-dot-org,
...
}@inputs:
outputs = { self, nixpkgs, nixpkgs-stable, mobile-nixos, home-manager, sops-nix, impermanence }:
let
patchedPkgs = system: nixpkgs.legacyPackages.${system}.applyPatches {
name = "nixpkgs-patched-uninsane";
src = nixpkgs;
patches = import ./nixpatches/list.nix nixpkgs.legacyPackages.${system}.fetchpatch;
};
# return something which behaves like `pkgs`, for the provided system
# `local` = architecture of builder. `target` = architecture of the system beying deployed to
nixpkgsFor = local: target: import (patchedPkgs target) { crossSystem = target; localSystem = local; };
# evaluate ONLY our overlay, for the provided system
customPackagesFor = local: target: import ./pkgs/overlay.nix (nixpkgsFor local target) (nixpkgsFor local target);
decl-machine = { name, local, target }:
let
inherit (builtins) attrNames elem listToAttrs map mapAttrs;
# redefine some nixpkgs `lib` functions to avoid the infinite recursion
# of if we tried to use patched `nixpkgs.lib` as part of the patching process.
mapAttrs' = f: set:
listToAttrs (map (attr: f attr set.${attr}) (attrNames set));
optionalAttrs = cond: attrs: if cond then attrs else {};
# mapAttrs but without the `name` argument
mapAttrValues = f: mapAttrs (_: f);
# rather than apply our nixpkgs patches as a flake input, do that here instead.
# this (temporarily?) resolves the bad UX wherein a subflake residing in the same git
# repo as the main flake causes the main flake to have an unstable hash.
nixpkgs = (import ./nixpatches/flake.nix).outputs {
self = nixpkgs;
nixpkgs = nixpkgs-unpatched;
} // {
# provide values that nixpkgs ordinarily sources from the flake.lock file,
# inaccessible to it here because of the import-from-derivation.
# rev and shortRev seem to not always exist (e.g. if the working tree is dirty),
# so those are made conditional.
#
# these values impact the name of a produced nixos system. having date/rev in the
# `readlink /run/current-system` store path helps debuggability.
inherit (self) lastModifiedDate lastModified;
} // optionalAttrs (self ? rev) {
inherit (self) rev;
} // optionalAttrs (self ? shortRev) {
inherit (self) shortRev;
};
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;
nixosSystem = import ((patchedPkgs target) + "/nixos/lib/eval-config.nix");
in (nixosSystem {
# by default the local system is the same as the target, employing emulation when they differ
system = target;
specialArgs = { inherit mobile-nixos home-manager impermanence; };
modules = [
./modules
./machines/${name}
(import ./helpers/set-hostname.nix name)
home-manager.nixosModule
impermanence.nixosModule
sops-nix.nixosModules.sops
{
nixpkgs.config.allowUnfree = true;
nixpkgs.overlays = [
(import "${mobile-nixos}/overlay/overlay.nix")
(import ./pkgs/overlay.nix)
(next: prev: rec {
# non-emulated packages build *from* local *for* target.
# for large packages like the linux kernel which are expensive to build under emulation,
# the config can explicitly pull such packages from `pkgs.cross` to do more efficient cross-compilation.
cross = (nixpkgsFor local target) // (customPackagesFor local target);
stable = import nixpkgs-stable { system = target; };
# pinned packages:
electrum = stable.electrum;
})
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
decl-bootable-machine = { name, local, target }: rec {
nixosConfiguration = decl-machine { inherit name local target; };
# 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)
@@ -183,308 +78,24 @@
# - 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
hostPkgs = mapAttrValues (host: host.config.system.build.pkgs) self.nixosConfigurations;
hostPrograms = 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;
in
(mobile final prev)
// (uninsane 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";
sanePkgs = import ./pkgs { inherit pkgs; };
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 $@
'';
# pkg updating.
# a cleaner alternative lives here: <https://discourse.nixos.org/t/how-can-i-run-the-updatescript-of-personal-packages/25274/2>
mkUpdater = attrPath: {
type = "app";
program = let
pkg = pkgs.lib.getAttrFromPath attrPath sanePkgs;
strAttrPath = pkgs.lib.concatStringsSep "." attrPath;
commandArgv = pkg.updateScript.command or pkg.updateScript;
command = pkgs.lib.escapeShellArgs commandArgv;
in builtins.toString (pkgs.writeShellScript "update-${strAttrPath}" ''
export UPDATE_NIX_NAME=${pkg.name}
export UPDATE_NIX_PNAME=${pkg.pname}
export UPDATE_NIX_OLD_VERSION=${pkg.version}
export UPDATE_NIX_ATTR_PATH=${strAttrPath}
${command}
'');
};
mkUpdatersNoAliases = opts: basePath: pkgs.lib.concatMapAttrs
(name: pkg:
if pkg.recurseForDerivations or false then {
"${name}" = mkUpdaters opts (basePath ++ [ name ]);
} else if pkg.updateScript or null != null then {
"${name}" = mkUpdater (basePath ++ [ name ]);
} else {}
)
(pkgs.lib.getAttrFromPath basePath sanePkgs);
mkUpdaters = { ignore ? [] }@opts: basePath:
let
updaters = mkUpdatersNoAliases opts basePath;
invokeUpdater = name: pkg:
let
fullPath = basePath ++ [ name ];
doUpdateByDefault = !builtins.elem fullPath ignore;
# in case `name` has a `.` in it, we have to quote it
escapedPath = builtins.map (p: ''"${p}"'') fullPath;
updatePath = builtins.concatStringsSep "." ([ "update" "pkgs" ] ++ escapedPath);
in pkgs.lib.optionalString doUpdateByDefault (
pkgs.lib.escapeShellArgs [
"nix" "run" ".#${updatePath}"
]
);
in {
type = "app";
program = builtins.toString (pkgs.writeShellScript
(builtins.concatStringsSep "-" (["update"] ++ basePath))
(builtins.concatStringsSep
"\n"
(pkgs.lib.mapAttrsToList invokeUpdater updaters)
)
);
} // updaters;
in {
help = {
type = "app";
program = let
helpMsg = builtins.toFile "nixos-config-help-message" ''
commands:
- `nix run '.#help'`
- show this message
- `nix run '.#update.pkgs'`
- updates every package
- `nix run '.#update.feeds'`
- updates metadata for all feeds
- `nix run '.#init-feed' <url>`
- `nix run '.#deploy-{lappy,moby,moby-test,servo}' [nixos-rebuild args ...]`
- `nix run '.#check'`
- make sure all systems build; NUR evaluates
specific build targets of interest:
- `nix build '.#imgs.rescue'`
'';
in builtins.toString (pkgs.writeShellScript "nixos-config-help" ''
cat ${helpMsg}
echo ""
echo "complete flake structure:"
nix flake show --option allow-import-from-derivation true
'');
};
update.pkgs = mkUpdaters { ignore = [ ["feeds"] ]; } [];
update.feeds = mkUpdaters {} [ "feeds" ];
init-feed = {
type = "app";
program = "${pkgs.feeds.init-feed}";
};
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"}'';
};
sync-moby = {
# copy music from the current device to moby
# TODO: should i actually sync from /mnt/servo-media/Music instead of the local drive?
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-moby" ''
sudo mount /mnt/moby-home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music ~/Music /mnt/moby-home/Music
'');
};
sync-lappy = {
# copy music from servo to lappy
# can run this from any device that has ssh access to lappy
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-lappy" ''
sudo mount /mnt/lappy-home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music /mnt/servo-media/Music /mnt/lappy-home/Music
'');
};
check = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "check-all" ''
nix run '.#check.nur'
RC0=$?
nix run '.#check.host-configs'
RC1=$?
nix run '.#check.rescue'
RC2=$?
echo "nur: $RC0"
echo "host-configs: $RC1"
echo "rescue: $RC2"
exit $(($RC0 | $RC1 | $RC2))
'');
};
check.nur = {
# `nix run '.#check-nur'`
# validates that my repo can be included in the Nix User Repository
type = "app";
program = builtins.toString (pkgs.writeShellScript "check-nur" ''
cd ${./.}/integrations/nur
NIX_PATH= NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix-env -f . -qa \* --meta --xml \
--allowed-uris https://static.rust-lang.org \
--option restrict-eval true \
--option allow-import-from-derivation true \
--drv-path --show-trace \
-I nixpkgs=$(nix-instantiate --find-file nixpkgs) \
-I ../../ \
| tee # tee to prevent interactive mode
'');
};
check.host-configs = {
type = "app";
program = let
checkHost = host: ''
nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} -j2 $@
RC_${host}=$?
'';
in builtins.toString (pkgs.writeShellScript
"check-host-configs"
''
${checkHost "desko"}
${checkHost "lappy"}
${checkHost "servo"}
${checkHost "moby"}
${checkHost "rescue"}
echo "desko: $RC_desko"
echo "lappy: $RC_lappy"
echo "servo: $RC_servo"
echo "moby: $RC_moby"
echo "rescue: $RC_rescue"
exit $(($RC_desko | $RC_lappy | $RC_servo | $RC_moby | $RC_rescue))
''
);
};
check.rescue = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "check-rescue" ''
nix build -v '.#imgs.rescue' --out-link ./result-rescue-img -j2
'');
};
};
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";
};
};
# - `nixos-rebuild --flake './#<machine>' switch`
img = nixosConfiguration.config.system.build.img;
};
machines.servo = decl-bootable-machine { name = "servo"; local = "aarch64-linux"; target = "aarch64-linux"; };
machines.desko = decl-bootable-machine { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; };
machines.lappy = decl-bootable-machine { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; };
machines.moby = decl-bootable-machine { name = "moby"; local = "aarch64-linux"; target = "aarch64-linux"; };
# special cross-compiled variant, to speed up deploys from an x86 box to the arm target
# note that these *do* produce different store paths, because the closure for the tools used to cross compile
# v.s. emulate differ.
# so deploying moby-cross and then moby incurs some rebuilding.
machines.moby-cross = decl-bootable-machine { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; };
machines.rescue = decl-bootable-machine { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; };
in {
nixosConfigurations = builtins.mapAttrs (name: value: value.nixosConfiguration) machines;
imgs = builtins.mapAttrs (name: value: value.img) machines;
packages.x86_64-linux = customPackagesFor "x86_64-linux" "x86_64-linux";
packages.aarch64-linux = customPackagesFor "aarch64-linux" "aarch64-linux";
};
}

4
helpers/set-hostname.nix Normal file
View File

@@ -0,0 +1,4 @@
hostName: { ... }:
{
networking.hostName = hostName;
}

View File

@@ -1,7 +0,0 @@
## directory structure
- by-name/<hostname>: configuration which is evaluated _only_ for the given hostname
- common/: configuration which applies to all hosts
- modules/: nixpkgs-style modules which may be used by multiple hosts, but configured separately per host.
- ideally no module here has effect unless `enable`d
- however, `enable` may default to true
- and in practice some of these modules surely aren't fully "disableable"

View File

@@ -1,55 +0,0 @@
{ config, pkgs, ... }:
{
imports = [
./fs.nix
];
# sane.guest.enable = true;
# services.distccd.enable = true;
# sane.programs.distcc.enableFor.user.guest = true;
sops.secrets.colin-passwd.neededForUsers = true;
sane.roles.build-machine.enable = true;
sane.roles.ac = true;
sane.roles.client = true;
sane.roles.dev-machine = true;
sane.services.wg-home.enable = true;
sane.services.wg-home.ip = config.sane.hosts.by-name."desko".wg-home.ip;
sane.services.duplicity.enable = true;
sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path;
sane.gui.sway.enable = true;
sane.programs.iphoneUtils.enableFor.user.colin = true;
sane.programs.steam.enableFor.user.colin = true;
sane.programs.guiApps.suggestedPrograms = [ "desktopGuiApps" ];
sane.programs.consoleUtils.suggestedPrograms = [ "consoleMediaUtils" "desktopConsoleUtils" ];
# sane.programs.devPkgs.enableFor.user.colin = true;
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
# needed to use libimobiledevice/ifuse, for iphone sync
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
# defaults to something like:
# - hourly snapshots
# - auto cleanup; keep the last 10 hourlies, last 10 daylies, last 10 monthlys.
services.snapper.configs.nix = {
# TODO: for the impermanent setup, we'd prefer to just do /nix/persist,
# but that also requires setting up the persist dir as a subvol
SUBVOLUME = "/nix";
# TODO: ALLOW_USERS doesn't seem to work. still need `sudo snapper -c nix list`
ALLOW_USERS = [ "colin" ];
};
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
system.stateVersion = "21.05";
}

View File

@@ -1,43 +0,0 @@
# 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 = "greetd-sway-gtkgreet";
noidle = true; #< power button requires 1s hold, which makes it impractical to be dealing with.
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";
# N.B.: thinkpad's power button requires a full second press to do anything
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";
# if sxmo doesn't know the device, it can't decide whether to use one_button or three_button mode
# and so it just wouldn't handle any button inputs (sxmo_hook_inputhandler.sh not on path)
SXMO_DEVICE_NAME = "three_button_touchscreen";
};
package = (pkgs.sxmo-utils-latest.override { preferSystemd = true; }).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
'';
});
};
}

View File

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

View File

@@ -1,174 +0,0 @@
# 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, ...:
# - Linux Phone Apps: <https://linuxphoneapps.org/>
# - massive mobile-friendly app database
# - Mobian wiki: <https://wiki.mobian-project.org/doku.php?id=start>
# - recommended apps, chatrooms
{ config, pkgs, lib, ... }:
{
imports = [
./bootloader.nix
./fs.nix
./gps.nix
./kernel.nix
./polyfill.nix
];
sane.roles.client = true;
sane.zsh.showDeadlines = false; # unlikely to act on them when in shell
sane.services.wg-home.enable = true;
sane.services.wg-home.ip = config.sane.hosts.by-name."moby".wg-home.ip;
# XXX colin: phosh doesn't work well with passwordless login,
# so set this more reliable default password should anything go wrong
users.users.colin.initialPassword = "147147";
services.getty.autologinUser = "root"; # allows for emergency maintenance?
sops.secrets.colin-passwd.neededForUsers = true;
sane.gui.sxmo.enable = true;
sane.programs.guiApps.suggestedPrograms = [ "handheldGuiApps" ];
# sane.programs.consoleUtils.enableFor.user.colin = false;
# sane.programs.guiApps.enableFor.user.colin = false;
sane.programs.blueberry.enableFor.user.colin = false; # bluetooth manager: doesn't cross compile!
sane.programs.mercurial.enableFor.user.colin = false; # does not cross compile
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;
# enabled for easier debugging
sane.programs.eg25-control.enableFor.user.colin = true;
sane.programs.rtl8723cs-wowlan.enableFor.user.colin = true;
# sane.programs.ntfy-sh.config.autostart = true;
sane.programs.dino.config.autostart = true;
# sane.programs.calls.config.autostart = true;
sane.programs.mpv.config.vo = "wlshm"; #< see hosts/common/programs/mpv.nix for details
sane.programs.firefox.mime.priority = 300; # prefer other browsers when possible
# HACK/TODO: make `programs.P.env.VAR` behave according to `mime.priority`
sane.programs.firefox.env = lib.mkForce {};
sane.programs.epiphany.env.BROWSER = "epiphany";
sane.programs.firefox.enableFor.user.colin = false; # use epiphany instead
# note the .conf.d approach: using ~/.config/pipewire/pipewire.conf directly breaks all audio,
# presumably because that deletes the defaults entirely whereas the .conf.d approach selectively overrides defaults
sane.user.fs.".config/pipewire/pipewire.conf.d/10-fix-dino-mic-cutout.conf".symlink.text = ''
# config docs: <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire#properties>
# useful to run `pw-top` to see that these settings are actually having effect,
# and `pw-metadata` to see if any settings conflict (e.g. max-quantum < min-quantum)
#
# restart pipewire after editing these files:
# - `systemctl --user restart pipewire`
# - pipewire users will likely stop outputting audio until they are also restarted
#
# there's seemingly two buffers for the mic (see: <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained>)
# 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`
context.properties = {
default.clock.min-quantum = 2048
default.clock.max-quantum = 8192
}
'';
boot.loader.efi.canTouchEfiVariables = false;
# /boot space is at a premium. default was 20.
# even 10 can be too much
boot.loader.generic-extlinux-compatible.configurationLimit = 8;
# mobile.bootloader.enable = false;
# mobile.boot.stage-1.enable = false;
# boot.initrd.systemd.enable = false;
# boot.initrd.services.swraid.enable = false; # attempt to fix dm_mod stuff
# hardware.firmware makes the referenced files visible to the kernel, for whenever a driver explicitly asks for them.
# these files are visible from userspace by following `/sys/module/firmware_class/parameters/path`
#
# mobile-nixos' /lib/firmware includes:
# rtl_bt (bluetooth)
# anx7688-fw.bin (USB-C chip: power negotiation, HDMI/dock)
# ov5640_af.bin (camera module)
# hardware.firmware = [ config.mobile.device.firmware ];
# hardware.firmware = [ pkgs.rtl8723cs-firmware ];
hardware.firmware = [
(pkgs.linux-firmware-megous.override {
# rtl_bt = false probably means no bluetooth connectivity.
# N.B.: DON'T RE-ENABLE without first confirming that wake-on-lan works during suspend (rtcwake).
# it seems the rtl_bt stuff ("bluetooth coexist") might make wake-on-LAN radically more flaky.
rtl_bt = false;
})
];
system.stateVersion = "21.11";
# defined: https://www.freedesktop.org/software/systemd/man/machine-info.html
# XXX colin: not sure which, if any, software makes use of this
environment.etc."machine-info".text = ''
CHASSIS="handset"
'';
# enable rotation sensor
hardware.sensor.iio.enable = true;
# 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.override {
# internal speaker has a tendency to break :(
preferEarpiece = true;
})
];
systemd = let
ucm-env = config.environment.variables.ALSA_CONFIG_UCM2;
in {
# cribbed from <repo:nixos/mobile-nixos:modules/quirks/audio.nix>
# 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;
# pulseaudio
# user.services.pulseaudio.environment.ALSA_CONFIG_UCM2 = ucm-env;
# services.pulseaudio.environment.ALSA_CONFIG_UCM2 = ucm-env;
# TODO: move elsewhere...
services.ModemManager.serviceConfig = {
# N.B.: the extra "" in ExecStart serves to force upstream ExecStart to be ignored
ExecStart = [ "" "${pkgs.modemmanager}/bin/ModemManager --debug" ];
# --debug sets DEBUG level logging: so reset
ExecStartPost = [ "${pkgs.modemmanager}/bin/mmcli --set-logging=INFO" ];
};
};
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"
'';
}

View File

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

View File

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

View File

@@ -1,96 +0,0 @@
# this file configures preferences per program, without actually enabling any programs.
# the goal is to separate the place where we decide *what* to use (i.e. `sane.programs.firefox.enable = true` -- at the toplevel)
# from where we specific how that thing should behave *if* it's in use.
#
# NixOS backgrounds:
# - <https://github.com/NixOS/nixos-artwork>
# - <https://github.com/NixOS/nixos-artwork/issues/50> (colorful; unmerged)
# - <https://github.com/NixOS/nixos-artwork/pull/60/files> (desktop-oriented; clean; unmerged)
# - <https://itsfoss.com/content/images/2023/04/nixos-tutorials.png>
{ lib, pkgs, sane-lib, ... }:
{
sane.programs.firefox.config = {
# compromise impermanence for the sake of usability
persistCache = "private";
persistData = "private";
# i don't do crypto stuff on moby
addons.ether-metamask.enable = false;
# sidebery UX doesn't make sense on small screen
addons.sidebery.enable = false;
};
sane.programs.swaynotificationcenter.config = {
backlight = "backlight"; # /sys/class/backlight/*backlight*/brightness
};
sane.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
DEFAULT_COUNTRY = "US";
SXMO_AUTOROTATE = "1"; # enable auto-rotation at launch. has no meaning in stock/upstream sxmo-utils
# 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_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"
# sway/wayland scaling:
# - conflicting info out there on how scaling actually works
# at the least, for things where it matters (mpv), it seems like scale settings have 0 effect on perf
# ways to enforce scaling:
# - <https://wiki.archlinux.org/title/HiDPI>
# - `swaymsg -- output DSI-1 scale 2.0` (scales everything)
# - `dconf write /org/gnome/desktop/interface/text-scaling-factor 2.0` (scales ONLY TEXT)
# - `GDK_DPI_SCALE=2.0` (scales ONLY TEXT)
#
# application notes:
# - cozy: in landscape, playback position is not visible unless scale <= 1.7
# - if in a tab, then scale 1.6 is the max
# SXMO_SWAY_SCALE = "1.5"; # hard to press gPodder icons
SXMO_SWAY_SCALE = "1.6";
# 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";
};
};
}

View File

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

View File

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

View File

@@ -1,116 +0,0 @@
{ ... }:
{
sane.persist.root-on-tmpfs = true;
# increase /tmp space (defaults to 50% of RAM) for building large nix things.
# even the stock `nixpkgs.linux` consumes > 16 GB of tmp
fileSystems."/tmp".options = [ "size=32G" ];
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/cc81cca0-3cc7-4d82-a00c-6243af3e7776";
fsType = "btrfs";
options = [
"compress=zstd"
"defaults"
];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/6EE3-4171";
fsType = "vfat";
};
# slow, external storage (for archiving, etc)
fileSystems."/mnt/persist/ext" = {
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
fsType = "btrfs";
options = [
"compress=zstd"
"defaults"
];
};
sane.persist.stores."ext" = {
origin = "/mnt/persist/ext/persist";
storeDescription = "external HDD storage";
};
sane.fs."/mnt/persist/ext".mount = {};
sane.persist.sys.byStore.plaintext = [
# TODO: this is overly broad; only need media and share directories to be persisted
{ user = "colin"; group = "users"; path = "/var/lib/uninsane"; }
];
# force some problematic directories to always get correct permissions:
sane.fs."/var/lib/uninsane/media".dir.acl = {
user = "colin"; group = "media"; mode = "0775";
};
sane.fs."/var/lib/uninsane/media/archive".dir = {};
sane.fs."/var/lib/uninsane/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/lib/uninsane/media/Books".dir = {};
sane.fs."/var/lib/uninsane/media/Books/Audiobooks".dir = {};
sane.fs."/var/lib/uninsane/media/Books/Books".dir = {};
sane.fs."/var/lib/uninsane/media/Books/Visual".dir = {};
sane.fs."/var/lib/uninsane/media/collections".dir = {};
sane.fs."/var/lib/uninsane/media/datasets".dir = {};
sane.fs."/var/lib/uninsane/media/freeleech".dir = {};
sane.fs."/var/lib/uninsane/media/Music".dir = {};
sane.fs."/var/lib/uninsane/media/Pictures".dir = {};
sane.fs."/var/lib/uninsane/media/Videos".dir = {};
sane.fs."/var/lib/uninsane/media/Videos/Film".dir = {};
sane.fs."/var/lib/uninsane/media/Videos/Shows".dir = {};
sane.fs."/var/lib/uninsane/media/Videos/Talks".dir = {};
sane.fs."/var/lib/uninsane/datasets/README.md".file.text = ''
this directory may seem redundant with ../media/datasets. it isn't.
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
# swapDevices = [
# { device = "/nix/persist/swapfile"; size = 4096; }
# ];
# this can be a partition. create with:
# fdisk <dev>
# n
# <default partno>
# <start>
# <end>
# t
# <partno>
# 19 # set part type to Linux swap
# w # write changes
# mkswap -L swap <part>
# swapDevices = [
# {
# label = "swap";
# # TODO: randomEncryption.enable = true;
# }
# ];
}

View File

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

View File

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

View File

@@ -1,124 +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.
{ lib, ... }:
let
# TODO: this range could be larger, but right now that's costly because each element is its own UPnP forward
# TURN port range (inclusive)
turnPortLow = 49152;
turnPortHigh = 49167;
turnPortRange = lib.range turnPortLow turnPortHigh;
in
{
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.ovpn = true;
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.ovpn = 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.ovpn = 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";
services.coturn.use-auth-secret = true;
services.coturn.static-auth-secret-file = "/var/lib/coturn/shared_secret.bin";
services.coturn.lt-cred-mech = true;
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"
# "no-multicast-peers" # disables sending to IPv4 broadcast addresses (e.g. 224.0.0.0/3)
"listening-ip=10.0.1.5"
# "external-ip=185.157.162.178/10.0.1.5"
"external-ip=185.157.162.178"
# "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" (?)
];
}

View File

@@ -1,27 +0,0 @@
{ 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";
};
};
}

View File

@@ -1,33 +0,0 @@
{ ... }:
{
imports = [
./calibre.nix
./coturn.nix
./ddns-afraid.nix
./ddns-he.nix
./email
./ejabberd.nix
./freshrss.nix
./export
./gitea.nix
./goaccess.nix
./ipfs.nix
./jackett.nix
./jellyfin.nix
./kiwix-serve.nix
./komga.nix
./lemmy.nix
./matrix
./navidrome.nix
./nginx.nix
./nixserve.nix
./ntfy
./pict-rs.nix
./pleroma.nix
./postgres.nix
./prosody
./transmission.nix
./trust-dns.nix
./wikipedia.nix
];
}

View File

@@ -1,471 +0,0 @@
# docs:
# - <https://docs.ejabberd.im/admin/configuration/basic>
# example configs:
# - <https://github.com/vkleen/machines/blob/138a2586ce185d7cf201d4e1fe898c83c4af52eb/hosts/europium/ejabberd.nix>
# - <https://github.com/Mic92/stockholm/blob/675ef0088624c9de1cb531f318446316884a9d3d/tv/3modules/ejabberd/default.nix>
# - <https://github.com/buffet/tararice/blob/master/programs/ejabberd.nix>
# - enables STUN and TURN
# - only over UDP 3478, not firewall-forwarding any TURN port range
# - uses stun_disco module (but with no options)
# - <https://github.com/leo60228/dotfiles/blob/39b3abba3009bdc31413d4757ca2f882a33eec8b/files/ejabberd.yml>
# - <https://github.com/Mic92/dotfiles/blob/ddf0f4821f554f7667fc803344657367c55fb9e6/nixos/eve/modules/ejabberd.nix>
# - <nixpkgs:nixos/tests/xmpp/ejabberd.nix>
# - 2013: <https://github.com/processone/ejabberd/blob/master/ejabberd.yml.example>
#
# compliance tests:
# - <https://compliance.conversations.im/server/uninsane.org/#xep0352>
#
# administration:
# - `sudo -u ejabberd ejabberdctl help`
#
# federation/support matrix:
# - avatars
# - nixnet.services + dino: works in MUCs but not DMs (as of 2023 H1)
# - movim.eu + dino: works in DMs, MUCs untested (as of 2023/08/29)
# - calls
# - local + dino: audio, video, works in DMs (as of 2023/08/29)
# - movim.eu + dino: audio, video, works in DMs, no matter which side initiates (as of 2023/08/30)
# - +native-cell-number@cheogram.com + dino: audio works in DMs, no matter which side initiates (as of 2023/09/01)
# - can receive calls even if sender isn't in my roster
# - this is presumably using JMP.chat's SIP servers, which then convert it to XMPP call
#
# bugs:
# - 2023/09/01: will randomly stop federating. `systemctl restart ejabberd` fixes, but takes 10 minutes.
{ config, lib, pkgs, ... }:
let
# TODO: this range could be larger, but right now that's costly because each element is its own UPnP forward
# TURN port range (inclusive)
turnPortLow = 49152;
turnPortHigh = 49167;
turnPortRange = lib.range turnPortLow turnPortHigh;
in
# XXX(2023/10/15): disabled in favor of Prosody.
# everything configured below was fine: used ejabberd for several months.
lib.mkIf false
{
sane.persist.sys.byStore.plaintext = [
{ user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; }
];
sane.ports.ports = lib.mkMerge ([
{
"3478" = {
protocol = [ "tcp" "udp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-stun-turn";
};
"5222" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-client-to-server";
};
"5223" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpps-client-to-server"; # XMPP over TLS
};
"5269" = {
protocol = [ "tcp" ];
visibleTo.wan = true;
description = "colin-xmpp-server-to-server";
};
"5270" = {
protocol = [ "tcp" ];
visibleTo.wan = true;
description = "colin-xmpps-server-to-server"; # XMPP over TLS
};
"5280" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-bosh";
};
"5281" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-bosh-https";
};
"5349" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-stun-turn-over-tls";
};
"5443" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = 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.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-turn-${builtins.toString count}-of-${builtins.toString numPorts}";
};
})
turnPortRange
));
# this ejabberd config uses builtin STUN/TURN server, so hack to ensure no other implementation fights for ports
services.coturn.enable = false;
# provide access to certs
# TODO: this should just be `acme`. then we also add nginx to the `acme` group.
# why is /var/lib/acme/* owned by `nginx` group??
users.users.ejabberd.extraGroups = [ "nginx" ];
security.acme.certs."uninsane.org".extraDomainNames = [
"xmpp.uninsane.org"
"muc.xmpp.uninsane.org"
"pubsub.xmpp.uninsane.org"
"upload.xmpp.uninsane.org"
"vjid.xmpp.uninsane.org"
];
# exists so the XMPP server's cert can obtain altNames for all its resources
services.nginx.virtualHosts."xmpp.uninsane.org" = {
useACMEHost = "uninsane.org";
};
services.nginx.virtualHosts."muc.xmpp.uninsane.org" = {
useACMEHost = "uninsane.org";
};
services.nginx.virtualHosts."pubsub.xmpp.uninsane.org" = {
useACMEHost = "uninsane.org";
};
services.nginx.virtualHosts."upload.xmpp.uninsane.org" = {
useACMEHost = "uninsane.org";
};
services.nginx.virtualHosts."vjid.xmpp.uninsane.org" = {
useACMEHost = "uninsane.org";
};
sane.dns.zones."uninsane.org".inet = {
# XXX: SRV records have to point to something with a A/AAAA record; no CNAMEs
A."xmpp" = "%ANATIVE%";
CNAME."muc.xmpp" = "xmpp";
CNAME."pubsub.xmpp" = "xmpp";
CNAME."upload.xmpp" = "xmpp";
CNAME."vjid.xmpp" = "xmpp";
# _Service._Proto.Name TTL Class SRV Priority Weight Port Target
# - <https://xmpp.org/extensions/xep-0368.html>
# something's requesting the SRV records for muc.xmpp, so let's include it
# nothing seems to request XMPP SRVs for the other records (except @)
# lower numerical priority field tells clients to prefer this method
SRV."_xmpps-client._tcp.muc.xmpp" = "3 50 5223 xmpp";
SRV."_xmpps-server._tcp.muc.xmpp" = "3 50 5270 xmpp";
SRV."_xmpp-client._tcp.muc.xmpp" = "5 50 5222 xmpp";
SRV."_xmpp-server._tcp.muc.xmpp" = "5 50 5269 xmpp";
SRV."_xmpps-client._tcp" = "3 50 5223 xmpp";
SRV."_xmpps-server._tcp" = "3 50 5270 xmpp";
SRV."_xmpp-client._tcp" = "5 50 5222 xmpp";
SRV."_xmpp-server._tcp" = "5 50 5269 xmpp";
SRV."_stun._udp" = "5 50 3478 xmpp";
SRV."_stun._tcp" = "5 50 3478 xmpp";
SRV."_stuns._tcp" = "5 50 5349 xmpp";
SRV."_turn._udp" = "5 50 3478 xmpp";
SRV."_turn._tcp" = "5 50 3478 xmpp";
SRV."_turns._tcp" = "5 50 5349 xmpp";
};
# TODO: allocate UIDs/GIDs ?
services.ejabberd.enable = true;
services.ejabberd.configFile = "/var/lib/ejabberd/ejabberd.yaml";
systemd.services.ejabberd.preStart = let
config-in = pkgs.writeText "ejabberd.yaml.in" (lib.generators.toYAML {} {
hosts = [ "uninsane.org" ];
# none | emergency | alert | critical | error | warning | notice | info | debug
loglevel = "debug";
acme.auto = false;
certfiles = [ "/var/lib/acme/uninsane.org/full.pem" ];
# ca_file = "${pkgs.cacert.unbundled}/etc/ssl/certs/";
# ca_file = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
pam_userinfotype = "jid";
acl = {
admin.user = [ "colin@uninsane.org" ];
local.user_regexp = "";
loopback.ip = [ "127.0.0.0/8" "::1/128" ];
};
access_rules = {
local.allow = "local";
c2s_access.allow = "all";
announce.allow = "admin";
configure.allow = "admin";
muc_create.allow = "local";
pubsub_createnode_access.allow = "all";
trusted_network.allow = "loopback";
};
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shaper-rules>
shaper_rules = {
# setting this to above 1 may break outgoing messages
# - maybe some servers rate limit? or just don't understand simultaneous connections?
max_s2s_connections = 1;
max_user_sessions = 10;
max_user_offline_messages = 5000;
c2s_shaper.fast = "all";
s2s_shaper.med = "all";
};
# docs: <https://docs.ejabberd.im/admin/configuration/basic/#shapers>
# this limits the bytes/sec.
# for example, burst: 3_000_000 and rate: 100_000 means:
# - each client has a BW budget that accumulates 100kB/sec and is capped at 3 MB
shaper.fast = 1000000;
shaper.med = 500000;
# shaper.fast.rate = 1000000;
# shaper.fast.burst_size = 10000000;
# shaper.med.rate = 500000;
# shaper.med.burst_size = 5000000;
# see: <https://docs.ejabberd.im/admin/configuration/listen/>
# s2s_use_starttls = true;
s2s_use_starttls = "optional";
# lessens 504: remote-server-timeout errors
# see: <https://github.com/processone/ejabberd/issues/3105#issuecomment-562182967>
negotiation_timeout = 60;
listen = [
{
port = 5222;
module = "ejabberd_c2s";
shaper = "c2s_shaper";
starttls = true;
access = "c2s_access";
}
{
port = 5223;
module = "ejabberd_c2s";
shaper = "c2s_shaper";
tls = true;
access = "c2s_access";
}
{
port = 5269;
module = "ejabberd_s2s_in";
shaper = "s2s_shaper";
}
{
port = 5270;
module = "ejabberd_s2s_in";
shaper = "s2s_shaper";
tls = true;
}
{
port = 5443;
module = "ejabberd_http";
tls = true;
request_handlers = {
"/admin" = "ejabberd_web_admin"; # TODO: ensure this actually works
"/api" = "mod_http_api"; # ejabberd API endpoint (to control server)
"/bosh" = "mod_bosh";
"/upload" = "mod_http_upload";
"/ws" = "ejabberd_http_ws";
# "/.well-known/host-meta" = "mod_host_meta";
# "/.well-known/host-meta.json" = "mod_host_meta";
};
}
{
# STUN+TURN TCP
# note that the full port range should be forwarded ("not NAT'd")
# `use_turn=true` enables both TURN *and* STUN
port = 3478;
module = "ejabberd_stun";
transport = "tcp";
use_turn = true;
turn_min_port = turnPortLow;
turn_max_port = turnPortHigh;
turn_ipv4_address = "%ANATIVE%";
}
{
# STUN+TURN UDP
port = 3478;
module = "ejabberd_stun";
transport = "udp";
use_turn = true;
turn_min_port = turnPortLow;
turn_max_port = turnPortHigh;
turn_ipv4_address = "%ANATIVE%";
}
{
# STUN+TURN TLS over TCP
port = 5349;
module = "ejabberd_stun";
transport = "tcp";
tls = true;
certfile = "/var/lib/acme/uninsane.org/full.pem";
use_turn = true;
turn_min_port = turnPortLow;
turn_max_port = turnPortHigh;
turn_ipv4_address = "%ANATIVE%";
}
];
# TODO: enable mod_fail2ban
# TODO(low): look into mod_http_fileserver for serving macros?
modules = {
# mod_adhoc = {};
# mod_announce = {
# access = "admin";
# };
# allows users to set avatars in vCard
# - <https://docs.ejabberd.im/admin/configuration/modules/#mod-avatar>
mod_avatar = {};
mod_caps = {}; # for mod_pubsub
mod_carboncopy = {}; # allows multiple clients to receive a user's message
# queues messages when recipient is offline, including PEP and presence messages.
# compliance test suggests this be enabled
mod_client_state = {};
# mod_conversejs: TODO: enable once on 21.12
# allows clients like Dino to discover where to upload files
mod_disco.server_info = [
{
modules = "all";
name = "abuse-addresses";
urls = [
"mailto:admin.xmpp@uninsane.org"
"xmpp:colin@uninsane.org"
];
}
{
modules = "all";
name = "admin-addresses";
urls = [
"mailto:admin.xmpp@uninsane.org"
"xmpp:colin@uninsane.org"
];
}
];
mod_http_upload = {
host = "upload.xmpp.uninsane.org";
hosts = [ "upload.xmpp.uninsane.org" ];
put_url = "https://@HOST@:5443/upload";
dir_mode = "0750";
file_mode = "0750";
rm_on_unregister = false;
};
# allow discoverability of BOSH and websocket endpoints
# TODO: enable once on ejabberd 22.05 (presently 21.04)
# mod_host_meta = {};
mod_jidprep = {}; # probably not needed: lets clients normalize jids
mod_last = {}; # allow other users to know when i was last online
mod_mam = {
# Mnesia is limited to 2GB, better to use an SQL backend
# For small servers SQLite is a good fit and is very easy
# to configure. Uncomment this when you have SQL configured:
# db_type: sql
assume_mam_usage = true;
default = "always";
};
mod_muc = {
access = [ "allow" ];
access_admin = { allow = "admin"; };
access_create = "muc_create";
access_persistent = "muc_create";
access_mam = [ "allow" ];
history_size = 100; # messages to show new participants
host = "muc.xmpp.uninsane.org";
hosts = [ "muc.xmpp.uninsane.org" ];
default_room_options = {
anonymous = false;
lang = "en";
persistent = true;
mam = true;
};
};
mod_muc_admin = {};
mod_offline = {
# store messages for a user when they're offline (TODO: understand multi-client workflow?)
access_max_user_messages = "max_user_offline_messages";
store_groupchat = true;
};
mod_ping = {};
mod_privacy = {}; # deprecated, but required for `ejabberctl export_piefxis`
mod_private = {}; # allow local clients to persist arbitrary data on my server
# push notifications to services integrated with e.g. Apple/Android.
# default is for a maximum amount of PII to be withheld, since these push notifs
# generally traverse 3rd party services. can opt to include message body, etc, though.
mod_push = {};
# i don't fully understand what this does, but it seems aimed at making push notifs more reliable.
mod_push_keepalive = {};
mod_roster = {
versioning = true;
};
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-s2s-dialback>
# s2s dialback to verify inbound messages
# unclear to what degree the XMPP network requires this
mod_s2s_dialback = {};
mod_shared_roster = {}; # creates groups for @all, @online, and anything manually administered?
mod_stream_mgmt = {
# resend undelivered messages if the origin client is offline
resend_on_timeout = "if_offline";
};
# fallback for when DNS-based STUN discovery is unsupported.
# - see: <https://xmpp.org/extensions/xep-0215.html>
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-stun-disco>
# people say to just keep this defaulted (i guess ejabberd knows to return its `host` option of uninsane.org?)
mod_stun_disco = {};
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-vcard>
mod_vcard = {
allow_return_all = true; # all users are discoverable (?)
host = "vjid.xmpp.uninsane.org";
hosts = [ "vjid.xmpp.uninsane.org" ];
search = true;
};
mod_vcard_xupdate = {}; # needed for avatars
# docs: <https://docs.ejabberd.im/admin/configuration/modules/#mod-pubsub>
mod_pubsub = {
#^ needed for avatars
access_createnode = "pubsub_createnode_access";
host = "pubsub.xmpp.uninsane.org";
hosts = [ "pubsub.xmpp.uninsane.org" ];
ignore_pep_from_offline = false;
last_item_cache = true;
plugins = [
"pep"
"flat"
];
force_node_config = {
# ensure client bookmarks are private
"storage:bookmarks:" = {
"access_model" = "whitelist";
};
"urn:xmpp:avatar:data" = {
"access_model" = "open";
};
"urn:xmpp:avatar:metadata" = {
"access_model" = "open";
};
};
};
mod_version = {};
};
});
sed = "${pkgs.gnused}/bin/sed";
in ''
ip=$(cat '${config.sane.services.dyn-dns.ipPath}')
# config is 444 (not 644), so we want to write out-of-place and then atomically move
# TODO: factor this out into `sane-woop` helper?
rm -f /var/lib/ejabberd/ejabberd.yaml.new
${sed} "s/%ANATIVE%/$ip/g" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new
mv /var/lib/ejabberd/ejabberd.yaml{.new,}
'';
sane.services.dyn-dns.restartOnChange = [ "ejabberd.service" ];
}

View File

@@ -1,44 +0,0 @@
# nix configs to reference:
# - <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver>
# - <https://github.com/nix-community/nur-combined/-/tree/master/repos/eh5/machines/srv-m/mail-rspamd.nix>
# - postfix / dovecot / rspamd / stalwart-jmap / sogo
#
# rspamd:
# - nixos: <https://nixos.wiki/wiki/Rspamd>
# - guide: <https://rspamd.com/doc/quickstart.html>
# - non-nixos example: <https://dataswamp.org/~solene/2021-07-13-smtpd-rspamd.html>
#
#
# my rough understanding of the pieces:
# - postfix handles SMTP protocol with the rest of the world.
# - dovecot implements IMAP protocol.
# - client auth (i.e. validate that user@uninsane.org is who they claim)
# - "folders" (INBOX, JUNK) are internal to dovecot?
# or where do folders live, on-disk?
#
# - non-local clients (i.e. me) interact with BOTH postfix and dovecot, but primarily dovecot:
# - mail reading is done via IMAP (so, dovecot)
# - mail sending is done via SMTP/submission port (so, postfix)
# - but postfix delegates authorization of that outgoing mail to dovecot, on the server side
#
# - local clients (i.e. sendmail) interact only with postfix
#
# debugging: general connectivity issues
# - test that inbound port 25 is unblocked:
# - `curl https://canyouseeme.org/ --data 'port=25&IP=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
{ ... }:
{
imports = [
./dovecot.nix
./postfix.nix
];
#### SPAM FILTERING
# services.rspamd.enable = true;
# services.rspamd.postfix.enable = true;
}

View File

@@ -1,142 +0,0 @@
# dovecot config options: <https://doc.dovecot.org/configuration_manual/>
#
# sieve docs:
# - sieve language examples: <https://doc.dovecot.org/configuration_manual/sieve/examples/>
# - sieve protocol/language: <https://proton.me/support/sieve-advanced-custom-filters>
{ config, lib, pkgs, ... }:
{
sane.ports.ports."143" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-imap-imap.uninsane.org";
};
sane.ports.ports."993" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-imaps-imap.uninsane.org";
};
# exists only to manage certs for dovecot
services.nginx.virtualHosts."imap.uninsane.org" = {
enableACME = true;
};
sane.dns.zones."uninsane.org".inet = {
CNAME."imap" = "native";
};
sops.secrets."dovecot_passwd" = {
owner = config.users.users.dovecot2.name;
# TODO: debug why mail can't be sent without this being world-readable
mode = "0444";
};
# inspired by https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/
services.dovecot2.enable = true;
# services.dovecot2.enableLmtp = true;
services.dovecot2.sslServerCert = "/var/lib/acme/imap.uninsane.org/fullchain.pem";
services.dovecot2.sslServerKey = "/var/lib/acme/imap.uninsane.org/key.pem";
services.dovecot2.enablePAM = false;
# sieve scripts require me to set a user for... idk why?
services.dovecot2.mailUser = "colin";
services.dovecot2.mailGroup = "users";
users.users.colin.isSystemUser = lib.mkForce false;
services.dovecot2.extraConfig =
let
passwdFile = config.sops.secrets.dovecot_passwd.path;
in
''
passdb {
driver = passwd-file
args = ${passwdFile}
}
userdb {
driver = passwd-file
args = ${passwdFile}
}
# allow postfix to query our auth db
service auth {
unix_listener auth {
mode = 0660
user = postfix
group = postfix
}
}
auth_mechanisms = plain login
# accept incoming messaging from postfix
# service lmtp {
# unix_listener dovecot-lmtp {
# mode = 0600
# user = postfix
# group = postfix
# }
# }
# plugin {
# sieve_plugins = sieve_imapsieve
# }
mail_debug = yes
auth_debug = yes
# verbose_ssl = yes
'';
services.dovecot2.mailboxes = {
# special-purpose mailboxes: "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash"
# RFC6154 describes these special mailboxes: https://www.ietf.org/rfc/rfc6154.html
# how these boxes are treated is 100% up to the client and server to decide.
# client behavior:
# iOS
# - Drafts: ?
# - Sent: works
# - Trash: works
# - Junk: works ("mark" -> "move to Junk")
# aerc
# - Drafts: works
# - Sent: works
# - Trash: no; deleted messages are actually deleted
# use `:move trash` instead
# - Junk: ?
# Sent mailbox: all sent messages are copied to it. unclear if this happens server-side or client-side.
Drafts = { specialUse = "Drafts"; auto = "create"; };
Sent = { specialUse = "Sent"; auto = "create"; };
Trash = { specialUse = "Trash"; auto = "create"; };
Junk = { specialUse = "Junk"; auto = "create"; };
};
services.dovecot2.mailPlugins = {
perProtocol = {
# imap.enable = [
# "imap_sieve"
# ];
lda.enable = [
"sieve"
];
# lmtp.enable = [
# "sieve"
# ];
};
};
services.dovecot2.modules = [
pkgs.dovecot_pigeonhole # enables sieve execution (?)
];
services.dovecot2.sieveScripts = {
# 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
after = builtins.toFile "ensuredkim.sieve" ''
require "fileinto";
if not header :contains "Authentication-Results" "dkim=pass" {
fileinto "Junk";
stop;
}
'';
};
}

View File

@@ -1,198 +0,0 @@
# postfix config options: <https://www.postfix.org/postconf.5.html>
{ lib, pkgs, ... }:
let
submissionOptions = {
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "uninsane.org";
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
# reuse the virtual map so that sender mapping matches recipient mapping
smtpd_sender_login_maps = "hash:/var/lib/postfix/conf/virtual";
smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,permit_sasl_authenticated,reject";
};
in
{
sane.persist.sys.byStore.plaintext = [
# TODO: mode? could be more granular
{ user = "opendkim"; group = "opendkim"; path = "/var/lib/opendkim"; }
{ user = "root"; group = "root"; path = "/var/lib/postfix"; }
{ user = "root"; group = "root"; path = "/var/spool/mail"; }
# *probably* don't need these dirs:
# "/var/lib/dhparams" # https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/dhparams.nix
# "/var/lib/dovecot"
];
# XXX(2023/10/20): opening these ports in the firewall has the OPPOSITE effect as intended.
# these ports are only routable so long as they AREN'T opened.
# probably some cursed interaction with network namespaces introduced after 2023/10/10.
# sane.ports.ports."25" = {
# protocol = [ "tcp" ];
# # XXX visibleTo.lan effectively means "open firewall, but don't configure any NAT/forwarding"
# visibleTo.lan = true;
# description = "colin-smtp-mx.uninsane.org";
# };
# sane.ports.ports."465" = {
# protocol = [ "tcp" ];
# visibleTo.lan = true;
# description = "colin-smtps-mx.uninsane.org";
# };
# sane.ports.ports."587" = {
# protocol = [ "tcp" ];
# visibleTo.lan = true;
# description = "colin-smtps-submission-mx.uninsane.org";
# };
# exists only to manage certs for Postfix
services.nginx.virtualHosts."mx.uninsane.org" = {
enableACME = true;
};
sane.dns.zones."uninsane.org".inet = {
MX."@" = "10 mx.uninsane.org.";
# XXX: RFC's specify that the MX record CANNOT BE A CNAME
A."mx" = "185.157.162.178";
# Sender Policy Framework:
# +mx => mail passes if it originated from the MX
# +a => mail passes if it originated from the A address of this domain
# +ip4:.. => mail passes if it originated from this IP
# -all => mail fails if none of these conditions were met
TXT."@" = "v=spf1 a mx -all";
# DKIM public key:
TXT."mx._domainkey" =
"v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkSyMufc2KrRx3j17e/LyB+3eYSBRuEFT8PUka8EDX04QzCwDPdkwgnj3GNDvnB5Ktb05Cf2SJ/S1OLqNsINxJRWtkVfZd/C339KNh9wrukMKRKNELL9HLUw0bczOI4gKKFqyrRE9qm+4csCMAR79Te9FCjGV/jVnrkLdPT0GtFwIDAQAB"
;
# DMARC fields <https://datatracker.ietf.org/doc/html/rfc7489>:
# p=none|quarantine|reject: what to do with failures
# sp = p but for subdomains
# rua = where to send aggregrate reports
# ruf = where to send individual failure reports
# fo=0|1|d|s controls WHEN to send failure reports
# (1=on bad alignment; d=on DKIM failure; s=on SPF failure);
# Additionally:
# adkim=r|s (is DKIM relaxed [default] or strict)
# aspf=r|s (is SPF relaxed [default] or strict)
# pct = sampling ratio for punishing failures (default 100 for 100%)
# rf = report format
# ri = report interval
TXT."_dmarc" =
"v=DMARC1;p=quarantine;sp=reject;rua=mailto:admin+mail@uninsane.org;ruf=mailto:admin+mail@uninsane.org;fo=1:d:s"
;
};
services.postfix.enable = true;
services.postfix.hostname = "mx.uninsane.org";
services.postfix.origin = "uninsane.org";
services.postfix.destination = [ "localhost" "uninsane.org" ];
services.postfix.sslCert = "/var/lib/acme/mx.uninsane.org/fullchain.pem";
services.postfix.sslKey = "/var/lib/acme/mx.uninsane.org/key.pem";
services.postfix.virtual = ''
notify.matrix@uninsane.org matrix-synapse
@uninsane.org colin
'';
services.postfix.config = {
# smtpd_milters = local:/run/opendkim/opendkim.sock
# milter docs: http://www.postfix.org/MILTER_README.html
# mail filters for receiving email and from authorized SMTP clients (i.e. via submission)
# smtpd_milters = inet:185.157.162.190:8891
# opendkim.sock will add a Authentication-Results header, with `dkim=pass|fail|...` value to received messages
smtpd_milters = "unix:/run/opendkim/opendkim.sock";
# mail filters for sendmail
non_smtpd_milters = "$smtpd_milters";
# what to do when a milter exits unexpectedly:
milter_default_action = "accept";
inet_protocols = "ipv4";
smtp_tls_security_level = "may";
# hand received mail over to dovecot so that it can run sieves & such
mailbox_command = ''${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT"'';
# hand received mail over to dovecot
# virtual_alias_maps = [
# "hash:/etc/postfix/virtual"
# ];
# mydestination = "";
# virtual_mailbox_domains = [ "localhost" "uninsane.org" ];
# # virtual_mailbox_maps = "hash:/etc/postfix/virtual";
# virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# anti-spam options: <https://www.postfix.org/SMTPD_ACCESS_README.html>
# reject_unknown_sender_domain: causes postfix to `dig <sender> MX` and make sure that exists.
# but may cause problems receiving mail from google & others who load-balance?
# - <https://unix.stackexchange.com/questions/592131/how-to-reject-email-from-unknown-domains-with-postfix-on-centos>
# smtpd_sender_restrictions = reject_unknown_sender_domain
};
services.postfix.enableSubmission = true;
services.postfix.submissionOptions = submissionOptions;
services.postfix.enableSubmissions = true;
services.postfix.submissionsOptions = submissionOptions;
systemd.services.postfix.after = [ "wireguard-wg-ovpns.service" ];
systemd.services.postfix.partOf = [ "wireguard-wg-ovpns.service" ];
systemd.services.postfix.serviceConfig = {
# run this behind the OVPN static VPN
NetworkNamespacePath = "/run/netns/ovpns";
};
#### OPENDKIM
services.opendkim.enable = true;
# services.opendkim.domains = "csl:uninsane.org";
services.opendkim.domains = "uninsane.org";
# we use a custom (inet) socket, because the default perms
# of the unix socket don't allow postfix to connect.
# this sits on the machine-local 10.0.1 interface because it's the closest
# thing to a loopback interface shared by postfix and opendkim netns.
# services.opendkim.socket = "inet:8891@185.157.162.190";
# services.opendkim.socket = "local:/run/opendkim.sock";
# selectors can be used to disambiguate sender machines.
# keeping this the same as the hostname seems simplest
services.opendkim.selector = "mx";
systemd.services.opendkim.after = [ "wireguard-wg-ovpns.service" ];
systemd.services.opendkim.partOf = [ "wireguard-wg-ovpns.service" ];
systemd.services.opendkim.serviceConfig = {
# run this behind the OVPN static VPN
NetworkNamespacePath = "/run/netns/ovpns";
# /run/opendkim/opendkim.sock needs to be rw by postfix
UMask = lib.mkForce "0011";
};
#### OUTGOING MESSAGE REWRITING:
services.postfix.enableHeaderChecks = true;
services.postfix.headerChecks = [
# intercept gitea registration confirmations and manually screen them
{
# headerChecks are somehow ignorant of alias rules: have to redirect to a real user
action = "REDIRECT colin@uninsane.org";
pattern = "/^Subject: Please activate your account/";
}
# intercept Matrix registration confirmations
{
action = "REDIRECT colin@uninsane.org";
pattern = "/^Subject:.*Validate your email/";
}
# XXX postfix only supports performing ONE action per header.
# {
# action = "REPLACE Subject: git application: Please activate your account";
# pattern = "/^Subject:.*activate your account/";
# }
];
}

View File

@@ -1,53 +0,0 @@
{ config, ... }:
{
imports = [
./nfs.nix
./sftpgo.nix
];
users.groups.export = {};
fileSystems."/var/export/media" = {
# everything in here could be considered publicly readable (based on the viewer's legal jurisdiction)
device = "/var/lib/uninsane/media";
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.ext = [
{ user = "root"; group = "export"; mode = "0775"; path = "/var/export/playground"; }
];
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
'';
};
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
'';
};
}

View File

@@ -1,110 +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, ... }:
{
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
# - 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 = "/var/export";
baseOpts = [ "crossmnt" "fsid=root" ];
extraLanOpts = [ "ro" ];
extraVpnOpts = [ "rw" "no_root_squash" ];
})
(fmtExport {
export = "/var/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;
};
}

View File

@@ -1,184 +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.
#
# TODO: change umask so sftpgo-created files default to 644.
# - it does indeed appear that the 600 is not something sftpgo is explicitly doing.
{ config, lib, pkgs, sane-lib, ... }:
let
# 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.
authResponseSuccess = {
status = 1;
username = "anonymous";
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 = {
"/" = [ "list" "download" ];
"/playground" = [
# read-only:
"list"
"download"
# write:
"upload"
"overwrite"
"delete"
"rename"
"create_dirs"
"create_symlinks"
# intentionally omitted:
# "chmod"
# "chown"
# "chtimes"
];
};
upload_bandwidth = 0;
download_bandwidth = 0;
filters = {
allowed_ip = [];
denied_ip = [];
};
public_keys = [];
# other fields:
# ? groups
# ? virtual_folders
};
authResponseFail = {
username = "";
};
authSuccessJson = pkgs.writeText "sftp-auth-success.json" (builtins.toJSON authResponseSuccess);
authFailJson = pkgs.writeText "sftp-auth-fail.json" (builtins.toJSON authResponseFail);
unwrappedAuthProgram = pkgs.static-nix-shell.mkBash {
pname = "sftpgo_external_auth_hook";
src = ./.;
pkgs = [ "coreutils" ];
};
authProgram = pkgs.writeShellScript "sftpgo-auth-hook" ''
${unwrappedAuthProgram}/bin/sftpgo_external_auth_hook ${authFailJson} ${authSuccessJson}
'';
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;
group = "export";
settings = {
ftpd = {
bindings = [
{
# binding this means any wireguard client can connect
address = "10.0.10.5";
port = 21;
debug = true;
}
{
# binding this means any LAN client can connect
address = "10.78.79.51";
port = 21;
debug = true;
}
];
# active mode is susceptible to "bounce attacks", without much benefit over passive mode
disable_active_mode = true;
hash_support = true;
passive_port_range = {
start = 50000;
end = 50100;
};
banner = ''
Welcome, friends, to Colin's read-only FTP server! Also available via NFS on the same host.
Username: "anonymous"
Password: "anonymous"
CONFIGURE YOUR CLIENT FOR "PASSIVE" mode, e.g. `ftp --passive uninsane.org`
Please let me know if anything's broken or not as it should be. Otherwise, browse and DL freely :)
'';
};
data_provider = {
driver = "memory";
external_auth_hook = "${authProgram}";
# 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" ];
systemd.services.sftpgo.serviceConfig = {
ReadOnlyPaths = [ "/var/export" ];
ReadWritePaths = [ "/var/export/playground" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
Restart = "always";
RestartSec = "20s";
};
}

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p coreutils
# 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
#
#
# call with <script_name> /path/to/fail/response.json /path/to/success/response.json
if [ "$SFTPGO_AUTHD_USERNAME" = "anonymous" ]; then
cat "$2"
else
cat "$1"
fi

View File

@@ -1,63 +0,0 @@
# import feeds with e.g.
# ```console
# $ nix build '.#nixpkgs.freshrss'
# $ sudo -u freshrss -g freshrss FRESHRSS_DATA_PATH=/var/lib/freshrss ./result/cli/import-for-user.php --user admin --filename /home/colin/.config/newsflashFeeds.opml
# ```
#
# export feeds with
# ```console
# $ sudo -u freshrss -g freshrss FRESHRSS_DATA_PATH=/var/lib/freshrss ./result/cli/export-opml-for-user.php --user admin
# ```
{ config, lib, pkgs, sane-lib, ... }:
{
sops.secrets."freshrss_passwd" = {
owner = config.users.users.freshrss.name;
mode = "0400";
};
sane.persist.sys.byStore.plaintext = [
{ user = "freshrss"; group = "freshrss"; path = "/var/lib/freshrss"; }
];
services.freshrss.enable = true;
services.freshrss.baseUrl = "https://rss.uninsane.org";
services.freshrss.virtualHost = "rss.uninsane.org";
services.freshrss.passwordFile = config.sops.secrets.freshrss_passwd.path;
systemd.services.freshrss-import-feeds =
let
feeds = sane-lib.feeds;
fresh = config.systemd.services.freshrss-config;
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
opml = pkgs.writeText "sane-freshrss.opml" (feeds.feedsToOpml wanted-feeds);
in {
inherit (fresh) wantedBy environment;
serviceConfig = {
inherit (fresh.serviceConfig) Type User Group StateDirectory WorkingDirectory
# hardening options
CapabilityBoundingSet DeviceAllow LockPersonality NoNewPrivileges PrivateDevices PrivateTmp PrivateUsers ProcSubset ProtectClock ProtectControlGroups ProtectHome ProtectHostname ProtectKernelLogs ProtectKernelModules ProtectKernelTunables ProtectProc ProtectSystem RemoveIPC RestrictNamespaces RestrictRealtime RestrictSUIDSGID SystemCallArchitectures SystemCallFilter UMask;
};
description = "import sane RSS feed list";
after = [ "freshrss-config.service" ];
script = ''
# easiest way to preserve feeds: delete the user, recreate it, import feeds
${pkgs.freshrss}/cli/delete-user.php --user colin || true
${pkgs.freshrss}/cli/create-user.php --user colin --password "$(cat ${config.services.freshrss.passwordFile})" || true
${pkgs.freshrss}/cli/import-for-user.php --user colin --filename ${opml}
'';
};
# the default ("*:0/5") is to run every 5 minutes.
# `systemctl list-timers` to show
systemd.services.freshrss-updater.startAt = lib.mkForce "*:3/30";
services.nginx.virtualHosts."rss.uninsane.org" = {
addSSL = true;
enableACME = true;
# inherit kTLS;
# the routing is handled by services.freshrss.virtualHost
};
sane.dns.zones."uninsane.org".inet.CNAME."rss" = "native";
}

View File

@@ -1,68 +0,0 @@
{ pkgs, ... }:
{
# based on <https://bytes.fyi/real-time-goaccess-reports-with-nginx/>
# log-format setting can be derived with this tool if custom:
# - <https://github.com/stockrt/nginx2goaccess>
# config options:
# - <https://github.com/allinurl/goaccess/blob/master/config/goaccess.conf>
systemd.services.goaccess = {
description = "GoAccess server monitoring";
serviceConfig = {
ExecStart = ''
${pkgs.goaccess}/bin/goaccess \
-f /var/log/nginx/public.log \
--log-format=VCOMBINED \
--real-time-html \
--html-refresh=30 \
--no-query-string \
--anonymize-ip \
--ignore-panel=HOSTS \
--ws-url=wss://sink.uninsane.org:443/ws \
--port=7890 \
-o /var/lib/uninsane/sink/index.html
'';
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Type = "simple";
Restart = "on-failure";
RestartSec = "10s";
# hardening
WorkingDirectory = "/tmp";
NoNewPrivileges = true;
PrivateTmp = true;
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";
ProtectKernelTunables = "yes";
};
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
};
# server statistics
services.nginx.virtualHosts."sink.uninsane.org" = {
addSSL = true;
enableACME = true;
# inherit kTLS;
root = "/var/lib/uninsane/sink";
locations."/ws" = {
proxyPass = "http://127.0.0.1:7890";
# XXX not sure how much of this is necessary
extraConfig = ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 7d;
'';
};
};
sane.dns.zones."uninsane.org".inet.CNAME."sink" = "native";
}

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
{ ... }:
{
sane.services.kiwix-serve = {
enable = true;
port = 8013;
zimPaths = [ "/var/lib/uninsane/www-archive/wikipedia_en_all_maxi_2022-05.zim" ];
};
services.nginx.virtualHosts."w.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
locations."/".proxyPass = "http://127.0.0.1:8013";
};
sane.dns.zones."uninsane.org".inet.CNAME."w" = "native";
}

View File

@@ -1,22 +0,0 @@
{ config, ... }:
let
svc-cfg = config.services.komga;
inherit (svc-cfg) user group port stateDir;
in
{
sane.persist.sys.byStore.plaintext = [
{ inherit user group; mode = "0700"; path = stateDir; }
];
services.komga.enable = true;
services.komga.port = 11319; # chosen at random
services.nginx.virtualHosts."komga.uninsane.org" = {
addSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${builtins.toString port}";
};
};
sane.dns.zones."uninsane.org".inet.CNAME."komga" = "native";
}

View File

@@ -1,85 +0,0 @@
# docs:
# - <repo:LemmyNet/lemmy:docker/federation/nginx.conf>
# - <repo:LemmyNet/lemmy:docker/nginx.conf>
# - <repo:LemmyNet/lemmy-ansible:templates/nginx.conf>
{ config, lib, pkgs, ... }:
let
inherit (builtins) toString;
inherit (lib) mkForce;
uiPort = 1234; # default ui port is 1234
backendPort = 8536; # default backend port is 8536
#^ i guess the "backend" port is used for federation?
pict-rs = pkgs.pict-rs.overrideAttrs (upstream: {
# as of v 0.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.
postPatch = (upstream.postPatch or "") + ''
substituteInPlace src/validate.rs \
--replace 'if transcode_options.needs_reencode() {' 'if false {'
'';
});
in {
services.lemmy = {
enable = true;
settings.hostname = "lemmy.uninsane.org";
# federation.debug forces outbound federation queries to be run synchronously
# N.B.: this option might not be read for 0.17.0+? <https://github.com/LemmyNet/lemmy/blob/c32585b03429f0f76d1e4ff738786321a0a9df98/RELEASES.md#upgrade-instructions>
# settings.federation.debug = true;
settings.port = backendPort;
ui.port = uiPort;
database.createLocally = true;
nginx.enable = true;
};
systemd.services.lemmy.serviceConfig = {
# fix to use a normal user so we can configure perms correctly
DynamicUser = mkForce false;
User = "lemmy";
Group = "lemmy";
};
systemd.services.lemmy.environment = {
RUST_BACKTRACE = "full";
# RUST_LOG = "debug";
# RUST_LOG = "trace";
# upstream defaults LEMMY_DATABASE_URL = "postgres:///lemmy?host=/run/postgresql";
# - Postgres complains that we didn't specify a user
# lemmy formats the url as:
# - postgres://{user}:{password}@{host}:{port}/{database}
# SO suggests (https://stackoverflow.com/questions/3582552/what-is-the-format-for-the-postgresql-connection-string-url):
# - postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
# LEMMY_DATABASE_URL = "postgres://lemmy@/run/postgresql"; # connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: FATAL: database "run/postgresql" does not exist
# LEMMY_DATABASE_URL = "postgres://lemmy?host=/run/postgresql"; # no PostgreSQL user name specified in startup packet
# LEMMY_DATABASE_URL = mkForce "postgres://lemmy@?host=/run/postgresql"; # WORKS
LEMMY_DATABASE_URL = mkForce "postgres://lemmy@/lemmy?host=/run/postgresql";
};
users.groups.lemmy = {};
users.users.lemmy = {
group = "lemmy";
isSystemUser = true;
};
services.nginx.virtualHosts."lemmy.uninsane.org" = {
forceSSL = true;
enableACME = true;
};
sane.dns.zones."uninsane.org".inet.CNAME."lemmy" = "native";
#v DO NOT REMOVE: defaults to 0.3, instead of latest, so always need to explicitly set this.
services.pict-rs.package = pict-rs;
# pict-rs configuration is applied in this order:
# - 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-max-frame-count" (builtins.toString (30*60*60))
"--media-process-timeout 120"
"--media-enable-full-video true" # allow audio
]);
}

View File

@@ -1,165 +0,0 @@
# docs: <https://nixos.wiki/wiki/Matrix>
# docs: <https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-synapse>
# example config: <https://github.com/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, ... }:
{
imports = [
./discord-puppet.nix
./irc.nix
./signal.nix
];
sane.persist.sys.byStore.plaintext = [
{ user = "matrix-synapse"; group = "matrix-synapse"; path = "/var/lib/matrix-synapse"; }
];
services.matrix-synapse.enable = true;
services.matrix-synapse.settings = {
# this changes the default log level from INFO to WARN.
# maybe there's an easier way?
log_config = ./synapse-log_level.yaml;
server_name = "uninsane.org";
# services.matrix-synapse.enable_registration_captcha = true;
# services.matrix-synapse.enable_registration_without_verification = true;
enable_registration = true;
# services.matrix-synapse.registration_shared_secret = "<shared key goes here>";
# default for listeners is port = 8448, tls = true, x_forwarded = false.
# we change this because the server is situated behind nginx.
listeners = [
{
port = 8008;
bind_addresses = [ "127.0.0.1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [ "client" "federation" ];
compress = false;
}
];
}
];
ip_range_whitelist = [
# to communicate with ntfy.uninsane.org push notifs.
# TODO: move this to some non-shared loopback device: we don't want Matrix spouting http requests to *anything* on this machine
"10.78.79.51"
];
x_forwarded = true; # because we proxy matrix behind nginx
max_upload_size = "100M"; # default is "50M"
admin_contact = "admin.matrix@uninsane.org";
registrations_require_3pid = [ "email" ];
};
services.matrix-synapse.extraConfigFiles = [
config.sops.secrets."matrix_synapse_secrets.yaml".path
];
systemd.services.matrix-synapse.postStart = ''
ACCESS_TOKEN=$(${pkgs.coreutils}/bin/cat ${config.sops.secrets.matrix_access_token.path})
TOPIC=$(${pkgs.coreutils}/bin/cat ${config.sops.secrets.ntfy-sh-topic.path})
echo "ensuring ntfy push gateway"
${pkgs.curl}/bin/curl \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--data "{ \"app_display_name\": \"ntfy-adapter\", \"app_id\": \"ntfy.uninsane.org\", \"data\": { \"url\": \"https://ntfy.uninsane.org/_matrix/push/v1/notify\", \"format\": \"event_id_only\" }, \"device_display_name\": \"ntfy-adapter\", \"kind\": \"http\", \"lang\": \"en-US\", \"profile_tag\": \"\", \"pushkey\": \"$TOPIC\" }" \
localhost:8008/_matrix/client/v3/pushers/set
echo "registered push gateways:"
${pkgs.curl}/bin/curl \
--header "Authorization: Bearer $ACCESS_TOKEN" \
localhost:8008/_matrix/client/v3/pushers \
| ${pkgs.jq}/bin/jq .
'';
# new users may be registered on the CLI:
# register_new_matrix_user -c /nix/store/8n6kcka37jhmi4qpd2r03aj71pkyh21s-homeserver.yaml http://localhost:8008
#
# or provide an registration token then can use to register through the client.
# docs: https://github.com/matrix-org/synapse/blob/develop/docs/usage/administration/admin_api/registration_tokens.md
# first, grab your own user's access token (Help & About section in Element). then:
# curl --header "Authorization: Bearer <my_token>" localhost:8008/_synapse/admin/v1/registration_tokens
# create a token with unlimited uses:
# curl -d '{}' --header "Authorization: Bearer <my_token>" localhost:8008/_synapse/admin/v1/registration_tokens/new
# create a token with limited uses:
# curl -d '{ "uses_allowed": 1 }' --header "Authorization: Bearer <my_token>" localhost:8008/_synapse/admin/v1/registration_tokens/new
# matrix chat server
# TODO: was `publog`
services.nginx.virtualHosts."matrix.uninsane.org" = {
addSSL = true;
enableACME = true;
# inherit kTLS;
# TODO colin: replace this with something helpful to the viewer
# locations."/".extraConfig = ''
# return 404;
# '';
locations."/" = {
proxyPass = "http://127.0.0.1:8008";
extraConfig = ''
# allow uploading large files (matrix enforces a separate limit, downstream)
client_max_body_size 512m;
'';
};
# redirect browsers to the web client.
# i don't think native matrix clients ever fetch the root.
# ideally this would be put behind some user-agent test though.
locations."= /" = {
return = "301 https://web.matrix.uninsane.org";
};
# locations."/_matrix" = {
# proxyPass = "http://127.0.0.1:8008";
# };
};
# matrix web client
# docs: https://nixos.org/manual/nixos/stable/index.html#module-services-matrix-element-web
services.nginx.virtualHosts."web.matrix.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
root = pkgs.element-web.override {
conf = {
default_server_config."m.homeserver" = {
"base_url" = "https://matrix.uninsane.org";
"server_name" = "uninsane.org";
};
};
};
};
sane.dns.zones."uninsane.org".inet = {
CNAME."matrix" = "native";
CNAME."web.matrix" = "native";
};
sops.secrets."matrix_synapse_secrets.yaml" = {
owner = config.users.users.matrix-synapse.name;
};
sops.secrets."matrix_access_token" = {
owner = config.users.users.matrix-synapse.name;
};
# provide access to ntfy-sh-topic secret
users.users.matrix-synapse.extraGroups = [ "ntfy-sh" ];
}

View File

@@ -1,13 +0,0 @@
diff --git a/src/irc/ConnectionInstance.ts b/src/irc/ConnectionInstance.ts
index 688036ca..3373fa27 100644
--- a/src/irc/ConnectionInstance.ts
+++ b/src/irc/ConnectionInstance.ts
@@ -149,7 +149,7 @@ export class ConnectionInstance {
if (this.dead) {
return Promise.resolve();
}
- ircReason = ircReason || reason;
+ ircReason = "bye"; // don't reveal through the IRC quit message that we're a bridge
log.info(
"disconnect()ing %s@%s - %s", this.nick, this.domain, reason
);

View File

@@ -1,50 +0,0 @@
diff --git a/config.schema.yml b/config.schema.yml
index 2e71c8d6..42ba8ba1 100644
--- a/config.schema.yml
+++ b/config.schema.yml
@@ -433,7 +433,7 @@ properties:
type: "boolean"
realnameFormat:
type: "string"
- enum: ["mxid","reverse-mxid"]
+ enum: ["mxid","reverse-mxid","localpart"]
ipv6:
type: "object"
properties:
diff --git a/src/irc/IdentGenerator.ts b/src/irc/IdentGenerator.ts
index 7a2b5cf1..50f7815a 100644
--- a/src/irc/IdentGenerator.ts
+++ b/src/irc/IdentGenerator.ts
@@ -74,6 +74,9 @@ export class IdentGenerator {
else if (server.getRealNameFormat() === "reverse-mxid") {
realname = IdentGenerator.sanitiseRealname(IdentGenerator.switchAroundMxid(matrixUser));
}
+ else if (server.getRealNameFormat() == "localpart") {
+ realname = IdentGenerator.sanitiseRealname(matrixUser.localpart);
+ }
else {
throw Error('Invalid value for realNameFormat');
}
diff --git a/src/irc/IrcServer.ts b/src/irc/IrcServer.ts
index 2af73ab4..895b9783 100644
--- a/src/irc/IrcServer.ts
+++ b/src/irc/IrcServer.ts
@@ -101,7 +101,7 @@ export interface IrcServerConfig {
};
lineLimit: number;
userModes?: string;
- realnameFormat?: "mxid"|"reverse-mxid";
+ realnameFormat?: "mxid"|"reverse-mxid"|"localpart";
pingTimeoutMs: number;
pingRateMs: number;
kickOn: {
@@ -289,7 +289,7 @@ export class IrcServer {
return this.config.ircClients.userModes || "";
}
- public getRealNameFormat(): "mxid"|"reverse-mxid" {
+ public getRealNameFormat(): "mxid"|"reverse-mxid"|"localpart" {
return this.config.ircClients.realnameFormat || "mxid";
}

View File

@@ -1,168 +0,0 @@
# config docs:
# - <https://github.com/matrix-org/matrix-appservice-irc/blob/develop/config.sample.yaml>
# probably want to remove that.
{ config, lib, ... }:
let
ircServer = { name, additionalAddresses ? [], sasl ? true, port ? 6697 }: let
lowerName = lib.toLower name;
in {
# XXX sasl: appservice doesn't support NickServ identification (only SASL, or PASS if sasl = false)
inherit name additionalAddresses sasl port;
ssl = true;
botConfig = {
# bot has no presence in IRC channel; only real Matrix users
enabled = false;
# this is the IRC username/nickname *of the bot* (not visible in channels): not of the end-user.
# the irc username/nick of a mapped Matrix user is determined further down in `ircClients` section.
# if `enabled` is false, then this name probably never shows up on the IRC side (?)
nick = "uninsane";
username = "uninsane";
joinChannelsIfNoUsers = false;
};
dynamicChannels = {
enabled = true;
aliasTemplate = "#irc_${lowerName}_$CHANNEL";
published = false; # false => irc rooms aren't listed in homeserver public rooms list
federate = false; # false => Matrix users from other homeservers can't join IRC channels
};
ircClients = {
nickTemplate = "$LOCALPARTsane"; # @colin:uninsane.org (Matrix) -> colinsane (IRC)
realnameFormat = "reverse-mxid"; # @colin:uninsane.org (Matrix) -> org.uninsane:colin (IRC)
# realnameFormat = "localpart"; # @colin:uninsane.org (Matrix) -> colin (IRC) -- but requires the mxid patch below
# by default, Matrix will convert messages greater than (3) lines into a pastebin-like URL to send to IRC.
lineLimit = 20;
# Rizon in particular allows only 4 connections from one IP before a 30min ban.
# that's effectively reduced to 2 during a netsplit, or maybe during a restart.
# - https://wiki.rizon.net/index.php?title=Connection/Session_Limit_Exemptions
# especially, misconfigurations elsewhere in this config may cause hundreds of connections
# so this is a safeguard.
maxClients = 2;
# don't have the bridge disconnect me from IRC when idle.
idleTimeout = 0;
concurrentReconnectLimit = 2;
reconnectIntervalMs = 60000;
kickOn = {
# remove Matrix user from room when...
channelJoinFailure = false;
ircConnectionFailure = false;
userQuit = true;
};
};
matrixClients = {
userTemplate = "@irc_${lowerName}_$NICK"; # the :uninsane.org part is appended automatically
};
# this will let this user message the appservice with `!join #<IRCChannel>` and the rest "Just Works"
"@colin:uninsane.org" = "admin";
membershipLists = {
enabled = true;
global = {
ircToMatrix = {
initial = true;
incremental = true;
requireMatrixJoined = false;
};
matrixToIrc = {
initial = true;
incremental = true;
};
};
ignoreIdleUsersOnStartup = {
enabled = false; # false => always bridge users, even if idle
};
};
# sync room description?
bridgeInfoState = {
enabled = true;
initial = true;
};
# for per-user IRC password:
# - invite @irc_${lowerName}_NickServ:uninsane.org to a DM and type `help` => register
# - invite the matrix-appservice-irc user to a DM and type `!help` => add PW to database
# to validate that i'm authenticated on the IRC network, DM @irc_${lowerName}_NickServ:uninsane.org:
# - send: `STATUS colinsane`
# - response should be `3`: "user recognized as owner via password identification"
# passwordEncryptionKeyPath = "/path/to/privkey"; # appservice will generate its own if unspecified
};
in
{
nixpkgs.overlays = [
(next: prev: {
matrix-appservice-irc = prev.matrix-appservice-irc.overrideAttrs (super: {
patches = super.patches or [] ++ [
./irc-no-reveal-bridge.patch
# ./irc-no-reveal-mxid.patch
];
});
})
];
sane.persist.sys.byStore.plaintext = [
# TODO: mode?
{ 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,
# which requires matrix-appservice-irc to be of that group
users.users.matrix-appservice-irc.extraGroups = [ "matrix-synapse" ];
# weird race conditions around registration.yml mean we want matrix-synapse to be of matrix-appservice-irc group too.
users.users.matrix-synapse.extraGroups = [ "matrix-appservice-irc" ];
services.matrix-synapse.settings.app_service_config_files = [
"/var/lib/matrix-appservice-irc/registration.yml" # auto-created by irc appservice
];
services.matrix-appservice-irc.enable = true;
services.matrix-appservice-irc.registrationUrl = "http://127.0.0.1:8009";
services.matrix-appservice-irc.settings = {
homeserver = {
url = "http://127.0.0.1:8008";
dropMatrixMessagesAfterSecs = 300;
domain = "uninsane.org";
enablePresence = true;
bindPort = 9999;
bindHost = "127.0.0.1";
};
ircService = {
servers = {
"irc.esper.net" = ircServer {
name = "esper";
sasl = false;
# notable channels:
# - #merveilles
};
"irc.libera.chat" = ircServer {
name = "libera";
sasl = false;
# notable channels:
# - #hare
# - #mnt-reform
};
"irc.myanonamouse.net" = ircServer {
name = "MyAnonamouse";
additionalAddresses = [ "irc2.myanonamouse.net" ];
sasl = false;
};
"irc.oftc.net" = ircServer {
name = "oftc";
sasl = false;
# notable channels:
# - #sxmo
# - #sxmo-offtopic
};
"irc.rizon.net" = ircServer { name = "Rizon"; };
};
};
};
systemd.services.matrix-appservice-irc.serviceConfig = {
# XXX 2023/06/20: nixos specifies this + @aio and @memlock as forbidden
# the service actively uses at least one of these, and both of them are fairly innocuous
SystemCallFilter = lib.mkForce "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @setuid @swap";
};
}

View File

@@ -1,37 +0,0 @@
# config options:
# - <https://github.com/mautrix/signal/blob/master/mautrix_signal/example-config.yaml>
{ config, pkgs, ... }:
{
sane.persist.sys.byStore.plaintext = [
{ user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; }
{ user = "signald"; group = "signald"; path = "/var/lib/signald"; }
];
# allow synapse to read the registration file
users.users.matrix-synapse.extraGroups = [ "mautrix-signal" ];
services.signald.enable = true;
services.mautrix-signal.enable = true;
services.mautrix-signal.environmentFile =
config.sops.secrets.mautrix_signal_env.path;
services.mautrix-signal.settings.signal.socket_path = "/run/signald/signald.sock";
services.mautrix-signal.settings.homeserver.domain = "uninsane.org";
services.mautrix-signal.settings.bridge.permissions."@colin:uninsane.org" = "admin";
services.matrix-synapse.settings.app_service_config_files = [
# auto-created by mautrix-signal service
"/var/lib/mautrix-signal/signal-registration.yaml"
];
systemd.services.mautrix-signal.serviceConfig = {
# allow communication to signald
SupplementaryGroups = [ "signald" ];
ReadWritePaths = [ "/run/signald" ];
};
sops.secrets."mautrix_signal_env" = {
mode = "0440";
owner = config.users.users.mautrix-signal.name;
group = config.users.users.matrix-synapse.name;
};
}

View File

@@ -1,40 +0,0 @@
{ lib, ... }:
{
sane.persist.sys.byStore.plaintext = [
{ user = "navidrome"; group = "navidrome"; path = "/var/lib/navidrome"; }
];
services.navidrome.enable = true;
services.navidrome.settings = {
# docs: https://www.navidrome.org/docs/usage/configuration-options/
Address = "127.0.0.1";
Port = 4533;
MusicFolder = "/var/lib/uninsane/media/Music";
CovertArtPriority = "*.jpg, *.JPG, *.png, *.PNG, embedded";
AutoImportPlaylists = false;
ScanSchedule = "@every 1h";
};
systemd.services.navidrome.serviceConfig = {
# fix to use a normal user so we can configure perms correctly
DynamicUser = lib.mkForce false;
User = "navidrome";
Group = "navidrome";
};
users.groups.navidrome = {};
users.users.navidrome = {
group = "navidrome";
isSystemUser = true;
};
services.nginx.virtualHosts."music.uninsane.org" = {
forceSSL = true;
enableACME = true;
# inherit kTLS;
locations."/".proxyPass = "http://127.0.0.1:4533";
};
sane.dns.zones."uninsane.org".inet.CNAME."music" = "native";
}

View File

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

View File

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

View File

@@ -1,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;
};
}

View File

@@ -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"; }
];
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.wan = true;
description = "colin-ntfy.uninsane.org";
};
}

View File

@@ -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()

View File

@@ -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";
src = ./.;
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.lan = true;
visibleTo.wan = true;
description = "colin-notification-waiter-${builtins.toString (port+1)}-of-${builtins.toString numPorts}";
};
}));
systemd.services = lib.mkMerge (builtins.map mkService portRange);
};
}

View File

@@ -1,23 +0,0 @@
# pict-rs is an image database/store used by Lemmy.
# i don't explicitly activate it here -- just adjust its defaults to be a bit friendlier
{ config, lib, ... }:
let
cfg = config.services.pict-rs;
in
{
sane.persist.sys.byStore.plaintext = lib.mkIf cfg.enable [
{ user = "pict-rs"; group = "pict-rs"; path = cfg.dataDir; }
];
systemd.services.pict-rs.serviceConfig = {
# fix to use a normal user so we can configure perms correctly
DynamicUser = lib.mkForce false;
User = "pict-rs";
Group = "pict-rs";
};
users.groups.pict-rs = {};
users.users.pict-rs = {
group = "pict-rs";
isSystemUser = true;
};
}

View File

@@ -1,89 +0,0 @@
{ pkgs, ... }:
let
GiB = n: MiB 1024*n;
MiB = n: KiB 1024*n;
KiB = n: 1024*n;
in
{
sane.persist.sys.byStore.plaintext = [
# TODO: mode?
{ user = "postgres"; group = "postgres"; path = "/var/lib/postgresql"; }
];
services.postgresql.enable = true;
# HOW TO UPDATE:
# postgres version updates are manual and require intervention.
# - `sane-stop-all-servo`
# - `systemctl start postgresql`
# - as `sudo su postgres`:
# - `cd /var/log/postgresql`
# - `pg_dumpall > state.sql`
# - `echo placeholder > <new_version>` # to prevent state from being created earlier than we want
# - then, atomically:
# - update the `services.postgresql.package` here
# - `dataDir` is atomically updated to match package; don't touch
# - `nixos-rebuild --flake . switch ; sane-stop-all-servo`
# - `sudo rm -rf /var/lib/postgresql/<new_version>`
# - `systemctl start postgresql`
# - as `sudo su postgres`:
# - `cd /var/lib/postgreql`
# - `psql -f state.sql`
# - 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.
# services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
# CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD '<password goes here>';
# CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
# TEMPLATE template0
# ENCODING = "UTF8"
# LC_COLLATE = "C"
# LC_CTYPE = "C";
# '';
# perf tuning
# - for recommended values see: <https://pgtune.leopard.in.ua/>
# - for official docs (sparse), see: <https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
services.postgresql.settings = {
# 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
services.postgresqlBackup.enable = true;
# common admin operations:
# sudo systemctl start postgresql
# sudo -u postgres psql
# > \l # lists all databases
# > \du # lists all roles
# > \c pleroma # connects to database by name
# > \d # shows all tables
# > \q # exits psql
# dump/restore (-F t = tar):
# sudo -u postgres pg_dump -F t pleroma > /backup/pleroma-db.tar
# sudo -u postgres -g postgres pg_restore -d pleroma /backup/pleroma-db.tar
}

View File

@@ -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"; }
];
sane.ports.ports."5000" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-prosody-fileshare-proxy65";
};
sane.ports.ports."5222" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpp-client-to-server";
};
sane.ports.ports."5223" = {
protocol = [ "tcp" ];
visibleTo.lan = true;
visibleTo.wan = true;
description = "colin-xmpps-client-to-server"; # XMPP over TLS
};
sane.ports.ports."5269" = {
protocol = [ "tcp" ];
visibleTo.wan = true;
description = "colin-xmpp-server-to-server";
};
sane.ports.ports."5270" = {
protocol = [ "tcp" ];
visibleTo.wan = true;
description = "colin-xmpps-server-to-server"; # XMPP over TLS
};
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?
};
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
'';
};
}

View File

@@ -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);

View File

@@ -1,95 +0,0 @@
{ config, pkgs, ... }:
{
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"; }
];
users.users.transmission.extraGroups = [ "media" ];
services.transmission.enable = true;
services.transmission.package = pkgs.transmission_4; #< 2023/09/06: nixpkgs `transmission` defaults to old 3.00
#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 = {
# message-level = 3; #< enable for debug logging. 0-3, default is 2.
# 0.0.0.0 => allow rpc from any host: we gate it via firewall and auth requirement
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;
# 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 = 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 regularly fails to move stuff from the incomplete dir to the main one, so disable:
# TODO: uncomment this line!
incomplete-dir-enabled = false;
};
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";
Restart = "on-failure";
RestartSec = "30s";
};
# 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";
}

View File

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

View File

@@ -1,30 +0,0 @@
# docs: <https://nixos.wiki/wiki/MediaWiki>
{ config, lib, ... }:
# XXX: working to host wikipedia with kiwix instead of mediawiki
# mediawiki does more than i need and isn't obviously superior in any way
# except that the dumps are more frequent/up-to-date.
lib.mkIf false
{
sops.secrets."mediawiki_pw" = {
owner = config.users.users.mediawiki.name;
};
services.mediawiki.enable = true;
services.mediawiki.name = "Uninsane Wiki";
services.mediawiki.passwordFile = config.sops.secrets.mediawiki_pw.path;
services.mediawiki.extraConfig = ''
# Disable anonymous editing
$wgGroupPermissions['*']['edit'] = false;
'';
services.mediawiki.virtualHost.listen = [
{
ip = "127.0.0.1";
port = 8013;
ssl = false;
}
];
services.mediawiki.virtualHost.hostName = "w.uninsane.org";
services.mediawiki.virtualHost.adminAddr = "admin+mediawiki@uninsane.org";
# services.mediawiki.extensions = TODO: wikipedia sync extension?
}

View File

@@ -1,102 +0,0 @@
{ lib, pkgs, ... }:
{
imports = [
./feeds.nix
./fs.nix
./hardware
./home
./hosts.nix
./ids.nix
./machine-id.nix
./net.nix
./nix-path
./persist.nix
./programs
./secrets.nix
./ssh.nix
./users
./vpn.nix
];
sane.nixcache.enable-trusted-keys = true;
sane.nixcache.enable = lib.mkDefault true;
sane.persist.enable = lib.mkDefault true;
sane.programs.sysadminUtils.enableFor.system = 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 = "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;
services.journald.extraConfig = ''
# docs: `man journald.conf`
# merged journald config is deployed to /etc/systemd/journald.conf
[Journal]
# disable journal compression because the underlying fs is compressed
Compress=no
'';
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 = {
supportsDryActivation = true;
text = ''
# show which packages changed versions or are new/removed in this upgrade
# source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix>
${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
'';
};
# 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
# hopefully picked up by gdb automatically?
environment.enableDebugInfo = true;
}

View File

@@ -1,249 +0,0 @@
# where to find good stuff?
# - podcasts w/ a community: <https://lemmyverse.net/communities?query=podcast>
# - podcast rec thread: <https://lemmy.ml/post/1565858>
#
# candidates:
# - The Nonlinear Library (podcast): <https://forum.effectivealtruism.org/posts/JTZTBienqWEAjGDRv/listen-to-more-ea-content-with-the-nonlinear-library>
# - has ~10 posts per day, text-to-speech; i would need better tagging before adding this
# - <https://www.metaculus.com/questions/11102/introducing-the-metaculus-journal-podcast/>
# - dead since 2022/10 - 2023/03
{ lib, sane-data, ... }:
let
hourly = { freq = "hourly"; };
daily = { freq = "daily"; };
weekly = { freq = "weekly"; };
infrequent = { freq = "infrequent"; };
art = { cat = "art"; };
humor = { cat = "humor"; };
pol = { cat = "pol"; }; # or maybe just "social"
rat = { cat = "rat"; };
tech = { cat = "tech"; };
uncat = { cat = "uncat"; };
text = { format = "text"; };
img = { format = "image"; };
mkRss = format: url: { inherit url format; } // uncat // infrequent;
# format-specific helpers
mkText = mkRss "text";
mkImg = mkRss "image";
mkPod = mkRss "podcast";
# host-specific helpers
mkSubstack = subdomain: { substack = subdomain; };
fromDb = name:
let
raw = sane-data.feeds."${name}";
in {
url = raw.url;
# not sure the exact mapping with velocity here: entries per day?
freq = lib.mkIf (raw.velocity or 0 != 0) (lib.mkDefault (
if raw.velocity > 2 then
"hourly"
else if raw.velocity > 0.5 then
"daily"
else if raw.velocity > 0.1 then
"weekly"
else
"infrequent"
));
} // lib.optionalAttrs (raw.is_podcast or false) {
format = "podcast";
} // lib.optionalAttrs (raw.title or "" != "") {
title = lib.mkDefault raw.title;
};
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 "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.libsyn.com/421877" // rat) # Less Wrong Curated
(fromDb "feeds.megaphone.fm/behindthebastards" // pol) # also Maggie Killjoy
(fromDb "feeds.megaphone.fm/hubermanlab" // uncat) # Daniel Huberman on sleep
(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 "lexfridman.com/podcast" // rat)
(fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies
(fromDb "omegataupodcast.net" // tech) # 3/4 German; 1/4 eps are English
(fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol) # Maggie Killjoy -- referenced by Cory Doctorow
(fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol)
(fromDb "podcast.thelinuxexp.com" // tech)
(fromDb "politicalorphanage.libsyn.com" // pol)
(fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast
(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 "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 "techwontsave.us" // pol) # rec by Cory Doctorow
# (fromDb "trashfuturepodcast.podbean.com" // pol) # rec by Cory Doctorow, but way rambly
(fromDb "wakingup.libsyn.com" // pol) # Sam Harris
(fromDb "werenotwrong.fireside.fm" // pol)
# (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
# (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)
];
texts = [
# AGGREGATORS (> 1 post/day)
(fromDb "lwn.net" // tech)
# (fromDb "lesswrong.com" // rat)
# (fromDb "econlib.org" // pol)
# AGGREGATORS (< 1 post/day)
(fromDb "palladiummag.com" // uncat)
(fromDb "profectusmag.com" // uncat)
(fromDb "semiaccurate.com" // tech)
(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 "amosbbatto.wordpress.com" // tech)
(fromDb "fasterthanli.me" // 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)
(fromDb "artemis.sh" // 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)
(fromDb "harihareswara.net" // tech // pol) # rec by Cory Doctorow
(fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow
# (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly)
# TECH PROJECTS
(fromDb "blog.rust-lang.org" // tech)
# (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 = [
(fromDb "smbc-comics.com" // img // humor)
(fromDb "xkcd.com" // img // humor)
(fromDb "turnoff.us" // 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
{
sane.feeds = texts ++ images ++ podcasts;
assertions = builtins.map
(p: {
assertion = p.format or "unknown" == "podcast";
message = ''${p.url} is not a podcast: ${p.format or "unknown"}'';
})
podcasts;
}

View File

@@ -1,143 +0,0 @@
# docs
# - x-systemd options: <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
{ lib, pkgs, sane-lib, ... }:
let
fsOpts = rec {
common = [
"_netdev"
"noatime"
"user" # allow any user with access to the device to mount the fs
"x-systemd.requires=network-online.target"
"x-systemd.after=network-online.target"
"x-systemd.mount-timeout=10s" # how long to wait for mount **and** how long to wait for unmount
];
auto = [ "x-systemd.automount" ];
noauto = [ "noauto" ]; # don't mount as part of remote-fs.target
wg = [
"x-systemd.requires=wireguard-wg-home.service"
"x-systemd.after=wireguard-wg-home.service"
];
ssh = common ++ [
"identityfile=/home/colin/.ssh/id_ed25519"
"allow_other"
"default_permissions"
];
sshColin = ssh ++ [
"transform_symlinks"
"idmap=user"
"uid=1000"
"gid=100"
];
sshRoot = ssh ++ [
# we don't transform_symlinks because that breaks the validity of remote /nix stores
"sftp_server=/run/wrappers/bin/sudo\\040/run/current-system/sw/libexec/sftp-server"
];
# in the event of hunt NFS mounts, consider:
# - <https://unix.stackexchange.com/questions/31979/stop-broken-nfs-mounts-from-locking-a-directory>
# NFS options: <https://linux.die.net/man/5/nfs>
# actimeo=n = how long (in seconds) to cache file/dir attributes (default: 3-60s)
# bg = retry failed mounts in the background
# retry=n = for how many minutes `mount` will retry NFS mount operation
# 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)
# 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.
nfs = common ++ [
# "actimeo=10"
"bg"
"retrans=4"
"retry=0"
"soft"
"timeo=15"
"nofail" # don't fail remote-fs.target when this mount fails (not an option for sshfs else would be common)
];
};
remoteHome = host: {
fileSystems."/mnt/${host}-home" = {
device = "colin@${host}:/home/colin";
fsType = "fuse.sshfs";
options = fsOpts.sshColin ++ fsOpts.noauto;
noCheck = true;
};
sane.fs."/mnt/${host}-home" = sane-lib.fs.wantedDir;
};
in
lib.mkMerge [
{
# some services which use private directories error if the parent (/var/lib/private) isn't 700.
sane.fs."/var/lib/private".dir.acl.mode = "0700";
# in-memory compressed RAM
# defaults to compressing at most 50% size of RAM
# claimed compression ratio is about 2:1
# - but on moby w/ zstd default i see 4-7:1 (ratio lowers as it fills)
# note that idle overhead is about 0.05% of capacity (e.g. 2B per 4kB page)
# docs: <https://www.kernel.org/doc/Documentation/blockdev/zram.txt>
#
# to query effectiveness:
# `cat /sys/block/zram0/mm_stat`. whitespace separated fields:
# - *orig_data_size* (bytes)
# - *compr_data_size* (bytes)
# - mem_used_total (bytes)
# - mem_limit (bytes)
# - mem_used_max (bytes)
# - *same_pages* (pages which are e.g. all zeros (consumes no additional mem))
# - *pages_compacted* (pages which have been freed thanks to compression)
# - huge_pages (incompressible)
#
# see also:
# - `man zramctl`
zramSwap.enable = true;
# how much ram can be swapped into the zram device.
# this shouldn't be higher than the observed compression ratio.
# the default is 50% (why?)
# 100% should be "guaranteed" safe so long as the data is even *slightly* compressible.
# but it decreases working memory under the heaviest of loads by however much space the compressed memory occupies (e.g. 50% if 2:1; 25% if 4:1)
zramSwap.memoryPercent = 100;
# fileSystems."/mnt/servo-nfs" = {
# device = "servo-hn:/";
# noCheck = true;
# 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-nfs/playground" = {
device = "servo-hn:/playground";
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";
environment.pathsToLink = [
# needed to achieve superuser access for user-mounted filesystems (see optionsRoot above)
# we can only link whole directories here, even though we're only interested in pkgs.openssh
"/libexec"
];
environment.systemPackages = [
pkgs.sshfs-fuse
];
}
(remoteHome "desko")
(remoteHome "lappy")
(remoteHome "moby")
]

View File

@@ -1,73 +0,0 @@
{ lib, pkgs, ... }:
{
imports = [
./x86_64.nix
];
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.nvme-cli}/bin/nvme
copy_bin_and_libs ${pkgs.e2fsprogs}/bin/resize2fs
'';
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"
# 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;
# linux CPU governor: <https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt>
# - options:
# - "powersave" => force CPU to always run at lowest supported frequency
# - "performance" => force CPU to always run at highest frequency
# - "ondemand" => adjust frequency based on load
# - "conservative" (ondemand but slower to adjust)
# - "schedutil"
# - "userspace"
# - not all options are available for all platforms
# - intel (intel_pstate) appears to manage scaling w/o intervention/control from the OS.
# - AMD (acpi-cpufreq) appears to manage scaling via the OS *or* HW. but the ondemand defaults never put it to max hardware frequency.
# - qualcomm (cpufreq-dt) appears to manage scaling *only* via the OS. ondemand governor exercises the full range.
# - query details with `sudo cpupower frequency-info`
powerManagement.cpuFreqGovernor = "ondemand";
services.logind.extraConfig = ''
# dont shutdown when power button is short-pressed
HandlePowerKey=ignore
'';
# services.snapper.configs = {
# root = {
# subvolume = "/";
# extraConfig = {
# ALLOW_USERS = "colin";
# };
# };
# };
# services.snapper.snapshotInterval = "daily";
}

View File

@@ -1,9 +0,0 @@
{ ... }:
{
imports = [
./keyring
./mime.nix
./ssh.nix
./xdg-dirs.nix
];
}

View File

@@ -1,17 +0,0 @@
{ config, pkgs, sane-lib, ... }:
let
init-keyring = pkgs.static-nix-shell.mkBash {
pname = "init-keyring";
src = ./.;
};
in
{
sane.user.persist.byStore.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`
};
}

View File

@@ -1,21 +0,0 @@
#!/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

View File

@@ -1,28 +0,0 @@
{ config, lib, ...}:
let
# ProgramConfig -> { "<mime-type>" = { priority, desktop }; }
weightedMimes = prog: builtins.mapAttrs (_key: desktop: { priority = prog.mime.priority; desktop = desktop; }) prog.mime.associations;
# [ { "<mime-type>" = { priority, desktop } ]; } ] -> { "<mime-type>" = [ { priority, desktop } ... ]; }
mergeMimes = mimes: lib.foldAttrs (item: acc: [item] ++ acc) [] mimes;
# [ { priority, desktop } ... ] -> Self
sortOneMimeType = associations: builtins.sort (l: r: assert l.priority != r.priority; l.priority < r.priority) associations;
sortMimes = mimes: builtins.mapAttrs (_k: sortOneMimeType) mimes;
removePriorities = mimes: builtins.mapAttrs (_k: associations: builtins.map (a: a.desktop) associations) mimes;
# [ ProgramConfig ]
enabledPrograms = builtins.filter (p: p.enabled) (builtins.attrValues config.sane.programs);
# [ { "<mime-type>" = { prority, desktop } ]
enabledWeightedMimes = builtins.map weightedMimes enabledPrograms;
in
{
# the xdg mime type for a file can be found with:
# - `xdg-mime query filetype path/to/thing.ext`
# the default handler for a mime type can be found with:
# - `xdg-mime query default <mimetype>` (e.g. x-scheme-handler/http)
#
# we can have single associations or a list of associations.
# there's also options to *remove* [non-default] associations from specific apps
xdg.mime.enable = true;
xdg.mime.defaultApplications = removePriorities (sortMimes (mergeMimes enabledWeightedMimes));
}

View File

@@ -1,29 +0,0 @@
# TODO: this should be moved to users/colin.nix
{ config, lib, ... }:
let
host = config.networking.hostName;
user-pubkey-full = config.sane.ssh.pubkeys."colin@${host}" or {};
user-pubkey = user-pubkey-full.asUserKey or null;
host-keys = lib.filter (k: k.user == "root") (lib.attrValues config.sane.ssh.pubkeys);
known-hosts-text = lib.concatStringsSep
"\n"
(builtins.map (k: k.asHostKey) host-keys)
;
in
{
# ssh key is stored in private storage
sane.user.persist.byStore.private = [
{ type = "file"; path = ".ssh/id_ed25519"; }
];
sane.user.fs.".ssh/id_ed25519.pub" = lib.mkIf (user-pubkey != null) {
symlink.text = user-pubkey;
};
sane.user.fs.".ssh/known_hosts".symlink.text = known-hosts-text;
users.users.colin.openssh.authorizedKeys.keys =
let
user-keys = lib.filter (k: k.user == "colin") (lib.attrValues config.sane.ssh.pubkeys);
in
builtins.map (k: k.asUserKey) user-keys;
}

View File

@@ -1,20 +0,0 @@
{ ... }:
{
# XDG defines things like ~/Desktop, ~/Downloads, etc.
# these clutter the home, so i mostly don't use them.
sane.user.fs.".config/user-dirs.dirs".symlink.text = ''
XDG_DESKTOP_DIR="$HOME/.xdg/Desktop"
XDG_DOCUMENTS_DIR="$HOME/dev"
XDG_DOWNLOAD_DIR="$HOME/tmp"
XDG_MUSIC_DIR="$HOME/Music"
XDG_PICTURES_DIR="$HOME/Pictures"
XDG_PUBLICSHARE_DIR="$HOME/.xdg/Public"
XDG_TEMPLATES_DIR="$HOME/.xdg/Templates"
XDG_VIDEOS_DIR="$HOME/Videos"
'';
# prevent `xdg-user-dirs-update` from overriding/updating our config
# see <https://manpages.ubuntu.com/manpages/bionic/man5/user-dirs.conf.5.html>
sane.user.fs.".config/user-dirs.conf".symlink.text = "enabled=False";
}

View File

@@ -1,39 +0,0 @@
{ lib, ... }:
{
# TODO: this should be populated per-host
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";
};
}

View File

@@ -1,93 +0,0 @@
# TODO: migrate to nixpkgs `config.ids.uids`
# - note that nixpkgs' `config.ids.uids` is strictly a database: it doesn't set anything by default
# whereas our impl sets the gid/uid of the user/group specified if they exist.
{ ... }:
{
# legacy servo users, some are inconvenient to migrate
sane.ids.dhcpcd.gid = 991;
sane.ids.dhcpcd.uid = 992;
sane.ids.gitea.gid = 993;
sane.ids.git.uid = 994;
sane.ids.jellyfin.gid = 994;
sane.ids.pleroma.gid = 995;
sane.ids.jellyfin.uid = 996;
sane.ids.acme.gid = 996;
sane.ids.pleroma.uid = 997;
sane.ids.acme.uid = 998;
sane.ids.matrix-appservice-irc.uid = 993;
sane.ids.matrix-appservice-irc.gid = 992;
# greetd (used by sway)
sane.ids.greeter.uid = 999;
sane.ids.greeter.gid = 999;
# new servo users
sane.ids.freshrss.uid = 2401;
sane.ids.freshrss.gid = 2401;
sane.ids.mediawiki.uid = 2402;
sane.ids.signald.uid = 2403;
sane.ids.signald.gid = 2403;
sane.ids.mautrix-signal.uid = 2404;
sane.ids.mautrix-signal.gid = 2404;
sane.ids.navidrome.uid = 2405;
sane.ids.navidrome.gid = 2405;
sane.ids.calibre-web.uid = 2406;
sane.ids.calibre-web.gid = 2406;
sane.ids.komga.uid = 2407;
sane.ids.komga.gid = 2407;
sane.ids.lemmy.uid = 2408;
sane.ids.lemmy.gid = 2408;
sane.ids.pict-rs.uid = 2409;
sane.ids.pict-rs.gid = 2409;
sane.ids.sftpgo.uid = 2410;
sane.ids.sftpgo.gid = 2410;
sane.ids.trust-dns.uid = 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.colin.uid = 1000;
sane.ids.guest.uid = 1100;
# found on all hosts
sane.ids.sshd.uid = 2001; # 997
sane.ids.sshd.gid = 2001; # 997
sane.ids.polkituser.gid = 2002; # 998
sane.ids.systemd-coredump.gid = 2003; # 996 # 2023/02/12-2023/02/28: upstream temporarily specified this as 151
sane.ids.nscd.uid = 2004;
sane.ids.nscd.gid = 2004;
sane.ids.systemd-oom.uid = 2005;
sane.ids.systemd-oom.gid = 2005;
sane.ids.wireshark.gid = 2006;
# found on graphical hosts
sane.ids.nm-iodine.uid = 2101; # desko/moby/lappy
# found on desko host
# from services.usbmuxd
sane.ids.usbmux.uid = 2204;
sane.ids.usbmux.gid = 2204;
# originally found on moby host
# gnome core-shell
sane.ids.avahi.uid = 2304;
sane.ids.avahi.gid = 2304;
sane.ids.colord.uid = 2305;
sane.ids.colord.gid = 2305;
sane.ids.geoclue.uid = 2306;
sane.ids.geoclue.gid = 2306;
# gnome core-os-services
sane.ids.rtkit.uid = 2307;
sane.ids.rtkit.gid = 2307;
# phosh
sane.ids.feedbackd.gid = 2308;
# new moby users
sane.ids.eg25-control.uid = 2309;
sane.ids.eg25-control.gid = 2309;
}

View File

@@ -1,16 +0,0 @@
{ ... }:
{
# /etc/machine-id is a globally unique identifier used for:
# - systemd-networkd: DHCP lease renewal (instead of keying by the MAC address)
# - systemd-journald: to filter logs by host
# - chromium (potentially to track re-installations)
# - gdbus; system services that might upgrade to AF_LOCAL if both services can confirm they're on the same machine
# because of e.g. the chromium use, we *don't want* to persist this.
# however, `journalctl` won't show logs from previous boots unless the machine-ids match.
# so for now, generate something unique from the host ssh key.
# TODO: move this into modules?
system.activationScripts.machine-id = {
deps = [ "etc" ];
text = "sha256sum /etc/ssh/host_keys/ssh_host_ed25519_key | cut -c 1-32 > /etc/machine-id";
};
}

View File

@@ -1,47 +0,0 @@
{ lib, ... }:
{
# the default backend is "wpa_supplicant".
# wpa_supplicant reliably picks weak APs to connect to.
# see: <https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/474>
# iwd is an alternative that shouldn't have this problem
# docs:
# - <https://nixos.wiki/wiki/Iwd>
# - <https://iwd.wiki.kernel.org/networkmanager>
# - `man iwd.config` for global config
# - `man iwd.network` for per-SSID config
# use `iwctl` to control
# networking.networkmanager.wifi.backend = "iwd";
# networking.wireless.iwd.enable = true;
# networking.wireless.iwd.settings = {
# # auto-connect to a stronger network if signal drops below this value
# # bedroom -> bedroom connection is -35 to -40 dBm
# # bedroom -> living room connection is -60 dBm
# General.RoamThreshold = "-52"; # default -70
# General.RoamThreshold5G = "-52"; # default -76
# };
# plugins mostly add support for establishing different VPN connections.
# the default plugin set includes mostly proprietary VPNs:
# - fortisslvpn (Fortinet)
# - iodine (DNS tunnels)
# - l2tp
# - openconnect (Cisco Anyconnect / Juniper / ocserv)
# - openvpn
# - vpnc (Cisco VPN)
# - sstp
#
# i don't use these, and notably they drag in huge dependency sets and don't cross compile well.
# e.g. openconnect drags in webkitgtk (for SSO)!
networking.networkmanager.plugins = lib.mkForce [];
networking.firewall.allowedUDPPorts = [
1900 # to received UPnP advertisements. required by sane-ip-check-upnp
];
# keyfile.path = where networkmanager should look for connection credentials
networking.networkmanager.extraConfig = ''
[keyfile]
path=/var/lib/NetworkManager/system-connections
'';
}

View File

@@ -1,13 +0,0 @@
{ pkgs, ... }:
{
# allow `nix-shell` (and probably nix-index?) to locate our patched and custom packages
nix.nixPath = [
"nixpkgs=${pkgs.path}"
# note the import starts at repo root: this allows `./overlay/default.nix` to access the stuff at the root
# "nixpkgs-overlays=${../../..}/hosts/common/nix-path/overlay"
# as long as my system itself doesn't rely on NIXPKGS at runtime, we can point the overlays to git
# to avoid switching so much during development
"nixpkgs-overlays=/home/colin/dev/nixos/hosts/common/nix-path/overlay"
];
}

View File

@@ -1,4 +0,0 @@
# XXX: NIX_PATH=...:nixpkgs-overlays=... will import every overlay in the directory
# so we prefer to give it a directory with just this *one* overlay, otherwise it imports conflicting overlays
# and gets stuck in a loop until it OOMs
import ../../../../overlays/all.nix

View File

@@ -1,16 +0,0 @@
{ ... }:
{
sane.persist.stores.private.origin = "/home/colin/private";
# store /home/colin/a/b in /home/private/a/b instead of /home/private/home/colin/a/b
sane.persist.stores.private.prefix = "/home/colin";
sane.persist.sys.byStore.plaintext = [
# TODO: these should be private.. somehow
"/var/log"
"/var/backup" # for e.g. postgres dumps
];
sane.persist.sys.byStore.cryptClearOnBoot = [
"/var/lib/systemd/coredump"
];
}

View File

@@ -1,9 +0,0 @@
# Terminal UI mail client
{ ... }:
{
sane.programs.aerc = {
secrets.".config/aerc/accounts.conf" = ../../../secrets/common/aerc_accounts.conf.bin;
mime.associations."x-scheme-handler/mailto" = "aerc.desktop";
};
}

View File

@@ -1,24 +0,0 @@
# alacritty terminal emulator
# - config options: <https://github.com/alacritty/alacritty/blob/master/extra/man/alacritty.5.scd>
# - `man 5 alacritty`
# - defaults: <https://github.com/alacritty/alacritty/releases> -> alacritty.yml
# - irc: #alacritty on libera.chat
{ lib, ... }:
{
sane.programs.alacritty = {
env.TERMINAL = lib.mkDefault "alacritty";
# note: alacritty will switch to .toml config in 13.0 release
# - run `alacritty migrate` to convert the yaml to toml
fs.".config/alacritty/alacritty.yml".symlink.text = ''
font:
size: 14
key_bindings:
- { key: N, mods: Control, action: CreateNewWindow }
- { key: PageUp, mods: Control, action: ScrollPageUp }
- { key: PageDown, mods: Control, action: ScrollPageDown }
- { key: PageUp, mods: Control|Shift, action: ScrollPageUp }
- { key: PageDown, mods: Control|Shift, action: ScrollPageDown }
'';
};
}

View File

@@ -1,277 +0,0 @@
{ config, lib, pkgs, ... }:
let
declPackageSet = pkgs: {
package = null;
suggestedPrograms = pkgs;
};
in
{
sane.programs = {
# PACKAGE SETS
"sane-scripts.backup" = declPackageSet [
"sane-scripts.backup-ls"
"sane-scripts.backup-restore"
];
"sane-scripts.bittorrent" = declPackageSet [
"sane-scripts.bt-add"
"sane-scripts.bt-rm"
"sane-scripts.bt-search"
"sane-scripts.bt-show"
];
"sane-scripts.dev" = declPackageSet [
"sane-scripts.dev-cargo-loop"
"sane-scripts.git-init"
];
"sane-scripts.cli" = declPackageSet [
"sane-scripts.deadlines"
"sane-scripts.find-dotfiles"
"sane-scripts.ip-check"
"sane-scripts.ip-reconnect"
"sane-scripts.private-change-passwd"
"sane-scripts.private-do"
"sane-scripts.private-init"
"sane-scripts.private-lock"
"sane-scripts.private-unlock"
"sane-scripts.rcp"
"sane-scripts.reboot"
"sane-scripts.reclaim-boot-space"
"sane-scripts.reclaim-disk-space"
"sane-scripts.secrets-dump"
"sane-scripts.secrets-unlock"
"sane-scripts.secrets-update-keys"
"sane-scripts.shutdown"
"sane-scripts.ssl-dump"
"sane-scripts.sudo-redirect"
"sane-scripts.sync-from-servo"
"sane-scripts.vpn"
"sane-scripts.which"
"sane-scripts.wipe-browser"
"sane-scripts.wipe-flare"
"sane-scripts.wipe-fractal"
];
"sane-scripts.sys-utils" = declPackageSet [
"sane-scripts.ip-port-forward"
"sane-scripts.sync-music"
];
sysadminUtils = declPackageSet [
"btrfs-progs"
"cacert.unbundled" # some services require unbundled /etc/ssl/certs
"cryptsetup"
"ddrescue"
"dig"
"dtc" # device tree [de]compiler
"e2fsprogs" # resize2fs
"efibootmgr"
"ethtool"
"fatresize"
"fd"
"file"
# "fwupd"
"gawk"
"gdb" # to debug segfaults
"git"
"gptfdisk" # gdisk
"hdparm"
"htop"
"iftop"
"inetutils" # for telnet
"iotop"
"iptables"
"iw"
"jq"
"killall"
"lsof"
"miniupnpc"
"nano"
# "ncdu" # ncurses disk usage. doesn't cross compile (zig)
"neovim"
"netcat"
"nethogs"
"nmap"
"nvme-cli" # nvme
"openssl"
"parted"
"pciutils"
"powertop"
"pstree"
"ripgrep"
"screen"
"smartmontools" # smartctl
"socat"
"strace"
"subversion"
"tcpdump"
"tree"
"usbutils"
"util-linux" # lsblk, lscpu, etc
"wget"
"wirelesstools" # iwlist
];
sysadminExtraUtils = declPackageSet [
"backblaze-b2"
"duplicity"
"sane-scripts.backup"
"sqlite" # to debug sqlite3 databases
];
# TODO: split these into smaller groups.
# - moby doesn't want a lot of these.
# - categories like
# - dev?
# - debugging?
consoleUtils = declPackageSet [
"alsaUtils" # for aplay, speaker-test
"binutils-unwrapped" # for strings; though this brings 80MB of unrelated baggage too
# "cdrtools"
"clinfo"
"dmidecode"
"dtrx" # `unar` alternative, "Do The Right eXtraction"
"efivar"
"eza" # a better 'ls'
# "flashrom"
"git" # needed as a user package, for config.
# "gnupg"
# "gocryptfs"
# "gopass"
# "gopass-jsonapi"
"helix" # text editor
"libsecret" # for managing user keyrings. TODO: what needs this? lift into the consumer
"lm_sensors" # for sensors-detect. TODO: what needs this? lift into the consumer
"lshw"
# "memtester"
"mercurial" # hg
"neovim" # needed as a user package, for swap persistence
# "nettools"
# "networkmanager"
# "nixos-generators"
"nmon"
# "node2nix"
# "oathToolkit" # for oathtool
# "ponymix"
"pulsemixer"
"python3-repl"
# "python3Packages.eyeD3" # music tagging
"ripgrep" # needed as a user package so that its user-level config file can be installed
"rsync"
"sane-scripts.bittorrent"
"sane-scripts.cli"
"snapper"
"sops"
"speedtest-cli"
# "ssh-to-age"
"sudo"
# "tageditor" # music tagging
# "unar"
"unzip"
"wireguard-tools"
"xdg-utils" # for xdg-open
# "yarn"
"zsh"
];
desktopConsoleUtils = declPackageSet [
"gh" # MS GitHub cli
"nix-index"
"nixpkgs-review"
"sane-scripts.dev"
"sequoia"
];
consoleMediaUtils = declPackageSet [
"ffmpeg"
"imagemagick"
"sox"
"yt-dlp"
];
tuiApps = declPackageSet [
"aerc" # email client
"msmtp" # sendmail
"offlineimap" # email mailbox sync
"sfeed" # RSS fetcher
"visidata" # TUI spreadsheet viewer/editor
"w3m" # web browser
];
iphoneUtils = declPackageSet [
"ifuse"
"ipfs"
"libimobiledevice"
"sane-scripts.sync-from-iphone"
];
devPkgs = declPackageSet [
"cargo"
"clang"
"lua"
"nodejs"
"patchelf"
"rustc"
"tree-sitter"
];
# INDIVIDUAL PACKAGE DEFINITIONS
cargo.persist.byStore.plaintext = [ ".cargo" ];
# creds, but also 200 MB of node modules, etc
discord.persist.byStore.private = [ ".config/discord" ];
# `emote` will show a first-run dialog based on what's in this directory.
# mostly, it just keeps a LRU of previously-used emotes to optimize display order.
# TODO: package [smile](https://github.com/mijorus/smile) for probably a better mobile experience.
emote.persist.byStore.plaintext = [ ".local/share/Emote" ];
fluffychat-moby.persist.byStore.plaintext = [ ".local/share/chat.fluffy.fluffychat" ];
font-manager.package = pkgs.font-manager.override {
# build without the "Google Fonts" integration feature, to save closure / avoid webkitgtk_4_0
withWebkit = false;
};
# MS GitHub stores auth token in .config
# TODO: we can populate gh's stuff statically; it even lets us use the same oauth across machines
gh.persist.byStore.private = [ ".config/gh" ];
"gnome.gnome-maps".persist.byStore.plaintext = [ ".cache/shumate" ];
"gnome.gnome-maps".persist.byStore.private = [ ".local/share/maps-places.json" ];
# actual monero blockchain (not wallet/etc; safe to delete, just slow to regenerate)
# XXX: is it really safe to persist this? it doesn't have info that could de-anonymize if captured?
monero-gui.persist.byStore.plaintext = [ ".bitmonero" ];
mumble.persist.byStore.private = [ ".local/share/Mumble" ];
# settings (electron app)
obsidian.persist.byStore.plaintext = [ ".config/obsidian" ];
python3-repl.package = pkgs.python3.withPackages (ps: with ps; [
requests
]);
# creds, media
signal-desktop.persist.byStore.private = [ ".config/Signal" ];
# printer/filament settings
slic3r.persist.byStore.plaintext = [ ".Slic3r" ];
tdesktop.persist.byStore.private = [ ".local/share/TelegramDesktop" ];
tokodon.persist.byStore.private = [ ".cache/KDE/tokodon" ];
whalebird.persist.byStore.private = [ ".config/Whalebird" ];
yarn.persist.byStore.plaintext = [ ".cache/yarn" ];
# zcash coins. safe to delete, just slow to regenerate (10-60 minutes)
zecwallet-lite.persist.byStore.private = [ ".zcash" ];
};
programs.feedbackd = lib.mkIf config.sane.programs.feedbackd.enabled {
enable = true;
};
}

View File

@@ -1,103 +0,0 @@
{ lib, pkgs, ... }:
# 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
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
bemenuArgs = [
"--wrap --scrollbar autohide --fixed-height"
"--center --margin 45"
"--no-spacing"
# XXX: font size doesn't seem to take effect (would prefer larger)
"--fn 'monospace 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)
];
bemenuOpts = lib.concatStringsSep " " bemenuArgs;
in
{
sane.programs.bemenu = {
package = pkgs.bemenu.overrideAttrs (upstream: {
nativeBuildInputs = (upstream.nativeBuildInputs or []) ++ [
pkgs.makeWrapper
];
# can alternatively be specified as CLI flags
postInstall = (upstream.postInstall or "") + ''
wrapProgram $out/bin/bemenu \
--set BEMENU_OPTS "${bemenuOpts}"
wrapProgram $out/bin/bemenu-run \
--set BEMENU_OPTS "${bemenuOpts}"
'';
});
};
}

View File

@@ -1,9 +0,0 @@
{ ... }:
{
sane.programs.brave = {
persist.byStore.cryptClearOnBoot = [
".cache/BraveSoftware"
".config/BraveSoftware"
];
};
}

View File

@@ -1,61 +0,0 @@
# GNOME calls
# - <https://gitlab.gnome.org/GNOME/calls>
# - both a dialer and a call handler.
# - uses callaudiod dbus package.
#
# initial JMP.chat configuration:
# - message @cheogram.com "reset sip account" (this is not destructive, despite the name)
# - the bot will reply with auto-generated username/password plus a SIP server endpoint.
# just copy those into gnome-calls' GUI configurator
# - now gnome-calls can do outbound calls. inbound calls requires more chatting with the help bot
#
# my setup here is still very WIP.
# open questions:
# - can i receive calls even with GUI closed?
# - e.g. activated by callaudiod?
# - looks like `gnome-calls --daemon` does that?
{ config, lib, ... }:
let
cfg = config.sane.programs.calls;
in
{
sane.programs.calls = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
};
};
};
persist.byStore.private = [
# ".cache/folks" # contact avatars?
# ".config/calls"
".local/share/calls" # call "records"
# .local/share/folks # contacts?
];
secrets.".config/calls/sip-account.cfg" = ../../../secrets/common/gnome_calls_sip-account.cfg.bin;
suggestedPrograms = [
"feedbackd" # needs `phone-incoming-call`, in particular
];
services.gnome-calls = {
# TODO: prevent gnome-calls from daemonizing when started manually
description = "gnome-calls daemon to monitor incoming SIP calls";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
# add --verbose for more debugging
ExecStart = "${cfg.package}/bin/gnome-calls --daemon";
Type = "simple";
Restart = "always";
RestartSec = "10s";
};
environment.G_MESSAGES_DEBUG = "all";
};
};
programs.calls = lib.mkIf cfg.enabled {
enable = true;
};
}

View File

@@ -1,36 +0,0 @@
# cantata is a mpd frontend.
# before launching it, run `mopidy` in some tab
# TODO: auto-launch mopidy when cantata launches?
{ ... }:
{
sane.programs.cantata = {
persist.byStore.plaintext = [
".cache/cantata" # album art
".local/share/cantata/library" # library index (?)
];
fs.".config/cantata/cantata.conf".symlink.text = ''
[General]
fetchCovers=true
storeCoversInMpdDir=false
version=2.5.0
[Connection]
allowLocalStreaming=true
applyReplayGain=true
autoUpdate=false
dir=~/Music
host=localhost
partition=
passwd=
port=6600
replayGain=off
streamUrl=
[LibraryPage]
artist\gridZoom=100
artist\searchActive=false
artist\viewMode=detailedtree
'';
suggestedPrograms = [ "mopidy" ];
};
}

View File

@@ -1,46 +0,0 @@
{ pkgs, ... }:
let
chattyNoOauth = pkgs.chatty.override {
# the OAuth feature (presumably used for web-based logins) pulls a full webkitgtk.
# especially when using the gtk3 version of evolution-data-server, it's an ancient webkitgtk_4_1.
# disable OAuth for a faster build & smaller closure
evolution-data-server = pkgs.evolution-data-server.override {
enableOAuth2 = false;
gnome-online-accounts = pkgs.gnome-online-accounts.override {
# disables the upstream "goabackend" feature -- presumably "Gnome Online Accounts Backend"
# frees us from webkit_4_1, in turn.
enableBackend = false;
gvfs = pkgs.gvfs.override {
# saves 20 minutes of build time, for unused feature
samba = null;
};
};
};
};
chatty-latest = pkgs.chatty-latest.override {
evolution-data-server-gtk4 = pkgs.evolution-data-server-gtk4.override {
gnome-online-accounts = pkgs.gnome-online-accounts.override {
# disables the upstream "goabackend" feature -- presumably "Gnome Online Accounts Backend"
# frees us from webkit_4_1, in turn.
enableBackend = false;
gvfs = pkgs.gvfs.override {
# saves 20 minutes of build time and cross issues, for unused feature
samba = null;
};
};
};
};
in
{
sane.programs.chatty = {
# package = chattyNoOauth;
package = chatty-latest;
suggestedPrograms = [ "gnome-keyring" ];
persist.byStore.private = [
".local/share/chatty" # matrix avatars and files
# not just XMPP; without this Chatty will regenerate its device-id every boot.
# .purple/ contains XMPP *and* Matrix auth, logs, avatar cache, and a bit more
".purple"
];
};
}

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash
full=$(cat /sys/class/power_supply/axp20x-battery/charge_full_design)
rate=$(cat /sys/class/power_supply/axp20x-battery/current_now)
perc=$(cat /sys/class/power_supply/axp20x-battery/capacity)
perc_left=$((100 - $perc))
# these icons come from sxmo; they only render in nerdfonts
bat_dis="󱊢"
bat_chg="󱊥"
fmt_minutes() {
# args: <battery symbol> <text if ludicrous estimate> <estimated minutes to full/empty>
if [[ $3 -gt 1440 ]]; then
printf "%s %s" "$1" "$2" # more than 1d
else
hr=$(($3 / 60))
hr_in_min=$(($hr * 60))
min=$(($3 - $hr_in_min))
printf "%s %dh%02dm" "$1" "$hr" "$min"
fi
}
if [[ $rate -lt 0 ]]; then
# discharging
fmt_minutes "$bat_dis" '∞' "$(($full * 60 * $perc / (-100 * $rate)))"
elif [[ $rate -gt 0 ]]; then
# charging
fmt_minutes "$bat_chg" '100%' "$(($full * 60 * $perc_left / (100 * $rate)))"
else
echo "$bat_dis $perc%"
fi

View File

@@ -1,50 +0,0 @@
-- docs: <https://conky.cc/variables>
-- color names are X11 colors: <https://en.wikipedia.org/wiki/X11_color_names#Color_name_chart>
-- - can also use #rrggbb syntax
-- example configs: <https://forum.manjaro.org/t/conky-showcase-2022/97123>
-- example configs: <https://www.reddit.com/r/Conkyporn/>
conky.config = {
out_to_wayland = true,
update_interval = 10,
alignment = 'middle_middle',
own_window_type = 'desktop',
-- own_window_argb_value: opacity of the background (0-255)
own_window_argb_value = 0,
-- own_window_argb_value = 92,
-- own_window_colour = '#beebe5', -- beebe5 matches nixos flake bg color
-- "border" pads the entire conky window
-- this can be used to control the extent of the own_window background
border_inner_margin = 8,
-- optionally, actually draw borders
-- draw_borders = true,
-- shades are drop-shadows, outline is the centered version. both apply to text only
draw_shades = true,
draw_outline = false,
default_shade_color = '#beebe5',
default_outline_color = '#beebe5',
font = 'sans-serif:size=8',
use_xft = true,
default_color = '#ffffff',
color1 = '000000',
color2 = '404040',
}
-- texeci <interval_sec> <cmd>: run the command periodically, _in a separate thread_ so as not to block rendering
conky.text = [[
${color1}${shadecolor 707070}${font sans-serif:size=50:style=Bold}${alignc}${exec date +"%H:%M"}${font}
${color2}${shadecolor a4d7d0}${font sans-serif:size=20}${alignc}${exec date +"%a %d %b"}${font}
${color1}${shadecolor}${font sans-serif:size=22:style=Bold}${alignc}${exec @bat@ }${font}
${color1}${shadecolor}${font sans-serif:size=20:style=Bold}${alignc}${texeci 600 @weather@ }${font}
${color2}${shadecolor a4d7d0}${font sans-serif:size=16}${alignc}⇅ ${downspeedf wlan0}K/s${font}
${font sans-serif:size=16}${alignc}☵ $memperc%  $cpu%${font}
]]

View File

@@ -1,34 +0,0 @@
{ config, pkgs, ... }:
{
sane.programs.conky = {
fs.".config/conky/conky.conf".symlink.target =
let
battery_estimate = pkgs.static-nix-shell.mkBash {
pname = "battery_estimate";
src = ./.;
};
in pkgs.substituteAll {
src = ./conky.conf;
bat = "${battery_estimate}/bin/battery_estimate";
weather = "timeout 20 ${pkgs.sane-weather}/bin/sane-weather";
};
services.conky = {
description = "conky dynamic desktop background";
wantedBy = [ "default.target" ];
# XXX: should be part of graphical-session.target, but whatever mix of greetd/sway
# i'm using means that target's never reached...
# wantedBy = [ "graphical-session.target" ];
# partOf = [ "graphical-session.target" ];
serviceConfig.ExecStart = "${config.sane.programs.conky.package}/bin/conky";
serviceConfig.Type = "simple";
serviceConfig.Restart = "on-failure";
serviceConfig.RestartSec = "10s";
# serviceConfig.Slice = "session.slice";
# don't start conky until after sway
preStart = ''test -n "$SWAYSOCK"'';
};
};
}

View File

@@ -1,11 +0,0 @@
{ ... }:
{
sane.programs.cozy = {
# cozy uses a sqlite db for its config and exposes no CLI options other than --help and --debug
persist.byStore.plaintext = [
".local/share/cozy" # sqlite db (config & index?)
".cache/cozy" # offline cache
];
};
}

View File

@@ -1,79 +0,0 @@
{ pkgs, ... }:
{
imports = [
./aerc.nix
./alacritty.nix
./assorted.nix
./bemenu.nix
./brave.nix
./calls.nix
./cantata.nix
./chatty.nix
./conky
./cozy.nix
./dino.nix
./element-desktop.nix
./epiphany.nix
./evince.nix
./feedbackd.nix
./firefox.nix
./flare-signal.nix
./fontconfig.nix
./fractal.nix
./fwupd.nix
./g4music.nix
./gajim.nix
./geary.nix
./git.nix
./gnome-feeds.nix
./gnome-keyring.nix
./gnome-weather.nix
./gpodder.nix
./gthumb.nix
./helix.nix
./imagemagick.nix
./jellyfin-media-player.nix
./komikku.nix
./koreader
./libreoffice.nix
./lemoa.nix
./mako.nix
./megapixels.nix
./mepo.nix
./mopidy.nix
./mpv.nix
./msmtp.nix
./neovim.nix
./newsflash.nix
./nheko.nix
./nix-index.nix
./ntfy-sh.nix
./obsidian.nix
./offlineimap.nix
./playerctl.nix
./rhythmbox.nix
./ripgrep.nix
./sfeed.nix
./splatmoji.nix
./spotify.nix
./steam.nix
./stepmania.nix
./sublime-music.nix
./swaynotificationcenter.nix
./tangram.nix
./tor-browser-bundle-bin.nix
./tuba.nix
./vlc.nix
./wireshark.nix
./xarchiver.nix
./zeal.nix
./zsh
];
config = {
# XXX: this might not be necessary. try removing this and cacert.unbundled (servo)?
environment.etc."ssl/certs".source = "${pkgs.cacert.unbundled}/etc/ssl/certs/*";
};
}

View File

@@ -1,71 +0,0 @@
# usage:
# - start a DM with a rando via
# - '+' -> 'start conversation'
# - add a user to your roster via
# - '+' -> 'start conversation' -> '+' (opens the "add contact" dialog)
# - this triggers a popup on the remote side asking them for confirmation
# - after the remote's confirmation there will be a local popup for you to allow them to add you to their roster
# - to make a call:
# - ensure the other party is in your roster
# - open a DM with the party
# - click the phone icon at top (only visible if other party is in your roster)
#
# dino can be autostarted on login -- useful to ensure that i always receive calls and notifications --
# but at present it has no "start in tray" type of option: it must render a window.
#
# outstanding bugs:
# - mic is sometimes disabled at call start despite presenting as enabled
# - fix is to toggle it off -> on in the Dino UI
# - default mic gain is WAY TOO MUCH (heavily distorted)
# - TODO: dino should have more optimal niceness/priority to ensure it can process its buffers
# - possibly this is solved by enabling RealtimeKit (rtkit)
# - TODO: see if Dino calls work better with `echo full > /sys/kernel/debug/sched/preempt`
#
# probably fixed:
# - once per 1-2 minutes dino will temporarily drop mic input:
# - `rtp-WRNING: plugin.vala:148: Warning in pipeline: Can't record audio fast enough
# - this was *partially* fixed by bumping the pipewire mic buffer to 2048 samples (from ~512)
# - this was further fixed by setting PULSE_LATENCY_MSEC=20.
# - possibly Dino should be updated internally: `info.rate / 100` -> `info.rate / 50`.
# - i think that affects the batching for echo cancellation, adaptive gain control, etc.
#
{ config, lib, ... }:
let
cfg = config.sane.programs.dino;
in
{
sane.programs.dino = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
};
};
};
persist.byStore.private = [ ".local/share/dino" ];
services.dino = {
description = "auto-start and maintain dino XMPP connection";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/dino";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
# audio buffering; see: <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained>
# dino defaults to 10ms mic buffer, which causes underruns, which Dino handles *very* poorly
# as in, the other end of the call will just not receive sound from us for a couple seconds.
# pipewire uses power-of-two buffering for the mic itself. that would put us at 21.33 ms, but this env var supports only whole numbers (21ms ends up not power-of-two).
# also, Dino's likely still doing things in 10ms batches internally anyway.
environment.PULSE_LATENCY_MSEC = "20";
# note that debug logging during calls produces so much journal spam that it pegs the CPU and causes dropped audio
# environment.G_MESSAGES_DEBUG = "all";
};
};
}

View File

@@ -1,15 +0,0 @@
# debugging tips:
# - if element opens but does not render:
# - `element-desktop --disable-gpu --in-process-gpu`
# - <https://github.com/vector-im/element-desktop/issues/1029#issuecomment-1632688224>
# - `rm -rf ~/.config/Element/GPUCache`
# - <https://github.com/NixOS/nixpkgs/issues/244486>
{ ... }:
{
sane.programs.element-desktop = {
# creds/session keys, etc
persist.byStore.private = [ ".config/Element" ];
suggestedPrograms = [ "gnome-keyring" ];
};
}

View File

@@ -1,48 +0,0 @@
# epiphany web browser
# - GTK4/webkitgtk
#
# usability notes:
# - touch-based scroll works well (for moby)
# - URL bar constantly resets cursor to the start of the line as i type
# - maybe due to the URLbar suggestions getting in the way
{ pkgs, ... }:
{
sane.programs.epiphany = {
# XXX(2023/07/08): running on moby without `WEBKIT_DISABLE_SANDBOX...` fails, with:
# - `bwrap: Can't make symlink at /var/run: File exists`
# this could be due to:
# - epiphany is somewhere following a symlink into /var/run instead of /run
# - (nothing in `env` or in this repo touches /var/run)
# - no xdg-desktop-portal is installed (unlikely)
#
# a few other users have hit this, in different contexts:
# - <https://gitlab.gnome.org/GNOME/gnome-builder/-/issues/1164>
# - <https://github.com/flatpak/flatpak/issues/3477>
# - <https://github.com/NixOS/nixpkgs/issues/197085>
#
# TODO: consider `WEBKIT_USE_SINGLE_WEB_PROCESS=1` for better perf
# - this runs all tabs in 1 process. which is fine, if i'm not a heavy multi-tabber
package = pkgs.epiphany.overrideAttrs (upstream: {
preFixup = ''
gappsWrapperArgs+=(
--set WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS "1"
);
'' + (upstream.preFixup or "");
});
persist.byStore.private = [
".cache/epiphany"
".local/share/epiphany"
# also .config/epiphany, but appears empty
];
mime.priority = 200; # default priority is 100: install epiphany only as a fallback
mime.associations = let
desktop = "org.gnome.Epiphany.desktop";
in {
"text/html" = desktop;
"x-scheme-handler/http" = desktop;
"x-scheme-handler/https" = desktop;
"x-scheme-handler/about" = desktop;
"x-scheme-handler/unknown" = desktop;
};
};
}

View File

@@ -1,4 +0,0 @@
{ ... }:
{
sane.programs.evince.mime.associations."application/pdf" = "org.gnome.Evince.desktop";
}

View File

@@ -1,114 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.feedbackd;
in
{
sane.programs.feedbackd = {
package = pkgs.rmDbusServices pkgs.feedbackd;
configOption = with lib; mkOption {
type = types.submodule {
options.proxied = mkOption {
type = types.bool;
default = false;
description = ''
whether to use a sound theme in which common application events are muted
with the intent that a proxy (notification daemon) with knowledge of this
modification will "speak" on behalf of all applications.
'';
};
};
default = {};
};
# N.B.: feedbackd will load ~/.config/feedbackd/themes/default.json by default
# - but using that would forbid `parent-theme = "default"`
# the default theme ships support for these events:
# - alarm-clock-elapsed
# - battery-caution
# - bell-terminal
# - button-pressed
# - button-released
# - camera-focus
# - camera-shutter
# - message-missed-email
# - message-missed-instant
# - message-missed-notification
# - message-missed-sms
# - message-new-email
# - message-new-instant
# - message-new-sms
# - message-sent-instant
# - phone-failure
# - phone-hangup
# - phone-incoming-call
# - phone-missed-call
# - phone-outgoing-busy
# - screen-capture
# - theme-demo
# - timeout-completed
# - window-close
fs.".config/feedbackd/themes/proxied.json".symlink.text = builtins.toJSON {
name = "proxied";
parent-theme = "default";
profiles = [
{
name = "full";
feedbacks = [
# forcibly disable normal events which we'd prefer for the notification daemon (e.g. swaync) to handle
{
event-name = "message-new-instant";
type = "Dummy";
}
{
event-name = "proxied-message-new-instant";
type = "Sound";
effect = "message-new-instant";
}
# re-define sounds from the default theme which we'd like to pass through w/o proxying.
# i guess this means i'm not inheriting the default theme :|
{
event-name = "phone-incoming-call";
type = "Sound";
effect = "phone-incoming-call";
}
{
event-name = "alarm-clock-elapsed";
type = "Sound";
effect = "alarm-clock-elapsed";
}
{
event-name = "timeout-completed";
type = "Sound";
effect = "complete";
}
];
}
];
};
services.feedbackd = {
description = "feedbackd audio/vibration/led controller";
wantedBy = [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/libexec/feedbackd";
Type = "simple";
Restart = "on-failure";
RestartSec = "10s";
};
environment = {
G_MESSAGES_DEBUG = "all";
} // (lib.optionalAttrs cfg.config.proxied {
FEEDBACK_THEME = "/home/colin/.config/feedbackd/themes/proxied.json";
});
};
};
services.udev.packages = lib.mkIf cfg.enabled [
# ships udev rules for `feedbackd` group to be able to control vibrator and LEDs
cfg.package
];
users.groups = lib.mkIf cfg.enabled {
feedbackd = {};
};
}

View File

@@ -1,268 +0,0 @@
# common settings to toggle (at runtime, in about:config):
# > security.ssl.require_safe_negotiation
# librewolf is a forked firefox which patches firefox to allow more things
# (like default search engines) to be configurable at runtime.
# many of the settings below won't have effect without those patches.
# see: https://gitlab.com/librewolf-community/settings/-/blob/master/distribution/policies.json
{ config, lib, pkgs, ...}:
with lib;
let
cfg = config.sane.programs.firefox.config;
mobile-prefs = lib.optionals false pkgs.librewolf-pmos-mobile.extraPrefsFiles;
# allow easy switching between firefox and librewolf with `defaultSettings`, below
librewolfSettings = {
browser = pkgs.librewolf-unwrapped;
extraPrefsFiles = pkgs.librewolf-unwrapped.extraPrefsFiles ++ mobile-prefs;
libName = "librewolf";
dotDir = ".librewolf";
cacheDir = ".cache/librewolf";
desktop = "librewolf.desktop";
};
firefoxSettings = {
browser = pkgs.firefox-esr-unwrapped;
extraPrefsFiles = mobile-prefs;
libName = "firefox";
dotDir = ".mozilla/firefox";
cacheDir = ".cache/mozilla";
desktop = "firefox.desktop";
};
# defaultSettings = firefoxSettings;
defaultSettings = librewolfSettings;
addon = name: extid: hash: pkgs.fetchFirefoxAddon {
inherit name hash;
url = "https://addons.mozilla.org/firefox/downloads/latest/${name}/latest.xpi";
# extid can be found by unar'ing the above xpi, and copying browser_specific_settings.gecko.id field
fixedExtid = extid;
};
localAddon = pkg: pkgs.fetchFirefoxAddon {
inherit (pkg) name;
src = "${pkg}/share/mozilla/extensions/\\{ec8030f7-c20a-464f-9b0e-13a3a9e97384\\}/${pkg.extid}.xpi";
fixedExtid = pkg.extid;
};
package = pkgs.wrapFirefox cfg.browser.browser {
# inherit the default librewolf.cfg
# it can be further customized via ~/.librewolf/librewolf.overrides.cfg
inherit (cfg.browser) extraPrefsFiles libName;
extraNativeMessagingHosts = optional cfg.addons.browserpass-extension.enable pkgs.browserpass;
# extraNativeMessagingHosts = [ pkgs.gopass-native-messaging-host ];
nixExtensions = concatMap (ext: optional ext.enable ext.package) (attrValues cfg.addons);
extraPolicies = {
FirefoxHome = {
Search = true;
Pocket = false;
Snippets = false;
TopSites = false;
Highlights = false;
};
NoDefaultBookmarks = true;
OfferToSaveLogins = false;
OfferToSaveLoginsDefault = false;
PasswordManagerEnabled = false;
SearchEngines = {
Default = "DuckDuckGo";
};
UserMessaging = {
ExtensionRecommendations = false;
FeatureRecommendations = false;
SkipOnboarding = true;
UrlbarInterventions = false;
WhatsNew = false;
};
# these were taken from Librewolf
AppUpdateURL = "https://localhost";
DisableAppUpdate = true;
OverrideFirstRunPage = "";
OverridePostUpdatePage = "";
DisableSystemAddonUpdate = true;
DisableFirefoxStudies = true;
DisableTelemetry = true;
DisableFeedbackCommands = true;
DisablePocket = true;
DisableSetDesktopBackground = false;
# remove many default search providers
# XXX this seems to prevent the `nixExtensions` from taking effect
# Extensions.Uninstall = [
# "google@search.mozilla.org"
# "bing@search.mozilla.org"
# "amazondotcom@search.mozilla.org"
# "ebay@search.mozilla.org"
# "twitter@search.mozilla.org"
# ];
# XXX doesn't seem to have any effect...
# docs: https://github.com/mozilla/policy-templates#homepage
# Homepage = {
# HomepageURL = "https://uninsane.org/";
# StartPage = "homepage";
# };
# NewTabPage = true;
};
# extraPrefs = ...
};
addonOpts = types.submodule {
options = {
package = mkOption {
type = types.package;
};
enable = mkOption {
type = types.bool;
};
};
};
configOpts = {
options = {
browser = mkOption {
default = defaultSettings;
type = types.anything;
};
persistData = mkOption {
description = "optional store name to which persist browsing data (like history)";
type = types.nullOr types.str;
default = null;
};
persistCache = mkOption {
description = "optional store name to which persist browser cache";
type = types.nullOr types.str;
default = "cryptClearOnBoot";
};
addons = mkOption {
type = types.attrsOf addonOpts;
default = {};
};
};
};
in
{
config = mkMerge [
({
sane.programs.firefox.configOption = mkOption {
type = types.submodule configOpts;
default = {};
};
sane.programs.firefox.config.addons = {
browserpass-extension = {
package = pkgs.firefox-extensions.browserpass-extension;
enable = lib.mkDefault true;
};
bypass-paywalls-clean = {
package = pkgs.firefox-extensions.bypass-paywalls-clean;
enable = lib.mkDefault true;
};
ether-metamask = {
package = pkgs.firefox-extensions.ether-metamask;
enable = lib.mkDefault false; # until i can disable the first-run notification
};
i2p-in-private-browsing = {
package = pkgs.firefox-extensions.i2p-in-private-browsing;
enable = lib.mkDefault config.services.i2p.enable;
};
sidebery = {
package = pkgs.firefox-extensions.sidebery;
enable = lib.mkDefault true;
};
sponsorblock = {
package = pkgs.firefox-extensions.sponsorblock;
enable = lib.mkDefault true;
};
ublacklist = {
package = pkgs.firefox-extensions.ublacklist;
enable = lib.mkDefault true;
};
ublock-origin = {
package = pkgs.firefox-extensions.ublock-origin;
enable = lib.mkDefault true;
};
};
})
({
sane.programs.firefox = {
inherit package;
mime.associations = let
inherit (cfg.browser) desktop;
in {
"text/html" = desktop;
"x-scheme-handler/http" = desktop;
"x-scheme-handler/https" = desktop;
"x-scheme-handler/about" = desktop;
"x-scheme-handler/unknown" = desktop;
};
# env.BROWSER = "${package}/bin/${cfg.browser.libName}";
env.BROWSER = cfg.browser.libName; # used by misc tools like xdg-email, as fallback
# uBlock filter list configuration.
# specifically, enable the GDPR cookie prompt blocker.
# data.toOverwrite.filterLists is additive (i.e. it supplements the default filters)
# this configuration method is documented here:
# - <https://github.com/gorhill/uBlock/issues/2986#issuecomment-364035002>
# the specific attribute path is found via scraping ublock code here:
# - <https://github.com/gorhill/uBlock/blob/master/src/js/storage.js>
# - <https://github.com/gorhill/uBlock/blob/master/assets/assets.json>
fs."${cfg.browser.dotDir}/managed-storage/uBlock0@raymondhill.net.json".symlink.text = ''
{
"name": "uBlock0@raymondhill.net",
"description": "ignored",
"type": "storage",
"data": {
"toOverwrite": "{\"filterLists\": [\"fanboy-cookiemonster\"]}"
}
}
'';
# TODO: this is better suited in `extraPrefs` during `wrapFirefox` call
fs."${cfg.browser.dotDir}/${cfg.browser.libName}.overrides.cfg".symlink.text = ''
// if we can't query the revocation status of a SSL cert because the issuer is offline,
// treat it as unrevoked.
// see: <https://librewolf.net/docs/faq/#im-getting-sec_error_ocsp_server_error-what-can-i-do>
defaultPref("security.OCSP.require", false);
// scrollbar configuration, see: <https://artemis.sh/2023/10/12/scrollbars.html>
// style=4 gives rectangular scrollbars
// could also enable "always show scrollbars" in about:preferences -- not sure what the actual pref name for that is
defaultPref("widget.non-native-theme.scrollbar.size.override", 50);
defaultPref("widget.non-native-theme.scrollbar.style", 4);
'';
fs."${cfg.browser.dotDir}/default".dir = {};
# instruct Firefox to put the profile in a predictable directory (so we can do things like persist just it).
# XXX: the directory *must* exist, even if empty; Firefox will not create the directory itself.
fs."${cfg.browser.dotDir}/profiles.ini".symlink.text = ''
[Profile0]
Name=default
IsRelative=1
Path=default
Default=1
[General]
StartWithLastProfile=1
'';
};
})
(mkIf config.sane.programs.firefox.enabled {
# TODO: move the persistence into the sane.programs API (above)
# flush the cache to disk to avoid it taking up too much tmp.
sane.user.persist.byPath."${cfg.browser.cacheDir}".store =
if (cfg.persistData != null) then
cfg.persistData
else
"cryptClearOnBoot"
;
sane.user.persist.byPath."${cfg.browser.dotDir}/default".store =
if (cfg.persistData != null) then
cfg.persistData
else
"cryptClearOnBoot"
;
})
];
}

View File

@@ -1,47 +0,0 @@
# Flare is a 3rd-party GTK4 Signal app.
# UI is effectively a clone of Fractal.
#
# compatibility:
# - desko: works fine. pairs, and exchanges contact list (but not message history) with the paired device. exchanges future messages fine.
# - moby (cross compiled flare-signal-nixified): nope. it pairs, but can only *receive* messages and never *send* them.
# - even `rsync`ing the data and keyrings from desko -> moby, still fails in that same manner.
# - console shows error messages. quite possibly an endianness mismatch somewhere
# - moby (partially-emulated flare-signal): works! pairs and can send/receive messages, same as desko.
#
# error signatures (to reset, run `sane-wipe-fractal`):
# - upon sending a message, the other side receives it, but Signal desktop gets "A message from Colin could not be delivered" and the local CLI shows:
# ```
# ERROR libsignal_service::websocket] SignalWebSocket: Websocket error: SignalWebSocket: end of application request stream; socket closing
# ERROR presage::manager] Error opening envelope: ProtobufDecodeError(DecodeError { description: "invalid tag value: 0", stack: [("Content", "data_message")] }), message will be skipped!
# ERROR presage::manager] Error opening envelope: ProtobufDecodeError(DecodeError { description: "invalid tag value: 0", stack: [("Content", "data_message")] }), message will be skipped!
# ```
# - this occurs on moby, desko, `flare-signal` and `flare-signal-nixified`
# - the Websocket error seems to be unrelated, occurs during normal/good operation
# - related issues: <https://github.com/whisperfish/presage/issues/152>
# error when sending from Flare to other Flare device:
# - ```
# ERROR libsignal_protocol::session_cipher] Message from <UUID>.3 failed to decrypt; sender ratchet public key <key> message counter 1
# No current session
# ERROR presage::manager] Error opening envelope: SignalProtocolError(InvalidKyberPreKeyId), message will be skipped!
# ```
# - but signal iOS will still read it.
#
# well, seems to have unpredictable errors particularly when being used on multiple devices.
# desktop _seems_ more reliable than on mobile, but not confident.
{ pkgs, ... }:
{
sane.programs.flare-signal = {
package = pkgs.flare-signal-nixified;
# package = pkgs.flare-signal;
persist.byStore.private = [
# everything: conf, state, files, all opaque
".local/share/flare"
# also persists a secret in ~/.local/share/keyrings. reset with:
# - `secret-tool search --all --unlock 'xdg:schema' 'de.schmidhuberj.Flare'`
# - `secret-tool clear 'xdg:schema' 'de.schmidhuberj.Flare'`
# and it persists some dconf settings (e.g. device name). reset with:
# - `dconf reset -f /de/schmidhuberj/Flare/`.
];
};
}

View File

@@ -1,68 +0,0 @@
# to preview fonts:
# - `font-manager` (gui)
# - useful to determine official name; codepoint support
{ config, lib, pkgs, ... }:
let
# nerdfonts takes popular open fonts and patches them to support a wider range of glyphs, notably emoji.
# any nerdfonts font includes icons such as these:
# - 󱊥 (battery charging)
# - 󰃝 (brightness)
# -  (gps / crosshairs)
# - 󰎈 (music note)
# - 󰍦 (message bubble)
# - 󰏲 (phone)
# -  (weather/sun-behind-clouds)
# used particularly by sxmo utilities, but also a few of my own (e.g. conky)
#
# nerdfonts is very heavy. each font is 20-900 MiB (2 MiB per "variation")
# lots of redundant data inside there, but no deduplication except whatever nix or the fs does implicitly.
wantedNerdfonts = [
# used explicitly by SXMO
# "DejaVuSansMono" # 25 MiB
# good terminal/coding font. grab via nerdfonts for more emoji/unicode support
"Hack" # 26 MiB
"Noto" # 861 MiB
];
nerdfontPkgs = builtins.map
(f: pkgs.nerdfonts.override { fonts = [ f ]; })
wantedNerdfonts;
in
{
fonts = lib.mkIf config.sane.programs.fontconfig.enabled {
fontconfig.enable = true;
fontconfig.defaultFonts = {
emoji = [
"Noto Color Emoji"
"Font Awesome 6 Free"
"Font Awesome 6 Brands"
];
monospace = [
"Hack Nerd Font Propo"
# "DejaVuSansM Nerd Font Propo"
"NotoMono Nerd Font Propo"
];
serif = [
"NotoSerif Nerd Font"
"DejaVu Serif"
];
sansSerif = [
"NotoSans Nerd Font"
"DejaVu Sans"
];
};
#vvv enables dejavu_fonts, freefont_ttf, gyre-fonts, liberation_ttf, unifont, noto-fonts-emoji
enableDefaultPackages = false;
packages = with pkgs; [
# TODO: reduce this font set.
# - probably need only one of dejavu/freefont/liberation
dejavu_fonts # 10 MiB; DejaVu {Sans,Serif,Sans Mono,Math TeX Gyre}; also available as a NerdFonts (Sans Mono only)
font-awesome # 2 MiB; Font Awesome 6 {Free,Brands}
freefont_ttf # 11 MiB; Free{Mono,Sans,Serif}
gyre-fonts # 4 MiB; Tex Gyre *; ttf substitutes for standard PostScript fonts
# hack-font # 1 MiB; Hack; also available as a NerdFonts
liberation_ttf # 4 MiB; Liberation {Mono,Sans,Serif}; also available as a NerdFonts
noto-fonts-color-emoji # 10 Mib; Noto Color Emoji
unifont # 16 MiB; Unifont; provides LOTS of unicode coverage
] ++ nerdfontPkgs;
};
}

View File

@@ -1,60 +0,0 @@
# Fractal: GTK4 instant messenger client for the Matrix protocol
#
# very susceptible to state corruption during hard power-cycles.
# if it stalls while launching, especially with a brief message at bottom
# "unable to open store"
# then:
# - remove ~/.local/share/stable/*
# - this might give I/O error, in which case remove the corresponding path under
# /nix/persist/home/colin/private (which can be found by correlating timestamps/sizes with that in ~/private/.local/share/stable).
# - reboot (maybe necessary).
# - now you can send messages, and read messages in unencrypted rooms, but not read messages from encrypted rooms.
# to fix encrypted message receipt:
# - start from above (fractal closed, no ~/.local/share/stable/*)
# - in ~/.local/share/keyrings/Default_keyring.keyring:
# - find the entry that says "display-name=Fractal: Matrix credentials for <mxid>"
# - remove that entry and all associated entries (i.e. ones with same number but different :attributeN)
# - REBOOT. otherwise keyring stuff seems to stay cached in RAM
# - login to Fractal. give an hour to sync.
# - it'll kick you back to a page asking you to cross-sign. open FluffyChat and do the emoji compare. success!
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.fractal;
in
{
sane.programs.fractal = {
package = pkgs.fractal-nixified;
# package = pkgs.fractal-latest;
# package = pkgs.fractal-next;
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = true;
};
};
};
persist.byStore.private = [
# XXX by default fractal stores its state in ~/.local/share/<build-profile>/<UUID>.
".local/share/hack" # for debug-like builds
".local/share/stable" # for normal releases
];
suggestedPrograms = [ "gnome-keyring" ];
services.fractal = {
description = "auto-start and maintain fractal Matrix connection";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/fractal";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
# environment.G_MESSAGES_DEBUG = "all";
};
};
}

View File

@@ -1,7 +0,0 @@
{ config, lib, ... }:
{
services.fwupd = lib.mkIf config.sane.programs.fwupd.enabled {
# enables the dbus service, which i think the frontend speaks to.
enable = true;
};
}

View File

@@ -1,16 +0,0 @@
# N.B.: requires first-run setup on moby:
# - UI will render transparent
# - click the hamburger (top-right: immediately left from close button)
# > Preferences
# > Background-blur mode: change from "Always" to "Never"
#
# the background blur is probably some dconf setting somewhere.
{ ... }:
{
sane.programs.g4music = {
persist.byStore.plaintext = [
# index?
".cache/com.github.neithern.g4music"
];
};
}

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