diff --git a/.envrc b/.envrc index 3550a30..24a35d2 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,5 @@ use flake + +export RELEASE_NODE=frajtano-test@nomos +export FRAJTANO_DIR=$PWD/.frajtano_state +export SSH_AUTH_SOCK=$FRAJTANO_DIR/agent.sock diff --git a/.gitignore b/.gitignore index 804e0e2..b263742 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ frajtano-*.tar /.nix-hex /.nix-mix /result +/.frajtano_state +/todo diff --git a/config/runtime.exs b/config/runtime.exs index f763b8c..1e1b7d2 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,2 +1,6 @@ import Config -config :frajtano, listen_path: Path.expand(System.get_env("FRAJTANO_DIR")) |> Path.join("agent.sock") +config :frajtano, + listen_path: Path.expand(System.get_env("FRAJTANO_DIR")) |> Path.join("agent.sock"), + initial_peers: System.get_env("FRAJTANO_PEERS", "") |> String.split(":", trim: true) |> Enum.map(&Path.expand/1) + + diff --git a/default.nix b/default.nix index e34944f..2af2dc3 100644 --- a/default.nix +++ b/default.nix @@ -21,6 +21,7 @@ }; script = pkgs.writeShellScriptBin pname '' set -eu + ${pkgs.coreutils}/bin/mkdir -p $FRAJTANO_DIR file="$FRAJTANO_DIR/cookie" (umask 077; [ -f "$file" ] || ${pkgs.coreutils}/bin/head -c 128 /dev/urandom | ${pkgs.coreutils}/bin/base64 -w0 > "$file") export RELEASE_COOKIE=$(${pkgs.coreutils}/bin/cat "$file") @@ -28,10 +29,13 @@ exec ${lib.getExe pkg} "$@" } - case $1 in + case "''${1:-}" in assimilate) run rpc ":ok = \"$(echo -n "$2" | ${pkgs.coreutils}/bin/base64)\" |> Base.decode64!() |> Frajtano.Agent.add_peer()" ;; + socket) + echo $FRAJTANO_DIR/agent.sock + ;; *) run "$@" ;; diff --git a/flake.nix b/flake.nix index 08a0b24..fef79fd 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,11 @@ ownPkgs = self.packages.${system}; }); in { - devShells = eachSystem ({pkgs, ownPkgs, ...}: { + devShells = eachSystem ({ + pkgs, + ownPkgs, + ... + }: { default = pkgs.beam.packages.erlang_26.callPackage ./shell.nix { inherit ownPkgs; }; @@ -23,5 +27,46 @@ packages = eachSystem ({pkgs, ...}: { default = pkgs.beam.packages.erlang_26.callPackage ./default.nix {}; }); + homeModules.default = { + config, + lib, + pkgs, + ... + }: let + cfg = config.bluepython508.frajtano; + in { + options.bluepython508.frajtano = { + enable = lib.mkEnableOption "frajtano"; + dir = lib.mkOption { + description = "directory in which to place the listening socket"; + default = "${config.home.homeDirectory}/.ssh/frajtano"; + }; + }; + + config = lib.mkIf cfg.enable { + home.sessionVariables.FRAJTANO_DIR = cfg.dir; + home.packages = [self.packages.${pkgs.system}.default]; + + systemd.user.services.frajtano = { + Unit.Description = "frajtano"; + Unit.After = ["default.target"]; + Service.Environment = "'FRAJTANO_DIR=${cfg.dir}'"; + Service.ExecStart = "${self.packages.${pkgs.system}.default}/bin/frajtano start"; + Install.WantedBy = ["default.target"]; + }; + + launchd.agents.frajtano = { + enable = true; + config = { + EnvironmentVariables = { + FRAJTANO_DIR = cfg.dir; + }; + ProgramArguments = ["${self.packages.${pkgs.system}.default}/bin/frajtano" "start"]; + RunAtLoad = true; + KeepAlive = true; + }; + }; + }; + }; }; } diff --git a/lib/agent.ex b/lib/agent.ex index a3bc661..3434edb 100644 --- a/lib/agent.ex +++ b/lib/agent.ex @@ -8,10 +8,19 @@ defmodule Frajtano.Agent do @impl true def init(_) do - {:ok, - %{ - keys: %{} - }} + { + :ok, + %{ + keys: %{} + }, + {:continue, :init_peers} + } + end + + @impl true + def handle_continue(:init_peers, state) do + for peer <- Application.fetch_env!(:frajtano, :initial_peers), do: {:ok, _} = Peer.start(peer) + {:noreply, state} end @impl true @@ -26,10 +35,9 @@ defmodule Frajtano.Agent do with {:ok, idents} <- Peer.identities(peer), do: {:ok, {idents, peer}} end) - # Double :ok-wrapping because of Task.async_stream - idents = (for {:ok, {:ok, {idents, peer}}} <- idents, do: {idents, peer}) + idents = for {:ok, {:ok, {idents, peer}}} <- idents, do: {idents, peer} { :reply, @@ -61,7 +69,8 @@ defmodule Frajtano.Agent do end def sign(agent \\ __MODULE__, request) do - GenServer.call(agent, {:sign, request}) + # Signing can take some time, as a password may need to be entered or similar + GenServer.call(agent, {:sign, request}, :infinity) end def add_peer(agent \\ __MODULE__, path) do diff --git a/lib/frajtano.ex b/lib/frajtano.ex index 88d1233..c42464e 100644 --- a/lib/frajtano.ex +++ b/lib/frajtano.ex @@ -17,10 +17,10 @@ defmodule Frajtano.Supervisor do @impl true def init(:ok) do children = [ + {DynamicSupervisor, name: Frajtano.Peer}, Frajtano.Agent, - {Frajtano.Listener, [Application.fetch_env!(:frajtano, :listen_path)]}, {Task.Supervisor, name: Frajtano.ClientSupervisor}, - {DynamicSupervisor, name: Frajtano.Peer} + {Frajtano.Listener, [Application.fetch_env!(:frajtano, :listen_path)]}, ] Supervisor.init(children, strategy: :one_for_one) diff --git a/lib/peer.ex b/lib/peer.ex index f01ff71..6208096 100644 --- a/lib/peer.ex +++ b/lib/peer.ex @@ -17,18 +17,8 @@ defmodule Frajtano.Peer do {:ok, %{conn: conn, clients: :queue.new(), buffer: <<>>}} end - @impl true - def handle_call(packet, from, %{conn: conn, clients: clients} = state) do - case :gen_tcp.send(conn, Proto.encode(packet)) do - :ok -> - {:noreply, %{state | clients: :queue.in(from, clients)}} - - {:error, :closed} -> - {:noreply, state, {:continue, :closed}} - - {:error, e} -> - raise(e) - end + def reply({client, ref}, msg) do + send(client, {ref, msg}) end defp handle_messages(%{clients: clients, buffer: buffer} = state) do @@ -38,42 +28,85 @@ defmodule Frajtano.Peer do {msg, buffer} -> {{:value, client}, clients} = :queue.out(clients) - GenServer.reply(client, {:ok, msg}) + reply(client, {:ok, msg}) handle_messages(%{state | clients: clients, buffer: buffer}) end end + @impl true + def handle_info({:send, packet, from}, %{conn: conn, clients: clients} = state) do + case :gen_tcp.send(conn, Proto.encode(packet)) do + :ok -> + {:noreply, %{state | clients: :queue.in(from, clients)}} + + {:error, e} -> + {:noreply, state, {:continue, {:error, e}}} + end + end + @impl true def handle_info({:tcp, conn, msg}, %{conn: conn, buffer: buffer} = state) do :inet.setopts(conn, active: :once) buffer = buffer <> msg - handle_messages(%{ state | buffer: buffer }) + handle_messages(%{state | buffer: buffer}) end @impl true def handle_info({:tcp_closed, _}, state) do - {:noreply, state, {:continue, :closed}} + {:noreply, state, {:continue, {:error, :closed}}} end @impl true - def handle_continue(:closed, %{clients: clients}) do + def handle_info(:timeout, state) do + {:noreply, state, {:continue, {:error, :closed}}} + end + + @impl true + def handle_continue({:error, e}, %{clients: clients}) do clients |> :queue.to_list() - |> Enum.each(&GenServer.reply(&1, {:error, :closed})) + |> Enum.each(&reply(&1, {:error, e})) - {:stop, :closed, %{}} + {:stop, {:error, e}, %{}} end def identities(peer) do - with {:ok, {:agent_identities_answer, identities}} <- - GenServer.call(peer, {:agentc_request_identities, nil}), - do: {:ok, identities} + ref = make_ref() + send(peer, {:send, {:agentc_request_identities, nil}, {self(), ref}}) + # Needs to be less than the timeout in Frajtano.Agent.identities on the Task.async_stream call + # That's 5000 by default + timer = Process.send_after(peer, :timeout, 4500) + + receive do + {^ref, msg} -> + Process.cancel_timer(timer) + msg + end + |> case do + {:ok, {:agent_identities_answer, identities}} -> {:ok, identities} + {:ok, {:agent_failure, nil}} -> {:error, :agent_failure} + {:ok, msg} -> raise("Unexpected message #{inspect(msg)}") + {:error, e} -> {:error, e} + end end def sign(peer, request) do - with {:ok, {:agent_sign_response, signature}} <- - GenServer.call(peer, {:agentc_sign_request, request}), - do: {:ok, signature} + # Signing may take some time, as a password may need to be entered or similar + # There is therefore no timeout + # If something requests identities afterwards, it will timeout, which also kills this signature request + # The SSH agent protocol strict ordering leaves fun problems with timeouts, as it turns out + ref = make_ref() + send(peer, {:send, {:agentc_sign_request, request}, {self(), ref}}) + + receive do + {^ref, msg} -> msg + end + |> case do + {:ok, {:agent_sign_response, signature}} -> {:ok, signature} + {:ok, {:agent_failure, nil}} -> {:error, :agent_failure} + {:ok, msg} -> raise("Unexpected message #{inspect(msg)}") + {:error, e} -> {:error, e} + end end end diff --git a/mix.exs b/mix.exs index d9b6c2e..ff47929 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,6 @@ defmodule Frajtano.MixProject do def application do [ extra_applications: [:logger], - env: [listen_path: nil], mod: {Frajtano, []} ] end diff --git a/shell.nix b/shell.nix index 0b1308e..58f1cbd 100644 --- a/shell.nix +++ b/shell.nix @@ -3,13 +3,14 @@ pkgs, ownPkgs, mkShell, + elixir, elixir-ls, inotify-tools, }: mkShell { inputsFrom = [ ownPkgs.default ]; packages = - [elixir-ls] + [elixir elixir-ls] ++ lib.lists.optional (pkgs.system == "x86_64-linux") inotify-tools ++ lib.lists.optionals (pkgs.system == "aarch64-darwin") (with pkgs.darwin.apple_sdk.frameworks; [ CoreFoundation