Compare commits

...

14 Commits

Author SHA1 Message Date
bluepython508
3c779e8fe2 Update dependencies 2024-11-01 16:31:00 +00:00
bluepython508
bdacd9905e Rename Peer to 'Backing Agent' 2024-09-26 13:10:33 +01:00
bluepython508
85bf788307 Deduplicate returned identities 2024-09-26 12:58:46 +01:00
bluepython508
6944ff05e0 Use portable hostname - macos doesn't have /etc/hostname 2024-09-26 12:23:45 +01:00
bluepython508
b0643a760e Add darwin hash for deps 2024-09-26 12:03:07 +01:00
bluepython508
44fa3976ad Fix launchd config: envvars must be strings, not drvs 2024-09-26 11:21:53 +01:00
bluepython508
83e18d7da4 Use raw strings rather than B64, assume paths don't have } 2024-09-26 11:12:29 +01:00
bluepython508
904cb233f3 More principled initial peers 2024-09-26 11:09:35 +01:00
bluepython508
66cb85ec1d Add systemd notification on listener available 2024-09-26 08:47:51 +01:00
bluepython508
4958944cb8 Log spawn info 2024-09-25 07:57:56 +01:00
bluepython508
dd60e5459b Don't crash on unbound variables 2024-09-24 23:22:47 +01:00
bluepython508
25c260c4a5 Remove unused support for initial peers 2024-09-24 22:16:28 +01:00
bluepython508
23d3a97643 Add support for spawning peers, and monitoring the processes 2024-09-24 22:13:31 +01:00
bluepython508
5e65fad33d Set the release node to allow multi-user use 2024-09-24 16:40:28 +01:00
11 changed files with 173 additions and 57 deletions

3
.envrc
View File

@@ -1,4 +1,5 @@
use flake
export RELEASE_NODE=frajtano-test@nomos
export RELEASE_NODE=frajtano-test@$(hostname -s)
export FRAJTANO_DIR=$PWD/.frajtano_state
export FRAJTANO_CONFIG=$PWD/config/config.exs

4
config/config.exs Normal file
View File

@@ -0,0 +1,4 @@
import Config
config :frajtano,
initial_backing_agents: []

View File

@@ -1,6 +1,5 @@
import Config
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)
listen_path: Path.expand(System.get_env("FRAJTANO_DIR")) |> Path.join("agent.sock")

View File

@@ -3,9 +3,10 @@
pkgs,
mixRelease,
elixir,
fetchMixDeps,
}: let
pname = "frajtano";
pkg = mixRelease {
pkg = mixRelease rec {
inherit pname;
version = "0.0.1";
@@ -16,22 +17,48 @@
filter = path: _type: baseNameOf path != "flake.nix" && baseNameOf path != "flake.lock";
};
# Adapted from https://blog.eigenvalue.net/nix-rerunning-fixed-output-derivations/
# deps hash should change any time ./mix.lock changes, and not otherwise
mixFodDeps = let
deps = fetchMixDeps {
pname = "mix-deps-${pname}";
inherit version src;
sha256 = {
"x86_64-linux" = "sha256-/9Ushiv0z1wJL9FwebUF4gem8IOwlUPnQuDBiXV0Ib8=";
"aarch64-darwin" = "sha256-8gFgTV1qYrC3iw5/AyeEOMAYtcEK7wTXie+Z9RDrqUI=";
}.${pkgs.system};
};
hash = builtins.substring 11 32 "${./mix.lock}";
in
deps.overrideAttrs (attrs: {name = "${deps.name}-${hash}";});
ELIXIR_MAKE_CACHE_DIR = "/tmp/.elixir-make-cache";
meta.mainProgram = pname;
};
script = pkgs.writeShellScriptBin pname ''
set -eu
set -e
${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")
[ -z $RELEASE_NODE ] && export RELEASE_NODE="frajtano-$(${pkgs.coreutils}/bin/whoami)@$(${pkgs.toybox}/bin/hostname -s)"
run() {
exec ${lib.getExe pkg} "$@"
}
list() {
for i in "$@"; do
echo "\"$i\", "
done
}
case "''${1:-}" in
assimilate)
run rpc ":ok = \"$(echo -n "$2" | ${pkgs.coreutils}/bin/base64)\" |> Base.decode64!() |> Frajtano.Agent.add_peer()"
run rpc ":ok = ~S{$2} |> Frajtano.Agent.assimilate()"
;;
spawn)
shift
run rpc ":ok = Frajtano.Agent.spawn_backing_agent({\"/usr/bin/env\", [$(list "$@")]})"
;;
socket)
echo $FRAJTANO_DIR/agent.sock
@@ -41,4 +68,9 @@
;;
esac
'';
in pkgs.symlinkJoin { name = pname; paths = [ script pkg ]; meta.mainProgram = pname; }
in
pkgs.symlinkJoin {
name = pname;
paths = [script pkg];
meta.mainProgram = pname;
}

17
flake.lock generated
View File

@@ -2,15 +2,18 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1721924956,
"narHash": "sha256-Sb1jlyRO+N8jBXEX9Pg9Z1Qb8Bw9QyOgLDNMEpmjZ2M=",
"path": "/nix/store/xblysc3prg1zqsi59fr36nj6wdiqryzp-source",
"rev": "5ad6a14c6bf098e98800b091668718c336effc95",
"type": "path"
"lastModified": 1730272153,
"narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {

View File

@@ -1,5 +1,6 @@
{
description = "frajtano: an ssh agent multiplexer";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = {
self,
@@ -40,18 +41,32 @@
dir = lib.mkOption {
description = "directory in which to place the listening socket";
default = "${config.home.homeDirectory}/.ssh/frajtano";
type = lib.types.path;
};
initialBackingAgents = lib.mkOption {
description = "initially spawned backing_agents - will be passed to /usr/bin/env";
type = with lib.types; listOf (listOf str);
default = [];
};
};
config = lib.mkIf cfg.enable {
config = let
backing_agents = lib.strings.concatMapStringsSep ", " (args: ''{:spawn, {"${pkgs.coreutils}/bin/env", [${lib.concatMapStringsSep ", " (s: ''~S{${s}}'') args}]}}'') cfg.initialBackingAgents;
configFile = pkgs.writeText "config.exs" ''
import Config
config :frajtano, initial_backing_agents: [${backing_agents}]
'';
in 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";
Service.Type = "notify";
Service.Environment = ["'FRAJTANO_DIR=${cfg.dir}'" "'FRAJTANO_CONFIG=${configFile}'"];
Service.ExecSearchPath = ["${self.packages.${pkgs.system}.default}/bin"];
Service.ExecStart = "frajtano start";
Install.WantedBy = ["default.target"];
};
@@ -59,7 +74,8 @@
enable = true;
config = {
EnvironmentVariables = {
FRAJTANO_DIR = cfg.dir;
FRAJTANO_DIR = "${cfg.dir}";
FRAJTANO_CONFIG = "${configFile}";
};
ProgramArguments = ["${self.packages.${pkgs.system}.default}/bin/frajtano" "start"];
RunAtLoad = true;

View File

@@ -1,6 +1,7 @@
defmodule Frajtano.Agent do
alias Frajtano.Peer
alias Frajtano.BackingAgent
use GenServer
require Logger
def start_link(args) do
GenServer.start_link(__MODULE__, nil, [{:name, __MODULE__} | args])
@@ -11,54 +12,52 @@ defmodule Frajtano.Agent do
{
:ok,
%{},
{: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}
def initial_backing_agents() do
initial_backing_agents = Application.fetch_env!(:frajtano, :initial_backing_agents)
|> Enum.map(&GenServer.call(__MODULE__, {:add_backing_agent, &1}))
Logger.info("Started initial backing_agents: #{inspect initial_backing_agents}")
end
# select: list of specs, where specs are a tuple of match, guards, and outputs
# match is {key, pid, value}, :"$1" is a match variable
def peer_paths() do
Registry.select(Frajtano.Peers, [{{:"$1", :_, :_}, [], [:"$1"]}])
def backing_agent_paths() do
Registry.select(Frajtano.BackingAgents, [{{:"$1", :_, :_}, [], [:"$1"]}])
end
def peer_pids() do
Registry.select(Frajtano.Peers, [{{:_, :"$1", :_}, [], [:"$1"]}])
def backing_agent_pids() do
Registry.select(Frajtano.BackingAgents, [{{:_, :"$1", :_}, [], [:"$1"]}])
end
@impl true
def handle_call({:identities}, _from, _state) do
idents =
Task.async_stream(
peer_pids(),
&{&1, Peer.identities(&1)},
backing_agent_pids(),
&{&1, BackingAgent.identities(&1)},
ordered: false,
on_timeout: :kill_task
)
idents = for {:ok, {peer, {:ok, idents}}} <- idents, do: {idents, peer}
idents = for {:ok, {backing_agent, {:ok, idents}}} <- idents, do: {idents, backing_agent}
{
:reply,
{:ok, Enum.flat_map(idents, &elem(&1, 0))},
for({idents, peer} <- idents, {key, _comment} <- idents, into: %{}, do: {key, peer})
{:ok, idents |> Enum.flat_map(&elem(&1, 0)) |> Enum.uniq},
for({idents, backing_agent} <- idents, {key, _comment} <- idents, into: %{}, do: {key, backing_agent})
}
end
@impl true
def handle_call({:sign, {key, _, _} = req}, _from, state) do
{:reply, Peer.sign(state[key], req), state}
{:reply, BackingAgent.sign(state[key], req), state}
end
@impl true
def handle_call({:add_peer, path}, _from, state) do
# TODO: deduplicate peers by socket path
case Peer.start(path) do
def handle_call({:add_backing_agent, spec}, _from, state) do
case BackingAgent.start(spec) do
{:ok, _} -> {:reply, :ok, state}
{:error, error} -> {:reply, {:error, error}, state}
end
@@ -73,7 +72,11 @@ defmodule Frajtano.Agent do
GenServer.call(__MODULE__, {:sign, request}, :infinity)
end
def add_peer(path) do
GenServer.call(__MODULE__, {:add_peer, path})
def assimilate(path) do
GenServer.call(__MODULE__, {:add_backing_agent, {:socket, path}})
end
def spawn_backing_agent(spec) do
GenServer.call(__MODULE__, {:add_backing_agent, {:spawn, spec}})
end
end

View File

@@ -17,11 +17,13 @@ defmodule Frajtano.Supervisor do
@impl true
def init(:ok) do
children = [
{DynamicSupervisor, name: Frajtano.PeerSupervisor},
{Registry, keys: :unique, name: Frajtano.Peers},
{DynamicSupervisor, name: Frajtano.BackingAgentSupervisor},
{Registry, keys: :unique, name: Frajtano.BackingAgents},
Frajtano.Agent,
{Task.Supervisor, name: Frajtano.ClientSupervisor},
{Frajtano.Listener, [Application.fetch_env!(:frajtano, :listen_path)]},
{Task, &Frajtano.Agent.initial_backing_agents/0},
:systemd.ready(),
]
Supervisor.init(children, strategy: :one_for_one)

View File

@@ -1,22 +1,63 @@
defmodule Frajtano.Peer do
defmodule Frajtano.BackingAgent do
alias Frajtano.Proto
require Logger
use GenServer, restart: :temporary
def start(path) do
DynamicSupervisor.start_child(Frajtano.PeerSupervisor, {__MODULE__, path})
defmodule Spawner do
use Supervisor, restart: :permanent
def start_link(spec) do
Supervisor.start_link(__MODULE__, spec, [])
end
@impl true
def init({executable, args}) do
Temp.track!()
path = Path.join(Temp.mkdir!(), "agent.sock")
children = [
Supervisor.child_spec({MuonTrap.Daemon, [executable, args ++ [path]]}, restart: :temporary, significant: true),
Supervisor.child_spec({Frajtano.BackingAgent, {path, :spawned, {executable, args}}}, restart: :permanent)
]
Supervisor.init(children, strategy: :one_for_all, auto_shutdown: :any_significant)
end
end
def start_link(path) do
GenServer.start_link(__MODULE__, path, name: {:via, Registry, {Frajtano.Peers, path}})
def start({:socket, path}) do
DynamicSupervisor.start_child(Frajtano.BackingAgentSupervisor, {__MODULE__, {path}})
end
def start({:spawn, spec}) do
Logger.info("Spawning #{inspect spec}")
DynamicSupervisor.start_child(Frajtano.BackingAgentSupervisor, {Spawner, spec})
end
def start_link({path}) do
GenServer.start_link(__MODULE__, {path}, name: {:via, Registry, {Frajtano.BackingAgents, path}})
end
def start_link({_, _, _} = spec) do
GenServer.start_link(__MODULE__, spec, name: {:via, Registry, {Frajtano.BackingAgents, spec}})
end
@impl true
def init(path) do
def init({path}) do
{:ok, conn} = :gen_tcp.connect({:local, path}, 0, [:binary, active: :once])
{:ok, %{conn: conn, clients: :queue.new(), buffer: <<>>}}
end
@impl true
def init({path, :spawned, spec}) do
if File.exists?(path) do
{:ok, conn} = :gen_tcp.connect({:local, path}, 0, [:binary, active: :once])
Logger.info("Connected to #{inspect spec}")
{:ok, %{conn: conn, clients: :queue.new(), buffer: <<>>}}
else
Process.sleep(100)
init({path, :spawned, spec})
end
end
def reply({client, ref}, msg) do
send(client, {ref, msg})
end
@@ -71,12 +112,12 @@ defmodule Frajtano.Peer do
{:stop, {:error, e}, %{}}
end
def identities(peer) do
def identities(backing_agent) do
ref = make_ref()
send(peer, {:send, {:agentc_request_identities, nil}, {self(), ref}})
send(backing_agent, {: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)
timer = Process.send_after(backing_agent, :timeout, 4500)
receive do
{^ref, msg} ->
@@ -91,13 +132,13 @@ defmodule Frajtano.Peer do
end
end
def sign(peer, request) do
def sign(backing_agent, request) do
# 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}})
send(backing_agent, {:send, {:agentc_sign_request, request}, {self(), ref}})
receive do
{^ref, msg} -> msg

13
mix.exs
View File

@@ -7,7 +7,12 @@ defmodule Frajtano.MixProject do
version: "0.0.1",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
deps: deps(),
releases: [
frajtano: [
config_providers: [{Config.Reader, {:system, "FRAJTANO_CONFIG", ""}}]
],
]
]
end
@@ -19,6 +24,10 @@ defmodule Frajtano.MixProject do
end
defp deps do
[]
[
{:muontrap, "~> 1.0"},
{:temp, "~> 0.4"},
{:systemd, "~> 0.6"},
]
end
end

View File

@@ -1 +1,7 @@
%{
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
"enough": {:hex, :enough, "0.1.0", "0254710c52d324e2dadde54cb56fbb80a792c2eb285669b8379efd0752bf89f0", [:rebar3], [], "hexpm", "0460c7abda5f5e0ea592b12bc6976b8a5c4b96e42f332059cd396525374bf9a1"},
"muontrap": {:hex, :muontrap, "1.5.0", "bf5c273872379968615a39974458328209ac97fa1f588396192131ff973d1ca2", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "daf605e877f60b5be9215e3420d7971fc468677b29921e40915b15fd928273d4"},
"systemd": {:hex, :systemd, "0.6.2", "aaa24f1e3e6cb978c45369768b1abd766a0dbff637ed61254ca64797bcec9963", [:rebar3], [{:enough, "~> 0.1.0", [hex: :enough, repo: "hexpm", optional: false]}], "hexpm", "5062b911800c1ab05157c7bf9a9fbe23dd24c58891c87fd12d2e3ed8fc1708b8"},
"temp": {:hex, :temp, "0.4.9", "eb6355bfa7925a568b3d9eb3bb57e89aa6d2b78bfe8dfb6b698e090631b7f41f", [:mix], [], "hexpm", "bc8bf7b27d9105bef933ef4bf4ba37ac6b899dbeba329deaa88c60b62d6b4b6d"},
}