sshbind 0.1.0

SSHBind is a Rust library that securely binds remote services behind multiple SSH jump hosts to a local socket, enabling seamless access with encrypted credential management, TOTP-based two-factor authentication, and automatic reconnection.
Documentation
{
  pkgs,
  lib,
  ...
}: let
  bobPassword = "bob";
  nodes = {
    user = {...}: {
      users.users.alice = {
        isNormalUser = true;
        useDefaultShell = true;
        description = "alice";
        initialPassword = "alice";
        packages = with pkgs; [
          age
          sops
          sshbind
          wget
          socat
          dig
        ];
      };
    };
    target = {...}: {
      users.users.bob = {
        isNormalUser = true;
        useDefaultShell = true;
        description = "bob";
        initialPassword = bobPassword;
        packages = with pkgs; [
          socat
        ];
      };
      networking.firewall.allowedTCPPorts = [80 8080];
      services = {
        openssh = {
          enable = true;
          settings = {
            PasswordAuthentication = true;
          };
        };
        httpd = {
          enable = true;
          virtualHosts = {
            "default" = {
              listen = [
                {
                  ip = "0.0.0.0";
                  port = 80;
                }
                {
                  ip = "127.0.0.1";
                  port = 8000;
                }
              ];
              documentRoot = "/etc/var/www";
              extraConfig = ''
                <Directory "/etc/var/www">
                  Require all granted
                </Directory>
              '';
            };
          };
        };
      };
      environment.etc = {
        "var/www/index.html".text = ''
          <!DOCTYPE html>
          <html>
          <head><title>My Site</title></head>
          <body><h1>Hello from NixOS!</h1></body>
          </html>
        '';
      };
    };
  };
in
  pkgs.nixosTest {
    name = "Testing CLI usage";
    inherit nodes;

    testScript = ''
      start_all()

      for m in machines:
          m.wait_for_unit("multi-user.target")

      for m in machines:
          m.wait_for_unit("network.target")

      def setup_sops(m: Machine, user: str):
          out = m.succeed(f"su -l {user} -c 'age-keygen -o age.key 2>&1'")
          age_pk = out.split("key: ")[1].strip()

          m.succeed(f"su -l {user} -c 'mkdir -p ~/.config/sops/age/ && mv age.key ~/.config/sops/age/keys.txt'")
          m.succeed(f"su -l {user} -c 'chmod 600 ~/.config/sops/age/keys.txt'")

          creds = "target:22:\n  username: bob\n  password: ${bobPassword}\n"
          creds += "192.168.10.1:22:\n  username: bob\n  password: ${bobPassword}\n"
          with open("secrets.yaml", "w") as f:
              f.write(creds)

          sops_config = f"""keys:\n  - &my_age_keys {age_pk}\n"""
          sops_config += """creation_rules:\n  - path_regex: \\.yaml$\n"""
          sops_config += "    key_groups:\n      - age:\n        - *my_age_keys"
          with open(".sops.yaml", "w") as f:
              f.write(sops_config)

          m.copy_from_host("secrets.yaml", f"/home/{user}/secrets.yaml")
          m.copy_from_host(".sops.yaml", f"/home/{user}/.sops.yaml")
          m.succeed(f"chown {user}:users -R /home/{user}/")
          m.succeed(f"su -l {user} -c 'sops --in-place --encrypt secrets.yaml'")

      # Setup sops for alice
      setup_sops(user, "alice")

      # Testing basic cli usage
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8000 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind unbind -a 127.0.0.1:8000'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind shutdown'")
      user.succeed("su -l alice -c 'sshbind list'")

      # Testing unbinding via shutdown
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8001 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind shutdown'")
      user.succeed("su -l alice -c 'sshbind list'")

      # Testing binding options
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8000 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8001 -r 127.0.0.1:80 -j target:22 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8002 -r 127.0.0.1:8000 -j target:22 -s ~/secrets.yaml'")
      user.succeed("""su -l alice -c 'sshbind bind -a 127.0.0.1:8003 -j target:22 -s ~/secrets.yaml -c "cat" '""")
      user.succeed(r"""su -l alice -c 'sshbind bind -a 127.0.0.1:8080 -r 192.168.1.1:8080 -j target:22 -s ~/secrets.yaml -c "socat -lf /dev/null TCP-LISTEN:8080,bind=0.0.0.0,reuseaddr,fork,crlf SYSTEM:\"echo HTTP/1.1 200 OK; echo Content-Type\\: text/plain; echo Content-Length\\: 3; echo Connection\\: close; echo; echo OK\""'""")
      user.succeed(r"""su -l alice -c 'sshbind bind -a 127.0.0.1:8081 -r 127.0.0.1:8081 -j target:22 -s ~/secrets.yaml -c "socat -lf /dev/null TCP-LISTEN:8081,reuseaddr,fork,crlf SYSTEM:\"echo HTTP/1.1 200 OK; echo Content-Type\\: text/plain; echo Content-Length\\: 3; echo Connection\\: close; echo; echo OK\""'""")
      user.succeed(r"""su -l alice -c 'sshbind bind -a 127.0.0.1:8082 -r target:8082 -j target:22 -s ~/secrets.yaml -c "socat -lf /dev/null TCP-LISTEN:8082,reuseaddr,fork,crlf SYSTEM:\"echo HTTP/1.1 200 OK; echo Content-Type\\: text/plain; echo Content-Length\\: 3; echo Connection\\: close; echo; echo OK\""'""")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind shutdown'")

      # Testing missing binding options
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8000 -s ~/secrets.yaml'")

      # Double binding to the same address should fail
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8001 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8001 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind shutdown'")

      # Testing unbinding
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8000 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8001 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8002 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind unbind --all'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8000 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind bind -a 127.0.0.1:8001 -r target:80 -s ~/secrets.yaml'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind unbind -a 127.0.0.1:8000'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind unbind -a 127.0.0.1:8001'")
      user.succeed("su -l alice -c 'sshbind list'")
      user.succeed("su -l alice -c 'sshbind shutdown'")

      # Testing help commands
      user.succeed("su -l alice -c 'sshbind --help'")
      user.succeed("su -l alice -c 'sshbind help'")
      user.succeed("su -l alice -c 'sshbind help bind'")
      user.succeed("su -l alice -c 'sshbind help unbind'")
      user.succeed("su -l alice -c 'sshbind help list'")
      user.succeed("su -l alice -c 'sshbind help shutdown'")

      user.succeed("su -l alice -c 'sshbind --version'")

      # Testing crashing of daemon
      # user.succeed("su -l alice -c 'kill `pidof sshbind`'")
      # user.succeed("su -l alice -c 'sshbind list'")




      # user.send_chars("alice\n")
      # user.sleep(1)
      # user.send_chars("alice\n")
      # user.send_chars("curl 127.0.0.1:8080\n")
    '';
  }