From 0c4f3acb281be6290c55a6e96bc29fab5b5c7a11 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 11 Sep 2023 18:24:28 +0200 Subject: stockholm -> hrm --- modules/Xresources.nix | 44 ++ modules/charybdis/config.nix | 519 ++++++++++++++++++++++++ modules/charybdis/default.nix | 80 ++++ modules/default.nix | 7 + modules/dnsmasq.nix | 60 +++ modules/ejabberd/default.nix | 274 +++++++++++++ modules/focus.nix | 3 + modules/hosts.nix | 8 + modules/hw.nix | 15 + modules/im.nix | 109 +++++ modules/iptables.nix | 207 ++++++++++ modules/lidControl.nix | 44 ++ modules/org.freedesktop.machine1.host-shell.nix | 28 ++ modules/slock.nix | 77 ++++ modules/systemd.nix | 49 +++ modules/unbound.nix | 84 ++++ modules/wwan.nix | 181 +++++++++ modules/x0vncserver.nix | 44 ++ 18 files changed, 1833 insertions(+) create mode 100644 modules/Xresources.nix create mode 100644 modules/charybdis/config.nix create mode 100644 modules/charybdis/default.nix create mode 100644 modules/default.nix create mode 100644 modules/dnsmasq.nix create mode 100644 modules/ejabberd/default.nix create mode 100644 modules/focus.nix create mode 100644 modules/hosts.nix create mode 100644 modules/hw.nix create mode 100644 modules/im.nix create mode 100644 modules/iptables.nix create mode 100644 modules/lidControl.nix create mode 100644 modules/org.freedesktop.machine1.host-shell.nix create mode 100644 modules/slock.nix create mode 100644 modules/systemd.nix create mode 100644 modules/unbound.nix create mode 100644 modules/wwan.nix create mode 100644 modules/x0vncserver.nix (limited to 'modules') diff --git a/modules/Xresources.nix b/modules/Xresources.nix new file mode 100644 index 0000000..3f9bc11 --- /dev/null +++ b/modules/Xresources.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, ... }: let + cfg = { + enable = config.services.xserver.enable && config.tv.Xresources != {}; + user = config.krebs.build.user; + }; + local.types.Xresources = lib.types.attrsOf lib.types.str; + + mapAttrNames = f: lib.mapAttrs' (name: lib.nameValuePair (f name)); + mapAttrValues = f: lib.mapAttrs (_: f); +in { + options.tv.Xresources = lib.mkOption { + default = {}; + type = lib.types.attrsOf lib.types.str; + }; + config = { + nixpkgs.overlays = lib.singleton (self: super: { + tv = super.tv or {} // { + Xresources = + self.writeText "Xresources" + (lib.concatStrings + (lib.mapAttrsToList + (name: value: /* xdefaults */ '' + ${name}: ${value} + '') + config.tv.Xresources)); + }; + }); + systemd.services.${if cfg.enable then "Xresources" else null} = { + wantedBy = [ "graphical.target" ]; + after = [ "xmonad.service" ]; + environment = { + DISPLAY = ":${toString config.services.xserver.display}"; + }; + serviceConfig = { + ExecStart = "${pkgs.xorg.xrdb}/bin/xrdb ${pkgs.tv.Xresources}"; + RemainAfterExit = true; + SyslogIdentifier = "Xresources"; + Type = "oneshot"; + User = cfg.user.name; + WorkingDirectory = cfg.user.home; + }; + }; + }; +} diff --git a/modules/charybdis/config.nix b/modules/charybdis/config.nix new file mode 100644 index 0000000..a157ac2 --- /dev/null +++ b/modules/charybdis/config.nix @@ -0,0 +1,519 @@ +{ config, ... }: let + cfg = config.tv.charybdis; +in builtins.toFile "charybdis.conf" '' + /* doc/example.conf - brief example configuration file + * + * Copyright (C) 2000-2002 Hybrid Development Team + * Copyright (C) 2002-2005 ircd-ratbox development team + * Copyright (C) 2005-2006 charybdis development team + * + * $Id: example.conf 3582 2007-11-17 21:55:48Z jilles $ + * + * See reference.conf for more information. + */ + + /* Extensions */ + #loadmodule "extensions/chm_operonly_compat.so"; + #loadmodule "extensions/chm_quietunreg_compat.so"; + #loadmodule "extensions/chm_sslonly_compat.so"; + #loadmodule "extensions/createauthonly.so"; + #loadmodule "extensions/extb_account.so"; + #loadmodule "extensions/extb_canjoin.so"; + #loadmodule "extensions/extb_channel.so"; + #loadmodule "extensions/extb_extgecos.so"; + #loadmodule "extensions/extb_oper.so"; + #loadmodule "extensions/extb_realname.so"; + #loadmodule "extensions/extb_server.so"; + #loadmodule "extensions/extb_ssl.so"; + #loadmodule "extensions/hurt.so"; + #loadmodule "extensions/m_findforwards.so"; + #loadmodule "extensions/m_identify.so"; + #loadmodule "extensions/no_oper_invis.so"; + #loadmodule "extensions/sno_farconnect.so"; + #loadmodule "extensions/sno_globalkline.so"; + #loadmodule "extensions/sno_globaloper.so"; + #loadmodule "extensions/sno_whois.so"; + loadmodule "extensions/override.so"; + + /* + * IP cloaking extensions: use ip_cloaking_4.0 + * if you're linking 3.2 and later, otherwise use + * ip_cloaking.so, for compatibility with older 3.x + * releases. + */ + + #loadmodule "extensions/ip_cloaking_4.0.so"; + #loadmodule "extensions/ip_cloaking.so"; + + serverinfo { + name = ${builtins.toJSON (builtins.head config.krebs.build.host.nets.retiolum.aliases)}; + sid = "4z3"; + description = "miep!"; + network_name = "irc.r"; + #network_desc = "Retiolum IRC Network"; + hub = yes; + + /* On multi-homed hosts you may need the following. These define + * the addresses we connect from to other servers. */ + /* for IPv4 */ + vhost = ${builtins.toJSON config.krebs.build.host.nets.retiolum.ip4.addr}; + /* for IPv6 */ + vhost6 = ${builtins.toJSON config.krebs.build.host.nets.retiolum.ip6.addr}; + + /* ssl_private_key: our ssl private key */ + ssl_private_key = "/tmp/credentials/ssl_private_key"; + + /* ssl_cert: certificate for our ssl server */ + ssl_cert = ${builtins.toJSON cfg.ssl_cert}; + + /* ssl_dh_params: DH parameters, generate with openssl dhparam -out dh.pem 1024 */ + ssl_dh_params = "/tmp/credentials/ssl_dh_params"; + + /* ssld_count: number of ssld processes you want to start, if you + * have a really busy server, using N-1 where N is the number of + * cpu/cpu cores you have might be useful. A number greater than one + * can also be useful in case of bugs in ssld and because ssld needs + * two file descriptors per SSL connection. + */ + ssld_count = 1; + + /* default max clients: the default maximum number of clients + * allowed to connect. This can be changed once ircd has started by + * issuing: + * /quote set maxclients + */ + default_max_clients = 1024; + + /* nicklen: enforced nickname length (for this server only; must not + * be longer than the maximum length set while building). + */ + nicklen = 30; + }; + + admin { + name = "tv"; + description = "peer"; + }; + + log { + fname_userlog = "/dev/stderr"; + fname_fuserlog = "/dev/stderr"; + fname_operlog = "/dev/stderr"; + fname_foperlog = "/dev/stderr"; + fname_serverlog = "/dev/stderr"; + fname_klinelog = "/dev/stderr"; + fname_killlog = "/dev/stderr"; + fname_operspylog = "/dev/stderr"; + fname_ioerrorlog = "/dev/stderr"; + }; + + /* class {} blocks MUST be specified before anything that uses them. That + * means they must be defined before auth {} and before connect {}. + */ + + class "krebs" { + ping_time = 2 minutes; + number_per_ident = 10; + number_per_ip = 2048; + number_per_ip_global = 4096; + cidr_ipv4_bitlen = 24; + cidr_ipv6_bitlen = 64; + number_per_cidr = 65536; + max_number = 3000; + sendq = 1 megabyte; + }; + + class "users" { + ping_time = 2 minutes; + number_per_ident = 10; + number_per_ip = 1024; + number_per_ip_global = 4096; + cidr_ipv4_bitlen = 24; + cidr_ipv6_bitlen = 64; + number_per_cidr = 65536; + max_number = 3000; + sendq = 400 kbytes; + }; + + class "opers" { + ping_time = 5 minutes; + number_per_ip = 10; + max_number = 1000; + sendq = 1 megabyte; + }; + + class "server" { + ping_time = 5 minutes; + connectfreq = 5 minutes; + max_number = 1; + sendq = 4 megabytes; + }; + + listen { + /* defer_accept: wait for clients to send IRC handshake data before + * accepting them. if you intend to use software which depends on the + * server replying first, such as BOPM, you should disable this feature. + * otherwise, you probably want to leave it on. + */ + defer_accept = yes; + + /* If you want to listen on a specific IP only, specify host. + * host definitions apply only to the following port line. + */ + #host = ${builtins.toJSON config.krebs.build.host.nets.retiolum.ip4.addr}; + port = ${toString cfg.port}; + sslport = ${toString cfg.sslport}; + }; + + /* auth {}: allow users to connect to the ircd (OLD I:) + * auth {} blocks MUST be specified in order of precedence. The first one + * that matches a user will be used. So place spoofs first, then specials, + * then general access, then restricted. + */ + auth { + /* user: the user@host allowed to connect. Multiple IPv4/IPv6 user + * lines are permitted per auth block. This is matched against the + * hostname and IP address (using :: shortening for IPv6 and + * prepending a 0 if it starts with a colon) and can also use CIDR + * masks. + */ + user = "*@10.243.0.0/16"; + user = "*@42::/16"; + + /* password: an optional password that is required to use this block. + * By default this is not encrypted, specify the flag "encrypted" in + * flags = ...; below if it is. + */ + #password = "letmein"; + + /* spoof: fake the users user@host to be be this. You may either + * specify a host or a user@host to spoof to. This is free-form, + * just do everyone a favour and dont abuse it. (OLD I: = flag) + */ + #spoof = "I.still.hate.packets"; + + /* Possible flags in auth: + * + * encrypted | password is encrypted with mkpasswd + * spoof_notice | give a notice when spoofing hosts + * exceed_limit (old > flag) | allow user to exceed class user limits + * kline_exempt (old ^ flag) | exempt this user from k/g/xlines&dnsbls + * dnsbl_exempt | exempt this user from dnsbls + * spambot_exempt | exempt this user from spambot checks + * shide_exempt | exempt this user from serverhiding + * jupe_exempt | exempt this user from generating + * warnings joining juped channels + * resv_exempt | exempt this user from resvs + * flood_exempt | exempt this user from flood limits + * USE WITH CAUTION. + * no_tilde (old - flag) | don't prefix ~ to username if no ident + * need_ident (old + flag) | require ident for user in this class + * need_ssl | require SSL/TLS for user in this class + * need_sasl | require SASL id for user in this class + */ + flags = kline_exempt, exceed_limit, flood_exempt; + + /* class: the class the user is placed in */ + class = "krebs"; + }; + + auth { + user = "*@*"; + class = "users"; + }; + + /* privset {} blocks MUST be specified before anything that uses them. That + * means they must be defined before operator {}. + */ + privset "local_op" { + privs = oper:local_kill, oper:operwall; + }; + + privset "server_bot" { + extends = "local_op"; + privs = oper:kline, oper:remoteban, snomask:nick_changes; + }; + + privset "global_op" { + extends = "local_op"; + privs = oper:global_kill, oper:routing, oper:kline, oper:unkline, oper:xline, + oper:resv, oper:mass_notice, oper:remoteban; + }; + + privset "admin" { + extends = "global_op"; + privs = oper:admin, oper:die, oper:rehash, oper:spy, oper:override; + }; + + privset "aids" { + privs = oper:override, oper:rehash; + }; + + operator "aids" { + user = "*@10.243.*"; + privset = "aids"; + flags = ~encrypted; + password = "balls"; + }; + + operator "god" { + /* name: the name of the oper must go above */ + + /* user: the user@host required for this operator. CIDR *is* + * supported now. auth{} spoofs work here, other spoofs do not. + * multiple user="" lines are supported. + */ + user = "*god@127.0.0.1"; + + /* password: the password required to oper. Unless ~encrypted is + * contained in flags = ...; this will need to be encrypted using + * mkpasswd, MD5 is supported + */ + password = "5"; + + /* rsa key: the public key for this oper when using Challenge. + * A password should not be defined when this is used, see + * doc/challenge.txt for more information. + */ + #rsa_public_key_file = "/usr/local/ircd/etc/oper.pub"; + + /* umodes: the specific umodes this oper gets when they oper. + * If this is specified an oper will not be given oper_umodes + * These are described above oper_only_umodes in general {}; + */ + #umodes = locops, servnotice, operwall, wallop; + + /* fingerprint: if specified, the oper's client certificate + * fingerprint will be checked against the specified fingerprint + * below. + */ + #fingerprint = "c77106576abf7f9f90cca0f63874a60f2e40a64b"; + + /* snomask: specific server notice mask on oper up. + * If this is specified an oper will not be given oper_snomask. + */ + snomask = "+Zbfkrsuy"; + + /* flags: misc options for the operator. You may prefix an option + * with ~ to disable it, e.g. ~encrypted. + * + * Default flags are encrypted. + * + * Available options: + * + * encrypted: the password above is encrypted [DEFAULT] + * need_ssl: must be using SSL/TLS to oper up + */ + flags = encrypted; + + /* privset: privileges set to grant */ + privset = "admin"; + }; + + service { + name = "services.int"; + }; + + cluster { + name = "*"; + flags = kline, tkline, unkline, xline, txline, unxline, resv, tresv, unresv; + }; + + shared { + oper = "*@*", "*"; + flags = all, rehash; + }; + + /* exempt {}: IPs that are exempt from Dlines and rejectcache. (OLD d:) */ + exempt { + ip = "127.0.0.1"; + }; + + channel { + use_invex = yes; + use_except = yes; + use_forward = yes; + use_knock = yes; + knock_delay = 5 minutes; + knock_delay_channel = 1 minute; + max_chans_per_user = 15; + max_bans = 100; + max_bans_large = 500; + default_split_user_count = 0; + default_split_server_count = 0; + no_create_on_split = no; + no_join_on_split = no; + burst_topicwho = yes; + kick_on_split_riding = no; + only_ascii_channels = no; + resv_forcepart = yes; + channel_target_change = yes; + disable_local_channels = no; + }; + + serverhide { + flatten_links = yes; + links_delay = 5 minutes; + hidden = no; + disable_hidden = no; + }; + + /* These are the blacklist settings. + * You can have multiple combinations of host and rejection reasons. + * They are used in pairs of one host/rejection reason. + * + * These settings should be adequate for most networks, and are (presently) + * required for use on StaticBox. + * + * Word to the wise: Do not use blacklists like SPEWS for blocking IRC + * connections. + * + * As of charybdis 2.2, you can do some keyword substitution on the rejection + * reason. The available keyword substitutions are: + * + * ''${ip} - the user's IP + * ''${host} - the user's canonical hostname + * ''${dnsbl-host} - the dnsbl hostname the lookup was done against + * ''${nick} - the user's nickname + * ''${network-name} - the name of the network + * + * As of charybdis 3.4, a type parameter is supported, which specifies the + * address families the blacklist supports. IPv4 and IPv6 are supported. + * IPv4 is currently the default as few blacklists support IPv6 operation + * as of this writing. + * + * Note: AHBL (the providers of the below *.ahbl.org BLs) request that they be + * contacted, via email, at admins@2mbit.com before using these BLs. + * See for more information. + */ + blacklist { + host = "rbl.efnetrbl.org"; + type = ipv4; + reject_reason = "''${nick}, your IP (''${ip}) is listed in EFnet's RBL. For assistance, see http://efnetrbl.org/?i=''${ip}"; + + # host = "ircbl.ahbl.org"; + # type = ipv4; + # reject_reason = "''${nick}, your IP (''${ip}) is listed in ''${dnsbl-host} for having an open proxy. In order to protect ''${network-name} from abuse, we are not allowing connections with open proxies to connect."; + # + # host = "tor.ahbl.org"; + # type = ipv4; + # reject_reason = "''${nick}, your IP (''${ip}) is listed as a TOR exit node. In order to protect ''${network-name} from tor-based abuse, we are not allowing TOR exit nodes to connect to our network."; + # + /* Example of a blacklist that supports both IPv4 and IPv6 */ + # host = "foobl.blacklist.invalid"; + # type = ipv4, ipv6; + # reject_reason = "''${nick}, your IP (''${ip}) is listed in ''${dnsbl-host} for some reason. In order to protect ''${network-name} from abuse, we are not allowing connections listed in ''${dnsbl-host} to connect"; + }; + + alias "NickServ" { + target = "NickServ"; + }; + + alias "ChanServ" { + target = "ChanServ"; + }; + + alias "OperServ" { + target = "OperServ"; + }; + + alias "MemoServ" { + target = "MemoServ"; + }; + + alias "NS" { + target = "NickServ"; + }; + + alias "CS" { + target = "ChanServ"; + }; + + alias "OS" { + target = "OperServ"; + }; + + alias "MS" { + target = "MemoServ"; + }; + + general { + hide_error_messages = opers; + hide_spoof_ips = yes; + + /* + * default_umodes: umodes to enable on connect. + * If you have enabled the new ip_cloaking_4.0 module, and you want + * to make use of it, add +x to this option, i.e.: + * default_umodes = "+ix"; + * + * If you have enabled the old ip_cloaking module, and you want + * to make use of it, add +h to this option, i.e.: + * default_umodes = "+ih"; + */ + default_umodes = "+i"; + + default_operstring = "is an IRC Operator"; + default_adminstring = "is a Server Administrator"; + servicestring = "is a Network Service"; + disable_fake_channels = no; + tkline_expire_notices = no; + default_floodcount = 1000; + failed_oper_notice = yes; + dots_in_ident=2; + min_nonwildcard = 4; + min_nonwildcard_simple = 3; + max_accept = 100; + max_monitor = 100; + anti_nick_flood = yes; + max_nick_time = 20 seconds; + max_nick_changes = 5; + anti_spam_exit_message_time = 5 minutes; + ts_warn_delta = 30 seconds; + ts_max_delta = 5 minutes; + client_exit = yes; + collision_fnc = yes; + resv_fnc = yes; + global_snotices = yes; + dline_with_reason = yes; + kline_delay = 0 seconds; + kline_with_reason = yes; + kline_reason = "K-Lined"; + identify_service = "NickServ@services.int"; + identify_command = "IDENTIFY"; + non_redundant_klines = yes; + warn_no_nline = yes; + use_propagated_bans = yes; + stats_e_disabled = no; + stats_c_oper_only=no; + stats_h_oper_only=no; + client_flood_max_lines = 16000; + client_flood_burst_rate = 32000; + client_flood_burst_max = 32000; + client_flood_message_num = 32000; + client_flood_message_time = 32000; + use_whois_actually = no; + oper_only_umodes = operwall, locops, servnotice; + oper_umodes = locops, servnotice, operwall, wallop; + oper_snomask = "+s"; + burst_away = yes; + nick_delay = 0 seconds; # 15 minutes if you want to enable this + reject_ban_time = 1 minute; + reject_after_count = 3; + reject_duration = 5 minutes; + throttle_duration = 1; + throttle_count = 1000; + max_ratelimit_tokens = 30; + away_interval = 30; + disable_auth = yes; + }; + + modules { + path = "modules"; + path = "modules/autoload"; + }; + + exempt { + ip = "10.243.0.0/16"; + }; +'' diff --git a/modules/charybdis/default.nix b/modules/charybdis/default.nix new file mode 100644 index 0000000..337ea13 --- /dev/null +++ b/modules/charybdis/default.nix @@ -0,0 +1,80 @@ +{ config, lib, mylib, pkgs, ... }@args: let + cfg = config.tv.charybdis; +in { + options.tv.charybdis = { + enable = lib.mkEnableOption "tv.charybdis"; + motd = lib.mkOption { + type = lib.types.str; + default = "/join #retiolum"; + }; + port = lib.mkOption { + type = lib.types.int; + default = 6667; + }; + ssl_cert = lib.mkOption { + type = lib.types.path; + }; + ssl_dh_params = lib.mkOption { + type = mylib.types.absolute-pathname; + default = "${config.krebs.secret.directory}/charybdis.dh.pem"; + }; + ssl_private_key = lib.mkOption { + type = mylib.types.absolute-pathname; + default = "${config.krebs.secret.directory}/charybdis.key.pem"; + }; + sslport = lib.mkOption { + type = lib.types.int; + default = 6697; + }; + user = lib.mkOption { + type = mylib.types.user; + default = { + name = "charybdis"; + home = "/var/lib/charybdis"; + }; + }; + }; + config = lib.mkIf cfg.enable { + + environment.etc."charybdis-ircd.motd".text = cfg.motd; + + krebs.systemd.services.charybdis = {}; + + systemd.services.charybdis = { + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + environment = { + BANDB_DBPATH = "${cfg.user.home}/ban.db"; + }; + serviceConfig = { + SyslogIdentifier = "charybdis"; + User = cfg.user.name; + PrivateTmp = true; + Restart = "always"; + ExecStartPre = [ + "${pkgs.coreutils}/bin/ln -s /etc/charybdis-ircd.motd /tmp/ircd.motd" + "${pkgs.coreutils}/bin/ln -s \${CREDENTIALS_DIRECTORY} /tmp/credentials" + ]; + ExecStart = toString [ + "${pkgs.charybdis}/bin/charybdis" + "-configfile ${import ./config.nix args}" + "-foreground" + "-logfile /dev/stderr" + ]; + LoadCredential = [ + "ssl_dh_params:${cfg.ssl_dh_params}" + "ssl_private_key:${cfg.ssl_private_key}" + ]; + }; + }; + + users.users.${cfg.user.name} = { + inherit (cfg.user) home name uid; + createHome = true; + group = cfg.user.name; + isSystemUser = true; + }; + + users.groups.${cfg.user.name} = {}; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..efe9420 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,7 @@ +{ lib, mylib, ... }: { + imports = + map + (name: ./. + "/${name}") + (builtins.attrNames + (lib.filterAttrs mylib.isNixDirEntry (builtins.readDir ./.))); +} diff --git a/modules/dnsmasq.nix b/modules/dnsmasq.nix new file mode 100644 index 0000000..b12cea3 --- /dev/null +++ b/modules/dnsmasq.nix @@ -0,0 +1,60 @@ +# TODO kill this in favor of unbound +{ config, lib, mylib, ... }: let + cfg = config.tv.dnsmasq; +in { + + options.tv.dnsmasq = { + enable = lib.mkEnableOption "tv.dnsmasq"; + dhcp-range = lib.mkOption { + type = lib.types.str; + }; + interface = lib.mkOption { + type = lib.types.str; + }; + address = lib.mkOption { + type = lib.types.str; + }; + prefixLength = lib.mkOption { + type = lib.types.addCheck lib.types.int (x: x >= 0 && x <= 32); + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + networking.dhcpcd.denyInterfaces = [ cfg.interface ]; + services.dnsmasq.resolveLocalQueries = false; + networking.interfaces.${cfg.interface} = { + ipv4.addresses = [ + { + address = cfg.address; + prefixLength = cfg.prefixLength; + } + ]; + }; + services.dnsmasq.enable = true; + services.dnsmasq.extraConfig = '' + bind-interfaces + dhcp-range=${cfg.dhcp-range} + listen-address=${cfg.address} + ''; + tv.iptables.extra.filter.INPUT = [ + "-i ${cfg.interface} -p tcp -m tcp --dport bootps -j ACCEPT" + "-i ${cfg.interface} -p udp -m udp --dport bootps -j ACCEPT" + "-i ${cfg.interface} -p tcp -m tcp --dport domain -j ACCEPT" + "-i ${cfg.interface} -p udp -m udp --dport domain -j ACCEPT" + ]; + } + { + # enable forwarding + boot.kernel.sysctl."net.ipv4.ip_forward" = true; + tv.iptables.extra.filter.FORWARD = [ + "-m state --state RELATED,ESTABLISHED -j ACCEPT" + "-i ${cfg.interface} -j ACCEPT" + ]; + tv.iptables.extra.nat.POSTROUTING = [ + "-j MASQUERADE" + ]; + } + ]); + +} diff --git a/modules/ejabberd/default.nix b/modules/ejabberd/default.nix new file mode 100644 index 0000000..02c060d --- /dev/null +++ b/modules/ejabberd/default.nix @@ -0,0 +1,274 @@ +{ config, lib, mylib, pkgs, ... }: let + cfg = config.tv.ejabberd; + + gen-dhparam = pkgs.writeDash "gen-dhparam" '' + set -efu + path=$1 + bits=2048 + # TODO regenerate dhfile after some time? + if ! test -e "$path"; then + ${pkgs.openssl}/bin/openssl dhparam "$bits" > "$path" + fi + ''; + + settingsFormat = pkgs.formats.json {}; + +in { + options.tv.ejabberd = { + enable = lib.mkEnableOption "tv.ejabberd"; + certfiles = lib.mkOption { + type = lib.types.listOf mylib.types.absolute-pathname; + default = [ + (toString + "/ejabberd.pem") + ]; + }; + configFile = lib.mkOption { + type = lib.types.either lib.types.package mylib.types.absolute-pathname; + default = settingsFormat.generate "ejabberd.yaml" cfg.settings; + }; + ciphers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "ECDHE-ECDSA-AES256-GCM-SHA384" + "ECDHE-RSA-AES256-GCM-SHA384" + "ECDHE-ECDSA-CHACHA20-POLY1305" + "ECDHE-RSA-CHACHA20-POLY1305" + "ECDHE-ECDSA-AES128-GCM-SHA256" + "ECDHE-RSA-AES128-GCM-SHA256" + "ECDHE-ECDSA-AES256-SHA384" + "ECDHE-RSA-AES256-SHA384" + "ECDHE-ECDSA-AES128-SHA256" + "ECDHE-RSA-AES128-SHA256" + ]; + }; + credentials.certfiles = lib.mkOption { + internal = true; + readOnly = true; + default = + lib.imap + (i: _: "/tmp/credentials/certfile${builtins.toJSON i}") + cfg.certfiles; + }; + hosts = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; + pkgs.ejabberd = lib.mkOption { + type = mylib.types.package; + default = pkgs.symlinkJoin { + name = "ejabberd-wrapper"; + paths = [ + (pkgs.writeDashBin "ejabberdctl" '' + exec ${pkgs.ejabberd}/bin/ejabberdctl \ + --config /etc/ejabberd/ejabberd.yaml \ + --ctl-config /etc/ejabberd/ejabberdctl.cfg \ + --logs ${cfg.stateDir} \ + --spool ${cfg.stateDir} \ + "$@" + '') + pkgs.ejabberd + ]; + }; + }; + protocol_options = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "no_sslv2" + "no_sslv3" + "no_tlsv1" + "no_tlsv1_10" + ]; + }; + registration_watchers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + config.krebs.users.tv.mail + ]; + }; + settings = lib.mkOption { + type = settingsFormat.type; + default = {}; + }; + stateDir = lib.mkOption { + type = + lib.types.addCheck + mylib.types.absolute-pathname + (path: + lib.hasPrefix "/var/lib/" path && + mylib.types.filename.check (lib.removePrefix "/var/lib/" path) + ); + default = "/var/lib/ejabberd"; + }; + }; + config = lib.mkIf cfg.enable { + + environment.etc."ejabberd/ejabberd.yaml".source = cfg.configFile; + environment.etc."ejabberd/ejabberdctl.cfg".source = + builtins.toFile "ejabberdctl.cfg" /* sh */ '' + ERL_OPTIONS='-setcookie ${cfg.stateDir}/.erlang.cookie' + ''; + + environment.systemPackages = [ + (pkgs.symlinkJoin { + name = "ejabberd-sudo-wrapper"; + paths = [ + (pkgs.writeDashBin "ejabberdctl" '' + exec ${pkgs.systemd}/bin/systemd-run \ + --unit=ejabberdctl \ + --property=StateDirectory=ejabberd \ + --property=User=ejabberd \ + --collect \ + --pipe \ + --quiet \ + ${cfg.pkgs.ejabberd}/bin/ejabberdctl "$@" + '') + cfg.pkgs.ejabberd + ]; + }) + ]; + + krebs.systemd.services.ejabberd.restartIfCredentialsChange = true; + + systemd.services.ejabberd = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + reloadTriggers = [ + config.environment.etc."ejabberd/ejabberd.yaml".source + config.environment.etc."ejabberd/ejabberdctl.cfg".source + ]; + serviceConfig = { + ExecStartPre = [ + "${pkgs.coreutils}/bin/ln -s \${CREDENTIALS_DIRECTORY} /tmp/credentials" + "${gen-dhparam} ${cfg.stateDir}/dhfile" + ]; + ExecStart = "${cfg.pkgs.ejabberd}/bin/ejabberdctl foreground"; + ExecStop = [ + "${cfg.pkgs.ejabberd}/bin/ejabberdctl stop" + "${cfg.pkgs.ejabberd}/bin/ejabberdctl stopped" + ]; + ExecReload = "${cfg.pkgs.ejabberd}/bin/ejabberdctl reload_config"; + LoadCredential = + lib.zipListsWith + (dst: src: "${baseNameOf dst}:${src}") + cfg.credentials.certfiles + cfg.certfiles; + LimitNOFILE = 65536; + PrivateDevices = true; + PrivateTmp = true; + SyslogIdentifier = "ejabberd"; + StateDirectory = "ejabberd"; + User = "ejabberd"; + DynamicUser = true; + TimeoutSec = 60; + RestartSec = 5; + Restart = "on-failure"; + Type = "notify"; + NotifyAccess = "all"; + WatchdogSec = 30; + }; + }; + + # preset config values + tv.ejabberd.settings = { + access_rules = { + announce = lib.mkDefault [{ allow = "admin"; }]; + local = lib.mkDefault [{ allow = "local"; }]; + configure = lib.mkDefault [{ allow = "admin"; }]; + register = lib.mkDefault ["allow"]; + s2s = lib.mkDefault ["allow"]; + trusted_network = lib.mkDefault [{ allow = "loopback"; }]; + }; + + acl = { + local.user_regexp = lib.mkDefault ""; + loopback.ip = lib.mkDefault [ + "127.0.0.0/8" + "::1/128" + "::FFFF:127.0.0.1/128" + ]; + }; + + certfiles = lib.mkDefault cfg.credentials.certfiles; + + hosts = lib.mkDefault cfg.hosts; + + language = lib.mkDefault "en"; + + listen = lib.mkDefault [ + { + port = 5222; + ip = "::"; + module = "ejabberd_c2s"; + shaper = "c2s_shaper"; + ciphers = lib.concatStringsSep ":" cfg.ciphers; + protocol_options = cfg.protocol_options; + starttls = true; + starttls_required = true; + tls = false; + tls_compression = false; + max_stanza_size = 65536; + } + { + port = 5269; + ip = "::"; + module = "ejabberd_s2s_in"; + shaper = "s2s_shaper"; + dhfile = "${cfg.stateDir}/dhfile"; + max_stanza_size = 131072; + } + ]; + + loglevel = lib.mkDefault "4"; + + modules = { + mod_adhoc = lib.mkDefault {}; + mod_admin_extra = lib.mkDefault {}; + mod_announce.access = lib.mkDefault "announce"; + mod_caps = lib.mkDefault {}; + mod_carboncopy = lib.mkDefault {}; + mod_client_state = lib.mkDefault {}; + mod_configure = lib.mkDefault {}; + mod_disco = lib.mkDefault {}; + mod_echo = lib.mkDefault {}; + mod_bosh = lib.mkDefault {}; + mod_last = lib.mkDefault {}; + mod_offline.access_max_user_messages = lib.mkDefault "max_user_offline_messages"; + mod_ping = lib.mkDefault {}; + mod_privacy = lib.mkDefault {}; + mod_private = lib.mkDefault {}; + mod_register = { + access_from = lib.mkDefault "deny"; + access = lib.mkDefault "register"; + ip_access = lib.mkDefault "trusted_network"; + registration_watchers = lib.mkDefault cfg.registration_watchers; + }; + mod_roster = lib.mkDefault {}; + mod_shared_roster = lib.mkDefault {}; + mod_stats = lib.mkDefault {}; + mod_time = lib.mkDefault {}; + mod_vcard.search = lib.mkDefault false; + mod_version = lib.mkDefault {}; + mod_http_api = lib.mkDefault {}; + }; + + s2s_access = lib.mkDefault "s2s"; + s2s_ciphers = lib.concatStringsSep ":" cfg.ciphers; + s2s_dhfile = lib.mkDefault "${cfg.stateDir}/dhfile"; + s2s_protocol_options = lib.mkDefault cfg.protocol_options; + s2s_tls_compression = lib.mkDefault false; + s2s_use_starttls = lib.mkDefault "required"; + + shaper_rules = { + max_user_offline_messages = lib.mkDefault [ + { "5000" = "admin"; } + 100 + ]; + max_user_sessions = lib.mkDefault 10; + c2s_shaper = lib.mkDefault [ + { "none" = "admin"; } + "normal" + ]; + s2s_shaper = lib.mkDefault "fast"; + }; + }; + }; +} diff --git a/modules/focus.nix b/modules/focus.nix new file mode 100644 index 0000000..0468489 --- /dev/null +++ b/modules/focus.nix @@ -0,0 +1,3 @@ +{ lib, ... }: { + options.tv.focus.enable = lib.mkEnableOption "tv.focus"; +} diff --git a/modules/hosts.nix b/modules/hosts.nix new file mode 100644 index 0000000..ba7ffe3 --- /dev/null +++ b/modules/hosts.nix @@ -0,0 +1,8 @@ +{ config, lib, mylib, ... }: { + options.tv.hosts = lib.mkOption { + type = lib.types.attrsOf mylib.types.host; + default = + lib.filterAttrs (_: host: host.owner.name == "tv") + config.krebs.hosts; + }; +} diff --git a/modules/hw.nix b/modules/hw.nix new file mode 100644 index 0000000..653b04f --- /dev/null +++ b/modules/hw.nix @@ -0,0 +1,15 @@ +{ lib, mylib, ... }: let + local.types.screen = lib.types.submodule { + options.width = lib.mkOption { + type = mylib.types.uint; + }; + options.height = lib.mkOption { + type = mylib.types.uint; + }; + }; +in { + options.tv.hw.screens = lib.mkOption { + type = lib.types.attrsOf local.types.screen; + default = {}; + }; +} diff --git a/modules/im.nix b/modules/im.nix new file mode 100644 index 0000000..d3c5158 --- /dev/null +++ b/modules/im.nix @@ -0,0 +1,109 @@ +{ config, lib, mylib, pkgs, ... }: let + im = config.tv.im; +in { + options = { + tv.im.client.enable = lib.mkEnableOption "tv.im.client" // { + default = config.krebs.build.host.name == im.client.host.name; + }; + tv.im.client.term = lib.mkOption { + default = "rxvt-unicode-256color"; + type = mylib.types.filename; + }; + tv.im.client.useIPv6 = lib.mkEnableOption "tv.im.client.useIPv6" // { + default = true; + }; + tv.im.client.host = lib.mkOption { + default = config.krebs.hosts.xu; + type = mylib.types.host; + }; + tv.im.client.user = lib.mkOption { + default = config.krebs.users.tv; + type = mylib.types.user; + }; + + tv.im.server.enable = lib.mkEnableOption "tv.im.server" // { + default = config.krebs.build.host.name == im.server.host.name; + }; + tv.im.server.host = lib.mkOption { + default = config.krebs.hosts.nomic; + type = mylib.types.host; + }; + tv.im.server.mosh.enable = lib.mkEnableOption "tv.im.server.mosh" // { + default = true; + }; + tv.im.server.weechat.relay.enable = + lib.mkEnableOption "tv.im.server.weechat.relay"; + tv.im.server.user = lib.mkOption { + default = config.krebs.users.tv; + type = mylib.types.user; + }; + }; + imports = [ + (lib.mkIf im.client.enable { + users.users.${im.client.user.name}.packages = [ + (pkgs.writeDashBin "im" '' + ${if im.server.mosh.enable then /* sh */ '' + exec ${pkgs.mosh}/bin/mosh \ + ${lib.optionalString im.client.useIPv6 "-6"} \ + ${im.server.user.name}@${lib.head im.server.host.nets.retiolum.aliases} \ + env TERM=${im.client.term} im + '' else /* sh */ '' + exec ${pkgs.openssh}/bin/ssh \ + ${lib.optionalString im.client.useIPv6 "-6"} \ + ${im.server.user.name}@${lib.head im.server.host.nets.retiolum.aliases} \ + -t \ + im + ''} + '') + ]; + }) + (lib.mkIf im.server.enable { + services.bitlbee = { + enable = true; + plugins = [ + pkgs.bitlbee-facebook + ]; + }; + users.users.${im.server.user.name}.packages = [ + pkgs.mosh + (pkgs.writeDashBin "im" '' + export PATH=${lib.makeSearchPath "bin" [ + pkgs.tmux + pkgs.gnugrep + pkgs.weechat-tv + ]} + if tmux list-sessions -F\#S | grep -q '^im''$'; then + exec tmux attach -t im + else + exec tmux new -s im weechat + fi + '') + ]; + }) + (lib.mkIf im.server.mosh.enable { + krebs.setuid.utempter = { + filename = "${pkgs.libutempter}/lib/utempter/utempter"; + owner = "nobody"; + group = "utmp"; + mode = "2111"; + }; + tv.iptables.extra4.filter.Retiolum = [ + "-s ${im.client.host.nets.retiolum.ip4.addr} -p udp --dport 60000:61000 -j ACCEPT" + ]; + tv.iptables.extra6.filter.Retiolum = [ + "-s ${im.client.host.nets.retiolum.ip6.addr} -p udp --dport 60000:61000 -j ACCEPT" + ]; + }) + (lib.mkIf im.server.weechat.relay.enable { + krebs.iana-etc.services = { + "9001".tcp.name = "weechat-ssl"; + }; + tv.iptables.extra4.filter.Retiolum = [ + "-s ${im.client.host.nets.retiolum.ip4.addr} -p tcp -m tcp --dport 9001 -j ACCEPT" + ]; + tv.iptables.extra6.filter.Retiolum = [ + "-s ${im.client.host.nets.retiolum.ip6.addr} -p tcp -m tcp --dport 9001 -j ACCEPT" + ]; + }) + ]; +} diff --git a/modules/iptables.nix b/modules/iptables.nix new file mode 100644 index 0000000..e2fdcbc --- /dev/null +++ b/modules/iptables.nix @@ -0,0 +1,207 @@ +{ config, lib, pkgs, ... }: let { + cfg = config.tv.iptables; + + body = { + options.tv.iptables = api; + config = lib.mkIf cfg.enable imp; + }; + + extraTypes = { + rules = lib.types.submodule { + options = { + nat.OUTPUT = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + nat.PREROUTING = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + nat.POSTROUTING = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + filter.FORWARD = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + filter.INPUT = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + filter.Retiolum = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + filter.Wiregrill = lib.mkOption { + type = with lib.types; listOf str; + default = []; + }; + }; + }; + }; + + api = { + enable = lib.mkEnableOption "tv.iptables"; + + accept-echo-request = lib.mkOption { + type = with lib.types; nullOr (enum ["internet" "retiolum"]); + default = "retiolum"; + }; + + input-internet-accept-tcp = lib.mkOption { + type = with lib.types; listOf (either int str); + default = []; + }; + + input-internet-accept-udp = lib.mkOption { + type = with lib.types; listOf (either int str); + default = []; + }; + + input-retiolum-accept-tcp = lib.mkOption { + type = with lib.types; listOf (either int str); + default = []; + }; + + input-retiolum-accept-udp = lib.mkOption { + type = with lib.types; listOf (either int str); + default = []; + }; + + input-wiregrill-accept-tcp = lib.mkOption { + type = with lib.types; listOf (either int str); + default = []; + }; + + input-wiregrill-accept-udp = lib.mkOption { + type = with lib.types; listOf (either int str); + default = []; + }; + + extra = lib.mkOption { + default = {}; + type = extraTypes.rules; + }; + + extra4 = lib.mkOption { + default = {}; + type = extraTypes.rules; + }; + + extra6 = lib.mkOption { + default = {}; + type = extraTypes.rules; + }; + }; + + imp = { + networking.firewall.enable = false; + + systemd.services.tv-iptables = { + wantedBy = [ "sysinit.target" ]; + wants = [ "network-pre.target" ]; + before = [ "network-pre.target" ]; + after = [ "systemd-modules-load.service" ]; + + path = with pkgs; [ + iptables + ]; + + restartIfChanged = true; + + serviceConfig = { + Type = "simple"; + RemainAfterExit = true; + Restart = "always"; + SyslogIdentifier = "tv-iptables_start"; + ExecStart = pkgs.writeDash "tv-iptables_start" '' + set -euf + iptables-restore < ${rules 4} + ip6tables-restore < ${rules 6} + ''; + }; + + unitConfig.DefaultDependencies = false; + }; + }; + + formatTable = table: + (lib.concatStringsSep "\n" + (lib.mapAttrsToList + (chain: lib.concatMapStringsSep "\n" (rule: "-A ${chain} ${rule}")) + table)); + + rules = iptables-version: let + accept-echo-request = { + ip4tables = "-p icmp -m icmp --icmp-type echo-request -j ACCEPT"; + ip6tables = "-p ipv6-icmp -m icmp6 --icmpv6-type echo-request -j ACCEPT"; + }."ip${toString iptables-version}tables"; + accept-tcp = port: "-p tcp -m tcp --dport ${port} -j ACCEPT"; + accept-udp = port: "-p udp -m udp --dport ${port} -j ACCEPT"; + in + pkgs.writeText "tv-iptables-rules${toString iptables-version}" '' + *nat + :PREROUTING ACCEPT [0:0] + :INPUT ACCEPT [0:0] + :OUTPUT ACCEPT [0:0] + :POSTROUTING ACCEPT [0:0] + ${formatTable cfg."extra${toString iptables-version}".nat} + ${formatTable cfg.extra.nat} + COMMIT + *filter + :INPUT DROP [0:0] + :FORWARD DROP [0:0] + :OUTPUT ACCEPT [0:0] + :Retiolum - [0:0] + :Wiregrill - [0:0] + ${lib.concatMapStringsSep "\n" (rule: "-A INPUT ${rule}") ([] + ++ [ + "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT" + "-i lo -j ACCEPT" + ] + ++ lib.optional (cfg.accept-echo-request == "internet") accept-echo-request + ++ map accept-tcp (lib.unique (map toString cfg.input-internet-accept-tcp)) + ++ map accept-udp (lib.unique (map toString cfg.input-internet-accept-udp)) + ++ ["-i retiolum -j Retiolum"] + ++ ["-i wiregrill -j Wiregrill"] + )} + ${formatTable cfg.extra.filter} + ${formatTable cfg."extra${toString iptables-version}".filter} + ${lib.concatMapStringsSep "\n" (rule: "-A Retiolum ${rule}") ([] + ++ lib.optional (cfg.accept-echo-request == "retiolum") accept-echo-request + ++ map accept-tcp (lib.unique (map toString cfg.input-retiolum-accept-tcp)) + ++ map accept-udp (lib.unique (map toString cfg.input-retiolum-accept-udp)) + ++ { + ip4tables = [ + "-p tcp -j REJECT --reject-with tcp-reset" + "-p udp -j REJECT --reject-with icmp-port-unreachable" + "-j REJECT --reject-with icmp-proto-unreachable" + ]; + ip6tables = [ + "-p tcp -j REJECT --reject-with tcp-reset" + "-p udp -j REJECT --reject-with icmp6-port-unreachable" + "-j REJECT" + ]; + }."ip${toString iptables-version}tables" + )} + ${lib.concatMapStringsSep "\n" (rule: "-A Wiregrill ${rule}") ([] + ++ lib.optional (cfg.accept-echo-request == "wiregrill") accept-echo-request + ++ map accept-tcp (lib.unique (map toString cfg.input-wiregrill-accept-tcp)) + ++ map accept-udp (lib.unique (map toString cfg.input-wiregrill-accept-udp)) + ++ { + ip4tables = [ + "-p tcp -j REJECT --reject-with tcp-reset" + "-p udp -j REJECT --reject-with icmp-port-unreachable" + "-j REJECT --reject-with icmp-proto-unreachable" + ]; + ip6tables = [ + "-p tcp -j REJECT --reject-with tcp-reset" + "-p udp -j REJECT --reject-with icmp6-port-unreachable" + "-j REJECT" + ]; + }."ip${toString iptables-version}tables" + )} + COMMIT + ''; +} diff --git a/modules/lidControl.nix b/modules/lidControl.nix new file mode 100644 index 0000000..6beb032 --- /dev/null +++ b/modules/lidControl.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, ... }: { + options = { + tv.lidControl.enable = lib.mkEnableOption "tv.lidControl"; + }; + config = let + cfg = config.tv.lidControl; + in lib.mkIf cfg.enable { + services.acpid.enable = true; + services.acpid.lidEventCommands = /* sh */ '' + set -- $1 + + # usage: vt_is_xserver NUMBER + vt_is_xserver() { + ${pkgs.iproute}/bin/ss -lp src unix:/tmp/.X11-unix/X* | + ${pkgs.gnused}/bin/sed -n 's|.*/tmp/.X11-unix/X\([0-9]\+\)\>.*|\1|p' | + ${pkgs.gnugrep}/bin/grep -Fqx "$1" + } + + console=$(${pkgs.kbd}/bin/fgconsole) + + if vt_is_xserver "$console"; then + # usage: run_on_display COMMAND [ARG...] + run_on_display() { + owner=$(${pkgs.coreutils}/bin/stat -c %u /tmp/.X11-unix/X$console) + ${pkgs.systemd}/bin/systemd-run -GPq \ + -E DISPLAY=:$console \ + --uid=$owner \ + "$@" + } + case $3 in + open) + run_on_display ${pkgs.xorg.xset}/bin/xset dpms force on + ;; + close) + run_on_display ${pkgs.xorg.xset}/bin/xset dpms force off + ;; + esac + fi + ''; + services.logind.lidSwitch = "ignore"; + services.logind.lidSwitchDocked = "ignore"; + services.logind.lidSwitchExternalPower = "ignore"; + }; +} diff --git a/modules/org.freedesktop.machine1.host-shell.nix b/modules/org.freedesktop.machine1.host-shell.nix new file mode 100644 index 0000000..b71799d --- /dev/null +++ b/modules/org.freedesktop.machine1.host-shell.nix @@ -0,0 +1,28 @@ +{ config, lib, mylib, ... }: { + options.org.freedesktop.machine1.host-shell.access = lib.mkOption { + default = {}; + type = + lib.types.addCheck + (lib.types.attrsOf (lib.types.attrsOf lib.types.bool)) + (x: + lib.all + mylib.types.username.check + (lib.concatLists + (lib.mapAttrsToList + (name: value: [name] ++ lib.attrNames value) + x))); + }; + config.security.polkit.extraConfig = let + cfg = config.org.freedesktop.machine1.host-shell; + enable = cfg.access != {}; + in lib.optionalString enable /* js */ '' + polkit.addRule(function () { + var access = ${builtins.toJSON cfg.access}; + return function(action, subject) { + if (action.id === "org.freedesktop.machine1.host-shell" + && (access[subject.user]||{})[action.lookup("user")]) + return polkit.Result.YES; + } + }()); + ''; +} diff --git a/modules/slock.nix b/modules/slock.nix new file mode 100644 index 0000000..d96ae42 --- /dev/null +++ b/modules/slock.nix @@ -0,0 +1,77 @@ +{ config, lib, mylib, pkgs, ... }: let + cfg = config.tv.slock; +in { + options.tv.slock = { + enable = lib.mkEnableOption "tv.slock"; + package = lib.mkOption { + default = pkgs.writeDashBin "slock" '' + set -efu + display=''${DISPLAY#:} + service=slock-$LOGNAME@$display.service + exec ${pkgs.systemd}/bin/systemctl start "$service" + ''; + type = lib.types.package; + }; + user = lib.mkOption { + type = mylib.types.user; + }; + }; + config = lib.mkIf cfg.enable { + security.polkit.extraConfig = /* js */ '' + polkit.addRule(function(action, subject) { + if (action.id === "org.freedesktop.systemd1.manage-units" && + subject.user === ${builtins.toJSON cfg.user.name} && + /^slock-${cfg.user.name}@[0-9]+\.service$/.test(action.lookup("unit")) ) { + return polkit.Result.YES; + } + }); + ''; + systemd.services."slock-${cfg.user.name}@" = { + conflicts = [ + "picom@%i.target" + ]; + environment = { + DISPLAY = ":%I"; + LD_PRELOAD = pkgs.runCommandCC "slock-${cfg.user.name}.so" { + passAsFile = ["text"]; + text = /* c */ '' + #include + #include + + static struct spwd entry = { + .sp_namp = "", + .sp_pwdp = + ${mylib.toC config.users.users.${cfg.user.name}.hashedPassword}, + .sp_lstchg = 0, + .sp_min = 0, + .sp_max = 0, + .sp_warn = 0, + .sp_inact = 0, + .sp_expire = 0, + .sp_flag = 0, + }; + + extern struct spwd *getspnam(const char *name) { return &entry; } + extern int setgroups(size_t size, const gid_t *list) { return 0; } + extern int setgid(gid_t gid) { return 0; } + extern int setuid(uid_t uid) { return 0; } + ''; + } /* sh */ '' + gcc -Wall -shared -o $out -xc "$textPath" + ''; + }; + restartIfChanged = false; + serviceConfig = { + ExecStart = "${pkgs.slock}/bin/slock"; + ExecStopPost = + "+${pkgs.systemd}/bin/systemctl start xsession@%i.target"; + OOMScoreAdjust = -1000; + Restart = "on-failure"; + RestartSec = "100ms"; + StartLimitBurst = 0; + SyslogIdentifier = "slock"; + User = cfg.user.name; + }; + }; + }; +} diff --git a/modules/systemd.nix b/modules/systemd.nix new file mode 100644 index 0000000..20b1d73 --- /dev/null +++ b/modules/systemd.nix @@ -0,0 +1,49 @@ +{ config, lib, ... }: let + normalUsers = + lib.filterAttrs (_: builtins.getAttr "isNormalUser") config.users.users; +in { + options = { + tv.systemd.services = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule (self: { + options = { + operators = lib.mkOption { + type = + lib.types.listOf + (lib.types.enum (builtins.attrNames normalUsers)); + default = []; + }; + }; + })); + default = {}; + }; + }; + config = { + security.polkit.extraConfig = let + access = + lib.mapAttrs' + (name: cfg: + lib.nameValuePair "${name}.service" + (lib.genAttrs cfg.operators (_: true)) + ) + config.tv.systemd.services; + in lib.optionalString (access != {}) /* js */ '' + polkit.addRule(function () { + const access = ${builtins.toJSON access}; + return function (action, subject) { + if (action.id === "org.freedesktop.systemd1.manage-units") { + const unit = action.lookup("unit"); + if ( + (access[unit]||{})[subject.user] || + ( + unit.includes("@") && + (access[unit.replace(/@[^.]+/, "@")]||{})[subject.user] + ) + ) { + return polkit.Result.YES; + } + } + } + }()); + ''; + }; +} diff --git a/modules/unbound.nix b/modules/unbound.nix new file mode 100644 index 0000000..6a51027 --- /dev/null +++ b/modules/unbound.nix @@ -0,0 +1,84 @@ +{ config, lib, pkgs, ... }: { + options.tv.unbound = { + enable = lib.mkEnableOption "tv.unbound"; + DoH.enable = lib.mkEnableOption "tv.unbound.DoH"; + DoT.enable = lib.mkEnableOption "tv.unbound.DoT"; + host = lib.mkOption { + type = lib.types.str; + }; + useACMEHost = lib.mkOption { + type = lib.types.str; + }; + }; + imports = let + cfg = config.tv.unbound; + in [ + (lib.mkIf cfg.enable { + services.unbound = { + enable = true; + settings.server = { + access-control = [ + "::/0 allow" + "0.0.0.0/0 allow" + ]; + interface = [ + "127.0.0.1@53" + "retiolum@53" + "wiregrill@53" + ]; + prefetch = true; + prefetch-key = true; + }; + }; + # Since we use this for local dns resolving, we don't want to stop/start + # but just restart, so we quickly get it back. + systemd.services.unbound.stopIfChanged = false; + + tv.iptables.input-retiolum-accept-udp = [ "domain" ]; + tv.iptables.input-wiregrill-accept-udp = [ "domain" ]; + }) + (lib.mkIf cfg.DoH.enable (let + http-port = 8053; + http-endpoint = "/query"; + in { + services.unbound.package = pkgs.unbound-with-systemd.override { + withDoH = true; + }; + services.unbound.settings.server.interface = [ + "127.0.0.1@${toString http-port}" + ]; + services.unbound.settings.server = { + https-port = http-port; + http-endpoint = http-endpoint; + http-notls-downstream = true; + }; + services.nginx.virtualHosts.${cfg.host} = { + useACMEHost = cfg.useACMEHost; + forceSSL = true; + http2 = true; + locations."/".return = ''404 "Not Found\n"''; + locations.${http-endpoint}.extraConfig = '' + grpc_pass grpc://127.0.0.1:${toString http-port}; + ''; + }; + + tv.iptables.input-internet-accept-tcp = [ "https" ]; + })) + (lib.mkIf cfg.DoT.enable { + services.unbound.settings.server = { + interface = [ + "::@853" + "0.0.0.0@853" + ]; + tls-service-key = "/run/credentials/unbound.service/tls-service-key"; + tls-service-pem = "/run/credentials/unbound.service/tls-service-pem"; + }; + krebs.systemd.services.unbound.restartIfCredentialsChange = true; + systemd.services.unbound.serviceConfig.LoadCredential = [ + "tls-service-key:/var/lib/acme/${cfg.useACMEHost}/key.pem" + "tls-service-pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem" + ]; + tv.iptables.input-internet-accept-tcp = [ "domain-s" ]; + }) + ]; +} diff --git a/modules/wwan.nix b/modules/wwan.nix new file mode 100644 index 0000000..1ce51ab --- /dev/null +++ b/modules/wwan.nix @@ -0,0 +1,181 @@ +{ config, lib, mylib, pkgs, ... }: { + options = { + tv.wwan.enable = lib.mkEnableOption "tv.wwan"; + tv.wwan.apn = lib.mkOption { + type = mylib.types.filename; + }; + tv.wwan.device = lib.mkOption { + type = mylib.types.pathname; + default = "/dev/cdc-wdm0"; + }; + tv.wwan.interface = lib.mkOption { + type = lib.types.nullOr mylib.types.filename; + default = null; + }; + tv.wwan.operators = lib.mkOption { + type = lib.types.listOf mylib.types.username; + default = []; + }; + tv.wwan.secrets = lib.mkOption { + type = mylib.types.pathname; + default = toString ; + # format: {"pin1":number} + }; + }; + config = let + cfg = config.tv.wwan; + in lib.mkIf cfg.enable { + nixpkgs.overlays = lib.singleton (self: super: { + uqmi-wrapper = pkgs.symlinkJoin { + name = "uqmi-wrapper"; + paths = [ + (pkgs.writeDashBin "uqmi" '' + exec ${pkgs.uqmi}/bin/uqmi --device=${cfg.device} "$@" + '') + (pkgs.writeTextDir "share/bash-completion/completions/uqmi" /* sh */'' + _uqmi_complete() { + case ''${#COMP_WORDS[@]} in + 2) + COMPREPLY=($(compgen -W "$( + ${pkgs.uqmi}/bin/uqmi --help 2>&1 | + ${pkgs.coreutils}/bin/tr , \\n | + ${pkgs.gnused}/bin/sed -nr 's/^ *(-[a-z-]+).*/\1/p' + )" -- "''${COMP_WORDS[1]}")) + ;; + esac + } + complete -F _uqmi_complete uqmi + '') + pkgs.uqmi + ]; + }; + }); + systemd.services.wwan = { + environment = { + SECRETS = "%d/secrets"; + }; + path = [ + pkgs.busybox + pkgs.coreutils + pkgs.iproute2 + pkgs.jq + pkgs.uqmi-wrapper + (pkgs.writeDashBin "get-interface" ( + if cfg.interface != null then /* sh */ '' + echo ${cfg.interface} + '' else /* sh */ '' + exec ${pkgs.libqmi}/bin/qmicli -d ${cfg.device} -p --get-wwan-iface + '' + )) + ]; + serviceConfig = { + LoadCredential = [ + "secrets:${cfg.secrets}" + ]; + Type = "oneshot"; + RemainAfterExit = true; + SyslogIdentifier = "wwan"; + ExecStart = pkgs.writeDash "tv.wwan.start.sh" '' + set -efu + + interface=$(get-interface) + + pin1_status=$( + uqmi --uim-get-sim-state | + jq -r '"\(.pin1_status)/\(.pin1_verify_tries)"' + ) + case $pin1_status in + verified/*) + : + ;; + not_verified/3) + pin1=$(jq .pin1 "$SECRETS") + echo "verifying PIN1" >&2 + if ! uqmi --uim-verify-pin1 "$pin1"; then + echo "error: failed to verify PIN1" >&2 + exit 1 + fi + ;; + not_verified/*) + echo "error: not trying to verify PIN1: not enough tries left" >&2 + echo \ + "please check your configuration in ${cfg.secrets}" \ + " and verify if manually using:" \ + " ${pkgs.uqmi}/bin/uqmi -d $device --uim-veriy-pin1 XXXX" \ + >&2 + exit 1 + esac + + raw_ip_path=/sys/class/net/$interface/qmi/raw_ip + raw_ip=$(cat "$raw_ip_path") + if [ "$raw_ip" != Y ]; then + echo "enabling raw-ip" >&2 + if ! echo Y > "$raw_ip_path"; then + echo "error: failed to enable raw-ip" >&2 + exit 1 + fi + fi + + operating_mode=$(uqmi --get-device-operating-mode | tr -d \") + case $operating_mode in + online) + : + ;; + persistent_low_power|low_power) + echo "settings device operating mode to online" >&2 + uqmi --set-device-operating-mode online + operating_mode=$(uqmi --get-device-operating-mode | tr -d \") + if test "$operating_mode" != online; then + echo "error: failed to set device operating mode to online" >&2 + exit 1 + fi + ;; + *) + echo "error: don't know how to change device operating mode to online: $operating_mode" >&2 + exit 1 + esac + + ip link set dev "$interface" up + + data_status=$(uqmi --get-data-status | tr -d \") + case $data_status in + connected) + : + ;; + disconnected) + echo "starting network (APN=${cfg.apn})" >&2 + sleep 1 + uqmi \ + --start-network \ + --autoconnect \ + --apn ${cfg.apn} \ + --ip-family ipv4 + sleep 1 + ;; + *) + echo "error: unsupported data status: $data_status" >&2 + exit 1 + esac + + udhcpc -q -f -n -i "$interface" + ''; + Restart = "on-failure"; + ExecStop = pkgs.writeDash "tv.wwan.stop.sh" '' + set -efu + + interface=$(get-interface) + + ip addr flush "$interface" + ip link set dev "$interface" down + uqmi --stop-network 0xFFFFFFFF --autoconnect + uqmi --sync + uqmi --set-device-operating-mode persistent_low_power + ''; + }; + }; + users.users.root.packages = [ + pkgs.uqmi-wrapper + ]; + tv.systemd.services.wwan.operators = cfg.operators; + }; +} diff --git a/modules/x0vncserver.nix b/modules/x0vncserver.nix new file mode 100644 index 0000000..d24c2d0 --- /dev/null +++ b/modules/x0vncserver.nix @@ -0,0 +1,44 @@ +{ config, lib, mylib, pkgs, ... }: let + cfg = config.tv.x0vncserver; +in { + options.tv.x0vncserver = { + display = lib.mkOption { + default = ":${toString config.services.xserver.display}"; + type = lib.types.str; + }; + enable = lib.mkEnableOption "tv.x0vncserver"; + pwfile = lib.mkOption { + default = "${config.krebs.secret.directory}/vncpasswd"; + description = '' + Use vncpasswd to edit pwfile. + See: nix-shell -p tigervnc --run 'man vncpasswd' + ''; + type = mylib.types.absolute-pathname; + }; + rfbport = lib.mkOption { + default = 5900; + type = lib.types.int; + }; + user = lib.mkOption { + default = config.krebs.build.user; + type = mylib.types.user; + }; + }; + config = lib.mkIf cfg.enable { + krebs.systemd.services.x0vncserver.restartIfCredentialsChange = true; + systemd.services.x0vncserver = { + after = [ "graphical.target" ]; + requires = [ "graphical.target" ]; + serviceConfig = { + ExecStart = "${pkgs.tigervnc}/bin/x0vncserver ${toString [ + "-display ${cfg.display}" + "-passwordfile \${CREDENTIALS_DIRECTORY}/pwfile" + "-rfbport ${toString cfg.rfbport}" + ]}"; + LoadCredential = "ssh_key:${cfg.pwfile}"; + User = cfg.user.name; + }; + }; + tv.iptables.input-retiolum-accept-tcp = [ (toString cfg.rfbport) ]; + }; +} -- cgit v1.2.3