From d213df5c00d3073d2f3bc09471fce466153df881 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 11 Jul 2015 16:55:22 +0200 Subject: NWO --- 3modules/tv/consul.nix | 122 ++++++++++++++ 3modules/tv/ejabberd.nix | 171 ++++++++++++++++++++ 3modules/tv/git.nix | 406 +++++++++++++++++++++++++++++++++++++++++++++++ 3modules/tv/identity.nix | 71 +++++++++ 3modules/tv/iptables.nix | 129 +++++++++++++++ 3modules/tv/nginx.nix | 83 ++++++++++ 3modules/tv/retiolum.nix | 241 ++++++++++++++++++++++++++++ 3modules/tv/urlwatch.nix | 156 ++++++++++++++++++ 8 files changed, 1379 insertions(+) create mode 100644 3modules/tv/consul.nix create mode 100644 3modules/tv/ejabberd.nix create mode 100644 3modules/tv/git.nix create mode 100644 3modules/tv/identity.nix create mode 100644 3modules/tv/iptables.nix create mode 100644 3modules/tv/nginx.nix create mode 100644 3modules/tv/retiolum.nix create mode 100644 3modules/tv/urlwatch.nix (limited to '3modules') diff --git a/3modules/tv/consul.nix b/3modules/tv/consul.nix new file mode 100644 index 00000000..dc9a180d --- /dev/null +++ b/3modules/tv/consul.nix @@ -0,0 +1,122 @@ +{ config, lib, pkgs, ... }: + +# if quorum gets lost, then start any node with a config that doesn't contain bootstrap_expect +# but -bootstrap +# TODO consul-bootstrap HOST that actually does is +# TODO tools to inspect state of a cluster in outage state + +with builtins; +with lib; +let + cfg = config.tv.consul; + + out = { + imports = [ ../../3modules/tv/iptables.nix ]; + options.tv.consul = api; + config = mkIf cfg.enable (mkMerge [ + imp + { tv.iptables.input-retiolum-accept-new-tcp = [ "8300" "8301" ]; } + # TODO udp for 8301 + ]); + }; + + api = { + # TODO inherit (lib) api.options.enable; oder so + enable = mkOption { + type = types.bool; + default = false; + description = "enable tv.consul"; + }; + dc = mkOption { + type = types.unspecified; + }; + hosts = mkOption { + type = with types; listOf unspecified; + }; + encrypt-file = mkOption { + type = types.str; # TODO path (but not just into store) + default = "/root/src/secrets/consul-encrypt.json"; + }; + data-dir = mkOption { + type = types.str; # TODO path (but not just into store) + default = "/var/lib/consul"; + }; + self = mkOption { + type = types.unspecified; + }; + server = mkOption { + type = types.bool; + default = false; + }; + GOMAXPROCS = mkOption { + type = types.int; + default = cfg.self.cores; + }; + }; + + consul-config = { + datacenter = cfg.dc; + data_dir = cfg.data-dir; + log_level = "INFO"; + #node_name = + server = cfg.server; + bind_addr = cfg.self.addr; # TODO cfg.addr + enable_syslog = true; + retry_join = map (getAttr "addr") (filter (host: host.fqdn != cfg.self.fqdn) cfg.hosts); + leave_on_terminate = true; + } // optionalAttrs cfg.server { + bootstrap_expect = length cfg.hosts; + leave_on_terminate = false; + }; + + imp = { + environment.systemPackages = with pkgs; [ + consul + ]; + + systemd.services.consul = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ + consul + ]; + environment = { + GOMAXPROCS = toString cfg.GOMAXPROCS; + }; + serviceConfig = { + PermissionsStartOnly = "true"; + SyslogIdentifier = "consul"; + User = user.name; + PrivateTmp = "true"; + Restart = "always"; + ExecStartPre = pkgs.writeScript "consul-init" '' + #! /bin/sh + mkdir -p ${cfg.data-dir} + chown consul: ${cfg.data-dir} + install -o ${user.name} -m 0400 ${cfg.encrypt-file} /tmp/encrypt.json + ''; + ExecStart = pkgs.writeScript "consul-service" '' + #! /bin/sh + set -euf + exec >/dev/null + exec consul agent \ + -config-file=${toFile "consul.json" (toJSON consul-config)} \ + -config-file=/tmp/encrypt.json + ''; + #-node=${cfg.self.fqdn} \ + #ExecStart = "${tinc}/sbin/tincd -c ${confDir} -d 0 -U ${user} -D"; + }; + }; + + users.extraUsers = singleton { + inherit (user) name uid; + }; + }; + + user = { + name = "consul"; + uid = 2983239726; # genid consul + }; + +in +out diff --git a/3modules/tv/ejabberd.nix b/3modules/tv/ejabberd.nix new file mode 100644 index 00000000..ffcc5cfb --- /dev/null +++ b/3modules/tv/ejabberd.nix @@ -0,0 +1,171 @@ +{ config, lib, pkgs, ... }: + +with builtins; +with lib; +let + cfg = config.tv.ejabberd; + + out = { + options.tv.ejabberd = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkOption { + type = types.bool; + default = false; + }; + + certFile = mkOption { + type = types.str; + default = "/root/src/secrets/ejabberd.pem"; + }; + + hosts = mkOption { + type = with types; listOf str; + }; + }; + + imp = { + environment.systemPackages = [ my-ejabberdctl ]; + + systemd.services.ejabberd = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = "yes"; + PermissionsStartOnly = "true"; + SyslogIdentifier = "ejabberd"; + User = user.name; + PrivateTmp = "true"; + ExecStartPre = pkgs.writeScript "ejabberd-start" '' + #! /bin/sh + install -o ${user.name} -m 0400 ${cfg.certFile} /tmp/certfile.pem + ''; + ExecStart = pkgs.writeScript "ejabberd-service" '' + #! /bin/sh + ${my-ejabberdctl}/bin/ejabberdctl start + ''; + }; + }; + + users.extraUsers = singleton { + inherit (user) name uid; + home = "/var/ejabberd"; + createHome = true; + }; + }; + + user = { + name = "ejabberd"; + uid = 405222; + # TODO uid = 3483034447; # genid ejabberd + }; + + my-ejabberdctl = pkgs.writeScriptBin "ejabberdctl" '' + #! /bin/sh + set -euf + exec env \ + SPOOLDIR=/var/ejabberd \ + EJABBERD_CONFIG_PATH=${config-file} \ + ${pkgs.ejabberd}/bin/ejabberdctl \ + --logs /var/ejabberd \ + "$@" + ''; + + config-file = pkgs.writeText "ejabberd.cfg" '' + {loglevel, 3}. + {hosts, ${toErlang cfg.hosts}}. + {listen, + [ + {5222, ejabberd_c2s, [ + starttls, + {certfile, "/tmp/certfile.pem"}, + {access, c2s}, + {shaper, c2s_shaper}, + {max_stanza_size, 65536} + ]}, + {5269, ejabberd_s2s_in, [ + {shaper, s2s_shaper}, + {max_stanza_size, 131072} + ]}, + {5280, ejabberd_http, [ + captcha, + http_bind, + http_poll, + web_admin + ]} + ]}. + {s2s_use_starttls, required}. + {s2s_certfile, "/tmp/certfile.pem"}. + {auth_method, internal}. + {shaper, normal, {maxrate, 1000}}. + {shaper, fast, {maxrate, 50000}}. + {max_fsm_queue, 1000}. + {acl, local, {user_regexp, ""}}. + {access, max_user_sessions, [{10, all}]}. + {access, max_user_offline_messages, [{5000, admin}, {100, all}]}. + {access, local, [{allow, local}]}. + {access, c2s, [{deny, blocked}, + {allow, all}]}. + {access, c2s_shaper, [{none, admin}, + {normal, all}]}. + {access, s2s_shaper, [{fast, all}]}. + {access, announce, [{allow, admin}]}. + {access, configure, [{allow, admin}]}. + {access, muc_admin, [{allow, admin}]}. + {access, muc_create, [{allow, local}]}. + {access, muc, [{allow, all}]}. + {access, pubsub_createnode, [{allow, local}]}. + {access, register, [{allow, all}]}. + {language, "en"}. + {modules, + [ + {mod_adhoc, []}, + {mod_announce, [{access, announce}]}, + {mod_blocking,[]}, + {mod_caps, []}, + {mod_configure,[]}, + {mod_disco, []}, + {mod_irc, []}, + {mod_http_bind, []}, + {mod_last, []}, + {mod_muc, [ + {access, muc}, + {access_create, muc_create}, + {access_persistent, muc_create}, + {access_admin, muc_admin} + ]}, + {mod_offline, [{access_max_user_messages, max_user_offline_messages}]}, + {mod_ping, []}, + {mod_privacy, []}, + {mod_private, []}, + {mod_pubsub, [ + {access_createnode, pubsub_createnode}, + {ignore_pep_from_offline, true}, + {last_item_cache, false}, + {plugins, ["flat", "hometree", "pep"]} + ]}, + {mod_register, [ + {welcome_message, {"Welcome!", + "Hi.\nWelcome to this XMPP server."}}, + {ip_access, [{allow, "127.0.0.0/8"}, + {deny, "0.0.0.0/0"}]}, + {access, register} + ]}, + {mod_roster, []}, + {mod_shared_roster,[]}, + {mod_stats, []}, + {mod_time, []}, + {mod_vcard, []}, + {mod_version, []} + ]}. + ''; + + + # XXX this is a placeholder that happens to work the default strings. + toErlang = builtins.toJSON; + +in +out diff --git a/3modules/tv/git.nix b/3modules/tv/git.nix new file mode 100644 index 00000000..4ab0df25 --- /dev/null +++ b/3modules/tv/git.nix @@ -0,0 +1,406 @@ +arg@{ config, pkgs, lib, ... }: + +with builtins; +with lib; +let + cfg = config.tv.git; + + out = { + imports = [ + ../../3modules/tv/nginx.nix + ]; + options.tv.git = api; + config = mkIf cfg.enable (mkMerge [ + imp + ]); + }; + + api = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable Git repository hosting."; + }; + cgit = mkOption { + type = types.bool; + default = true; + description = "Enable cgit."; # TODO better desc; talk about nginx + }; + dataDir = mkOption { + type = types.str; + default = "/var/lib/git"; + description = "Directory used to store repositories."; + }; + etcDir = mkOption { + type = types.str; + default = "/etc/git"; + }; + rules = mkOption { + type = types.unspecified; + }; + repos = mkOption { + type = types.attrsOf (types.submodule ({ + options = { + desc = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Repository description. + ''; + }; + section = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Repository section. + ''; + }; + name = mkOption { + type = types.str; + description = '' + Repository name. + ''; + }; + hooks = mkOption { + type = types.attrsOf types.str; + description = '' + Repository-specific hooks. + ''; + }; + public = mkOption { + type = types.bool; + default = false; + description = '' + Allow everybody to read the repository via HTTP if cgit enabled. + ''; + # TODO allow every configured user to fetch the repository via SSH. + }; + }; + })); + + default = {}; + + example = literalExample '' + { + testing = { + name = "testing"; + hooks.post-update = ''' + #! /bin/sh + set -euf + echo post-update hook: $* >&2 + '''; + }; + testing2 = { name = "testing2"; }; + } + ''; + + description = '' + Repositories. + ''; + }; + users = mkOption { + type = types.unspecified; + }; + }; + + imp = { + system.activationScripts.git-init = "${init-script}"; + + # TODO maybe put all scripts here and then use PATH? + environment.etc."${etc-base}".source = + scriptFarm "git-ssh-authorizers" { + authorize-command = makeAuthorizeScript (map ({ repo, user, perm }: [ + (map getName (ensureList user)) + (map getName (ensureList repo)) + (map getName perm.allow-commands) + ]) cfg.rules); + + authorize-push = makeAuthorizeScript (map ({ repo, user, perm }: [ + (map getName (ensureList user)) + (map getName (ensureList repo)) + (ensureList perm.allow-receive-ref) + (map getName perm.allow-receive-modes) + ]) (filter (x: hasAttr "allow-receive-ref" x.perm) cfg.rules)); + }; + + users.extraUsers = singleton { + description = "Git repository hosting user"; + name = "git"; + shell = "/bin/sh"; + openssh.authorizedKeys.keys = + mapAttrsToList (_: makeAuthorizedKey git-ssh-command) cfg.users; + uid = 112606723; # genid git + }; + }; + + + ensureList = x: + if typeOf x == "list" then x else [x]; + + getName = x: x.name; + + isPublicRepo = getAttr "public"; # TODO this is also in ./cgit.nix + + makeAuthorizedKey = git-ssh-command: user@{ name, pubkey }: + # TODO assert name + # TODO assert pubkey + let + options = concatStringsSep "," [ + ''command="exec ${git-ssh-command} ${name}"'' + "no-agent-forwarding" + "no-port-forwarding" + "no-pty" + "no-X11-forwarding" + ]; + in + "${options} ${pubkey}"; + + # [case-pattern] -> shell-script + # Create a shell script that succeeds (exit 0) when all its arguments + # match the case patterns (in the given order). + makeAuthorizeScript = + let + # TODO escape + to-pattern = x: concatStringsSep "|" (ensureList x); + go = i: ps: + if ps == [] + then "exit 0" + else '' + case ''$${toString i} in ${to-pattern (head ps)}) + ${go (i + 1) (tail ps)} + esac''; + in + patterns: '' + #! /bin/sh + set -euf + ${concatStringsSep "\n" (map (go 1) patterns)} + exit -1 + ''; + + reponames = rules: sort lessThan (unique (map (x: x.repo.name) rules)); + + # TODO makeGitHooks that uses runCommand instead of scriptFarm? + scriptFarm = + farm-name: scripts: + let + makeScript = script-name: script-string: { + name = script-name; + path = pkgs.writeScript "${farm-name}_${script-name}" script-string; + }; + in + pkgs.linkFarm farm-name (mapAttrsToList makeScript scripts); + + + git-ssh-command = pkgs.writeScript "git-ssh-command" '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + git + gnugrep + gnused + systemd + ])} + + abort() { + echo "error: $1" >&2 + systemd-cat -p err -t git echo "error: $1" + exit -1 + } + + GIT_SSH_USER=$1 + + systemd-cat -p info -t git echo \ + "authorizing $GIT_SSH_USER $SSH_CONNECTION $SSH_ORIGINAL_COMMAND" + + # References: The Base Definitions volume of + # POSIX.1‐2013, Section 3.278, Portable Filename Character Set + portable_filename_bre="^[A-Za-z0-9._-]\\+$" + + command=$(echo "$SSH_ORIGINAL_COMMAND" \ + | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\1/p' \ + | grep "$portable_filename_bre" \ + || abort 'cannot read command') + + GIT_SSH_REPO=$(echo "$SSH_ORIGINAL_COMMAND" \ + | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\2/p' \ + | grep "$portable_filename_bre" \ + || abort 'cannot read reponame') + + ${cfg.etcDir}/authorize-command \ + "$GIT_SSH_USER" "$GIT_SSH_REPO" "$command" \ + || abort 'access denied' + + repodir=${escapeShellArg cfg.dataDir}/$GIT_SSH_REPO + + systemd-cat -p info -t git \ + echo "authorized exec $command $repodir" + + export GIT_SSH_USER + export GIT_SSH_REPO + exec "$command" "$repodir" + ''; + + init-script = pkgs.writeScript "git-init" '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + findutils + gawk + git + gnugrep + gnused + ])} + + dataDir=${escapeShellArg cfg.dataDir} + mkdir -p "$dataDir" + + # Notice how the presence of hooks symlinks determine whether + # we manage a repositry or not. + + # Make sure that no existing repository has hooks. We can delete + # symlinks because we assume we created them. + find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks -type l -delete + bad_hooks=$(find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks) + if echo "$bad_hooks" | grep -q .; then + printf 'error: unknown hooks:\n%s\n' \ + "$(echo "$bad_hooks" | sed 's/^/ /')" \ + >&2 + exit -1 + fi + + # Initialize repositories. + ${concatMapStringsSep "\n" (repo: + let + hooks = scriptFarm "git-hooks" (makeHooks repo); + in + '' + reponame=${escapeShellArg repo.name} + repodir=$dataDir/$reponame + mode=${toString (if isPublicRepo repo then 0711 else 0700)} + if ! test -d "$repodir"; then + mkdir -m "$mode" "$repodir" + git init --bare --template=/var/empty "$repodir" + chown -R git:nogroup "$repodir" + fi + ln -s ${hooks} "$repodir/hooks" + '' + ) (attrValues cfg.repos)} + + # Warn about repositories that exist but aren't mentioned in the + # current configuration (and thus didn't receive a hooks symlink). + unknown_repos=$(find "$dataDir" -mindepth 1 -maxdepth 1 \ + -type d \! -exec test -e '{}/hooks' \; -print) + if echo "$unknown_repos" | grep -q .; then + printf 'warning: stale repositories:\n%s\n' \ + "$(echo "$unknown_repos" | sed 's/^/ /')" \ + >&2 + fi + ''; + + makeHooks = repo: removeAttrs repo.hooks [ "pre-receive" ] // { + pre-receive = '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils # env + git + systemd + ])} + + accept() { + #systemd-cat -p info -t git echo "authorized $1" + accept_string="''${accept_string+$accept_string + }authorized $1" + } + reject() { + #systemd-cat -p err -t git echo "denied $1" + #echo 'access denied' >&2 + #exit_code=-1 + reject_string="''${reject_string+$reject_string + }access denied: $1" + } + + empty=0000000000000000000000000000000000000000 + + accept_string= + reject_string= + while read oldrev newrev ref; do + + if [ $oldrev = $empty ]; then + receive_mode=create + elif [ $newrev = $empty ]; then + receive_mode=delete + elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then + receive_mode=fast-forward + else + receive_mode=non-fast-forward + fi + + if ${cfg.etcDir}/authorize-push \ + "$GIT_SSH_USER" "$GIT_SSH_REPO" "$ref" "$receive_mode"; then + accept "$receive_mode $ref" + else + reject "$receive_mode $ref" + fi + done + + if [ -n "$reject_string" ]; then + systemd-cat -p err -t git echo "$reject_string" + exit -1 + fi + + systemd-cat -p info -t git echo "$accept_string" + + ${optionalString (hasAttr "post-receive" repo.hooks) '' + # custom post-receive hook + ${repo.hooks.post-receive}''} + ''; + }; + + etc-base = + assert (hasPrefix "/etc/" cfg.etcDir); + removePrefix "/etc/" cfg.etcDir; + +in +out + + + + + + + + + + + +#let +# inherit (lib) mkIf mkMerge; +# +# cfg = config.tv.git; +# arg' = arg // { inherit cfg; }; +#in +# +## TODO unify logging of shell scripts to user and journal +## TODO move all scripts to ${etcDir}, so ControlMaster connections +## immediately pick up new authenticators +## TODO when authorized_keys changes, then restart ssh +## (or kill already connected users somehow) +# +#{ +# imports = [ +# ../../3modules/tv/nginx.nix +# ]; +# +# options.tv.git = import ./options.nix arg'; +# +# config = mkIf cfg.enable (mkMerge [ +# (import ./config.nix arg') +# (mkIf cfg.cgit (import ./cgit.nix arg')) +# ]); +#} diff --git a/3modules/tv/identity.nix b/3modules/tv/identity.nix new file mode 100644 index 00000000..7cf90de0 --- /dev/null +++ b/3modules/tv/identity.nix @@ -0,0 +1,71 @@ +{ lib, ... }: + +with lib; + +let + + cfg = config.tv.identity; + + out = { + options.tv.identity = api; + #config = mkIf cfg.enable imp; + }; + + api = { + enable = mkOption { + type = types.bool; + default = false; + }; + self = mkOption { + type = types.unspecified; + }; + hosts = mkOption { + type = with types; attrsOf unspecified; + default = { + cd = { + #dc = "cac"; + dc = "tv"; + fqdn = "cd.retiolum"; + addr = "10.243.113.222"; + #addr6 = "42:4522:25f8:36bb:8ccb:0150:231a:2af3"; + #internet-addr = "162.219.5.183"; + cores = 2; + }; + mkdir = { + #dc = "cac"; + dc = "tv"; + fqdn = "mkdir.retiolum"; + addr = "10.243.113.223"; + cores = 1; + }; + nomic = { + #dc = "gg"; + dc = "tv"; + fqdn = "nomic.retiolum"; + addr = "10.243.0.110"; + cores = 2; + }; + rmdir = { + #dc = "cac"; + dc = "tv"; + fqdn = "rmdir.retiolum"; + addr = "10.243.113.224"; + #addr = "42:4522:25f8:36bb:8ccb:0150:231a:2af5"; + cores = 1; + }; + wu = { + #dc = "gg"; + dc = "tv"; + fqdn = "wu.retiolum"; + addr = "10.243.13.37"; + cores = 8; + }; + }; + }; + }; + + #imp = { + #}; + +in +out diff --git a/3modules/tv/iptables.nix b/3modules/tv/iptables.nix new file mode 100644 index 00000000..096b04e1 --- /dev/null +++ b/3modules/tv/iptables.nix @@ -0,0 +1,129 @@ +{ config, lib, pkgs, ... }: + +with builtins; +with lib; +let + cfg = config.tv.iptables; + + out = { + options.tv.iptables = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkOption { + type = types.bool; + default = false; + }; + + input-internet-accept-new-tcp = mkOption { + type = with types; listOf str; + default = []; + }; + + input-retiolum-accept-new-tcp = mkOption { + type = with types; listOf str; + default = []; + }; + }; + + imp = { + networking.firewall.enable = false; + + systemd.services.tv-iptables = { + description = "tv-iptables"; + wantedBy = [ "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"; + ExecStart = "@${startScript} tv-iptables_start"; + }; + }; + }; + + + accept-new-tcp = port: + "-p tcp -m tcp --dport ${port} -m conntrack --ctstate NEW -j ACCEPT"; + + rules = iptables-version: + 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] + ${concatMapStringsSep "\n" (rule: "-A PREROUTING ${rule}") ([] + ++ [ + "! -i retiolum -p tcp -m tcp --dport 22 -j REDIRECT --to-ports 0" + "-p tcp -m tcp --dport 11423 -j REDIRECT --to-ports 22" + ] + )} + COMMIT + *filter + :INPUT DROP [0:0] + :FORWARD DROP [0:0] + :OUTPUT ACCEPT [0:0] + :Retiolum - [0:0] + ${concatMapStringsSep "\n" (rule: "-A INPUT ${rule}") ([] + ++ [ + "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT" + "-i lo -j ACCEPT" + ] + ++ map accept-new-tcp cfg.input-internet-accept-new-tcp + ++ ["-i retiolum -j Retiolum"] + )} + ${concatMapStringsSep "\n" (rule: "-A Retiolum ${rule}") ([] + ++ { + 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" + ++ map accept-new-tcp cfg.input-retiolum-accept-new-tcp + ++ { + 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 + ''; + + startScript = pkgs.writeScript "tv-iptables_start" '' + #! /bin/sh + set -euf + iptables-restore < ${rules 4} + ip6tables-restore < ${rules 6} + ''; + +in +out + +#let +# cfg = config.tv.iptables; +# arg' = arg // { inherit cfg; }; +#in +# +#{ +# options.tv.iptables = import ./options.nix arg'; +# config = lib.mkIf cfg.enable (import ./config.nix arg'); +#} diff --git a/3modules/tv/nginx.nix b/3modules/tv/nginx.nix new file mode 100644 index 00000000..d9d95be8 --- /dev/null +++ b/3modules/tv/nginx.nix @@ -0,0 +1,83 @@ +{ config, pkgs, lib, ... }: + +with builtins; +with lib; +let + cfg = config.tv.nginx; + + out = { + options.tv.nginx = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable nginx."; + }; + + retiolum-locations = mkOption { + type = with types; listOf (attrsOf str); + default = []; + }; + }; + + imp = { + services.nginx = + let + name = config.tv.retiolum.name; + qname = "${name}.retiolum"; + in + assert config.tv.retiolum.enable; + { + enable = true; + httpConfig = '' + include ${pkgs.nginx}/conf/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + gzip on; + server { + listen 80 default_server; + server_name _; + location / { + return 404; + } + } + server { + listen 80; + server_name ${name} ${qname}; + + ${indent (concatStrings (map to-location cfg.retiolum-locations))} + + location / { + return 404; + } + } + ''; + }; + }; + + + indent = replaceChars ["\n"] ["\n "]; + + to-location = { name, value }: '' + location ${name} { + ${indent value} + } + ''; + +in +out + + +#let +# cfg = config.tv.nginx; +# arg' = arg // { inherit cfg; }; +#in +# +#{ +# options.tv.nginx = import ./options.nix arg'; +# config = lib.mkIf cfg.enable (import ./config.nix arg'); +#} diff --git a/3modules/tv/retiolum.nix b/3modules/tv/retiolum.nix new file mode 100644 index 00000000..7c78d712 --- /dev/null +++ b/3modules/tv/retiolum.nix @@ -0,0 +1,241 @@ +{ config, pkgs, lib, ... }: + +with builtins; +with lib; +let + cfg = config.tv.retiolum; + + out = { + options.tv.retiolum = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable tinc daemon for Retiolum."; + }; + + name = mkOption { + type = types.str; + default = config.networking.hostName; + # Description stolen from tinc.conf(5). + description = '' + This is the name which identifies this tinc daemon. It must + be unique for the virtual private network this daemon will + connect to. The Name may only consist of alphanumeric and + underscore characters. If Name starts with a $, then the + contents of the environment variable that follows will be + used. In that case, invalid characters will be converted to + underscores. If Name is $HOST, but no such environment + variable exist, the hostname will be read using the + gethostnname() system call This is the name which identifies + the this tinc daemon. + ''; + }; + + generateEtcHosts = mkOption { + type = types.str; + default = "both"; + description = '' + If set to short, long, or both, + then generate entries in /etc/hosts from subnets. + ''; + }; + + network = mkOption { + type = types.str; + default = "retiolum"; + description = '' + The tinc network name. + It is used to generate long host entries, + derive the name of the user account under which tincd runs, + and name the TUN device. + ''; + }; + + tincPackage = mkOption { + type = types.package; + default = pkgs.tinc; + description = "Tincd package to use."; + }; + + hosts = mkOption { + default = null; + description = '' + Hosts package or path to use. + If a path is given, then it will be used to generate an ad-hoc package. + ''; + }; + + iproutePackage = mkOption { + type = types.package; + default = pkgs.iproute; + description = "Iproute2 package to use."; + }; + + + privateKeyFile = mkOption { + # TODO if it's types.path then it gets copied to /nix/store with + # bad unsafe permissions... + type = types.str; + default = "/root/src/secrets/retiolum.rsa_key.priv"; + description = "Generate file with tincd -K."; + }; + + connectTo = mkOption { + type = types.listOf types.str; + default = [ "fastpoke" "pigstarter" "kheurop" ]; + description = "TODO describe me"; + }; + + }; + + imp = { + environment.systemPackages = [ tinc hosts iproute ]; + + networking.extraHosts = retiolumExtraHosts; + + systemd.services.retiolum = { + description = "Tinc daemon for Retiolum"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ tinc iproute ]; + serviceConfig = { + PermissionsStartOnly = "true"; + PrivateTmp = "true"; + Restart = "always"; + # TODO we cannot chroot (-R) b/c we use symlinks to hosts + # and the private key. + ExecStartPre = pkgs.writeScript "retiolum-init" '' + #! /bin/sh + install -o ${user} -m 0400 ${cfg.privateKeyFile} /tmp/retiolum-rsa_key.priv + ''; + ExecStart = "${tinc}/sbin/tincd -c ${confDir} -d 0 -U ${user} -D"; + SyslogIdentifier = "retiolum"; + }; + }; + + # TODO user.name = "retiolum" + users.extraUsers = singleton { + name = user; + uid = 2961822815; # bin/genid retiolum-tinc + }; + }; + + + tinc = cfg.tincPackage; + hostsType = builtins.typeOf cfg.hosts; + hosts = + if hostsType == "package" then + # use package as is + cfg.hosts + else if hostsType == "path" then + # use path to generate a package + pkgs.stdenv.mkDerivation { + name = "custom-retiolum-hosts"; + src = cfg.hosts; + installPhase = '' + mkdir $out + find . -name .git -prune -o -type f -print0 | xargs -0 cp --target-directory $out + ''; + } + else + abort "The option `services.retiolum.hosts' must be set to a package or a path" + ; + iproute = cfg.iproutePackage; + + retiolumExtraHosts = import (pkgs.runCommand "retiolum-etc-hosts" + { } + '' + generate() { + (cd ${hosts} + printf \'\' + for i in `ls`; do + names=$(hostnames $i) + for j in `sed -En 's|^ *Aliases *= *(.+)|\1|p' $i`; do + names="$names $(hostnames $j)" + done + sed -En ' + s|^ *Subnet *= *([^ /]*)(/[0-9]*)? *$|\1 '"$names"'|p + ' $i + done | sort + printf \'\' + ) + } + + case ${cfg.generateEtcHosts} in + short) + hostnames() { echo "$1"; } + generate + ;; + long) + hostnames() { echo "$1.${cfg.network}"; } + generate + ;; + both) + hostnames() { echo "$1.${cfg.network} $1"; } + generate + ;; + *) + echo '""' + ;; + esac > $out + ''); + + + confDir = pkgs.runCommand "retiolum" { + # TODO text + executable = true; + preferLocalBuild = true; + } '' + set -euf + + mkdir -p $out + + ln -s ${hosts} $out/hosts + + cat > $out/tinc.conf < $out/tinc-up < changes 2>&1 || : + + if test -s changes; then + date=$(date -R) + subject=$(sed -n 's/^\(CHANGED\|ERROR\|NEW\): //p' changes \ + | tr \\n \ ) + { + echo "Date: $date" + echo "From: $from" + echo "Subject: $subject" + echo "To: $mailto" + echo + cat changes + } | /var/setuid-wrappers/sendmail -t + fi + ''; + }; + }; + users.extraUsers = optionals (cfg.user == "urlwatch") (singleton { + name = "urlwatch"; + uid = 3450919516; # bin/genid urlwatch + }); + }; + +in + +{ + # TODO + #imports = [ + # ./exim + #]; + #config = mkIf cfg.enable + # (if config.tv.exim.enable + # then impl + # else throw "tv.exim must be enabled when enabling tv.urlwatch"); + + options.tv.urlwatch = api; + + config = impl; +} -- cgit v1.2.3