From d4d8093f97f11dbd398f4fbd758cac6de2e11aaf Mon Sep 17 00:00:00 2001
From: Daniel Barlow <dan@telent.net>
Date: Thu, 20 Jun 2024 10:15:54 +0100
Subject: [PATCH] working l2tp-over-wwan stick example

---
 examples/l2tp.nix            | 90 +++++++++++++++++++++++++++---------
 modules/network/route.nix    |  4 +-
 pkgs/service-fns/default.nix | 12 +++++
 3 files changed, 84 insertions(+), 22 deletions(-)

diff --git a/examples/l2tp.nix b/examples/l2tp.nix
index 169e62f4..12441b23 100644
--- a/examples/l2tp.nix
+++ b/examples/l2tp.nix
@@ -6,7 +6,26 @@
 }: let
   secrets = import ./extneder-secrets.nix;
   rsecrets = import ./rotuer-secrets.nix;
-  lns = "l2tp.aaisp.net.uk";
+
+  # https://support.aa.net.uk/Category:Incoming_L2TP says:
+  # "Please use the DNS name (l2tp.aa.net.uk) instead of hardcoding an
+  # IP address; IP addresses can and do change. If you have to use an
+  # IP, use 194.4.172.12, but do check the DNS for l2tp.aa.net.uk in
+  # case it changes."
+
+  # but (1) we don't want to use the wwan stick's dns as our main
+  # resolver: it's provided by some mobile ISP and they aren't
+  # necessarily the best at providing unfettered services without
+  # deciding to do something weird; (2) it's not simple to arrange
+  # that xl2tpd gets a different resolver than every other process;
+  # (3) there's no way to specify an lns address to xl2tpd at runtime
+  # except by rewriting its config file. So what we will do is lookup
+  # the lns hostname using the mobile ISP's dns server and then refuse
+  # to start l2tp unless the expected lns address is one of the
+  # addresses returned. I think this satisfies "do check the DNS"
+
+  lns = { hostname = "l2tp.aaisp.net.uk"; address = "194.4.172.12"; };
+
   inherit (pkgs.liminix.services) oneshot longrun bundle target;
   inherit (pkgs.pseudofile) dir symlink;
   inherit (pkgs) writeText dropbear ifwait serviceFns;
@@ -46,46 +65,75 @@ in rec {
   services.sshd = svc.ssh.build { };
 
   services.resolvconf = oneshot rec {
-    dependencies = [ services.dhcpc ];
+    dependencies = [ services.l2tp ];
     name = "resolvconf";
     up = ''
       . ${serviceFns}
-      ( in_outputs ${name}
-      for i in $(output ${services.dhcpc} dns); do
-        echo "nameserver $i" > resolv.conf
-      done
-      )
+       ( in_outputs ${name}
+        for i in ns1 ns2 ; do
+          ns=$(output ${services.l2tp} $i)
+          echo "nameserver $ns" >> resolv.conf
+        done
+       )
     '';
   };
   filesystem = dir {
     etc = dir {
       "resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
     };
-    srv = dir {};
   };
 
-  services.lnsroute = svc.network.route.build {
-    via = "$(output ${services.dhcpc} router)";
-    target = lns;
-    dependencies = [services.dhcpc];
+  services.lns-address = let
+    ns = "$(output_word ${services.dhcpc} dns 1)";
+    route-to-bootstrap-nameserver = svc.network.route.build {
+      via = "$(output ${services.dhcpc} router)";
+      target = ns;
+      dependencies = [services.dhcpc];
+    };
+  in oneshot rec {
+    name = "resolve-l2tp-server";
+    dependencies = [ services.dhcpc route-to-bootstrap-nameserver ];
+    up = ''
+      (in_outputs ${name}
+       DNSCACHEIP="${ns}" ${pkgs.s6-dns}/bin/s6-dnsip4 ${lns.hostname} \
+        > addresses
+      )
+    '';
   };
 
-  services.l2tp = svc.l2tp.build {
-    inherit lns;
-    ppp-options = [
-      "debug" "+ipv6" "noauth"
-      "name" rsecrets.l2tp.name
-      "password" rsecrets.l2tp.password
-    ];
-    dependencies = [ services.lnsroute ];
+  services.l2tp =
+    let
+      check-address = oneshot rec {
+        name = "check-lns-address";
+        up = ''
+          grep -Fx ${lns.address} $(output_path ${services.lns-address} addresses)
+        '';
+        dependencies = [ services.lns-address ];
+      };
+      route = svc.network.route.build {
+        via = "$(output ${services.dhcpc} router)";
+        target = lns.address;
+        dependencies = [services.dhcpc check-address];
+      };
+    in svc.l2tp.build {
+      lns = lns.address;
+      ppp-options = [
+        "debug" "+ipv6" "noauth"
+        "name" rsecrets.l2tp.name
+        "connect-delay" "5000"
+        "password" rsecrets.l2tp.password
+      ];
+      dependencies = [config.services.lns-address route check-address];
   };
 
   services.defaultroute4 = svc.network.route.build {
-    via = "$(output ${services.l2tp} router)";
+    via = "$(output ${services.l2tp} peer-address)";
     target = "default";
     dependencies = [services.l2tp];
   };
 
+#  defaultProfile.packages = [ pkgs.go-l2tp ];
+
   users.root = {
     passwd = lib.mkForce secrets.root.passwd;
     openssh.authorizedKeys.keys = secrets.root.keys;
diff --git a/modules/network/route.nix b/modules/network/route.nix
index 0c933137..6a6644a3 100644
--- a/modules/network/route.nix
+++ b/modules/network/route.nix
@@ -8,8 +8,10 @@
 let
   inherit (liminix.services) oneshot;
   with_dev = if interface != null then "dev $(output ${interface} ifname)" else "";
+  target_hash = builtins.substring 0 12 (builtins.hashString "sha256" target);
+  via_hash = builtins.substring 0 12 (builtins.hashString "sha256" via);
 in oneshot {
-  name = "route-${target}-${builtins.substring 0 12 (builtins.hashString "sha256" "${via}-${if interface!=null then interface.name else ""}")}";
+  name = "route-${target_hash}-${builtins.substring 0 12 (builtins.hashString "sha256" "${via_hash}-${if interface!=null then interface.name else ""}")}";
   up = ''
     ip route add ${target} via ${via} metric ${toString metric} ${with_dev}
   '';
diff --git a/pkgs/service-fns/default.nix b/pkgs/service-fns/default.nix
index 1966396b..b2c1ad8d 100644
--- a/pkgs/service-fns/default.nix
+++ b/pkgs/service-fns/default.nix
@@ -1,6 +1,18 @@
 {writeText}:
 writeText "service-fns.sh" ''
   output() { cat $1/.outputs/$2; }
+  output_word() {
+    set -f
+    local i=1
+    for var in $(cat $1/.outputs/$2); do
+      if test "$i" == "$3" ; then
+        echo $var
+      fi
+      i=$(expr $i + 1)
+    done
+    set +f
+  }
+
   output_path() { echo $(realpath $1/.outputs)/$2; }
   SERVICE_OUTPUTS=/run/services/outputs
   SERVICE_STATE=/run/services/state