Skip to main content

src/vpn_manager.erl

%%%-------------------------------------------------------------------
%% @doc Read-only VPN management API.
%%%-------------------------------------------------------------------
-module(vpn_manager).

-export([list_peers/0,
         running_peers/0,
         status/0,
         peer_status/1,
         certificates/0,
         certificate_info/1,
         certificate_status/1,
         peer_info/1,
         peer_stats/1,
         start_peer/1,
         stop_peer/1,
         reload_config/0,
         peer_running/1,
         find_peer/1]).

list_peers() ->
    configured_peer_ids().

running_peers() ->
    running_peer_ids().

status() ->
    Configured = list_peers(),
    Running = running_peers(),
    #{configured => Configured,
      running => Running,
      peers => maps:from_list([{PeerId, peer_status(PeerId)} || PeerId <- Configured])}.

certificates() ->
    [certificate_status(PeerId) || PeerId <- list_peers()].

certificate_info(PeerId) ->
    case lists:member(PeerId, list_peers()) of
        true ->
            certificate_status(PeerId);
        false ->
            {error, not_found}
    end.

certificate_status(PeerId) ->
    case lists:member(PeerId, running_peers()) of
        true ->
            running_certificate_status(PeerId);
        false ->
            stopped_certificate_status(PeerId)
    end.

peer_status(PeerId) ->
    case lists:member(PeerId, running_peers()) of
        true ->
            running_peer_status(PeerId);
        false ->
            #{running => false}
    end.

peer_info(PeerId) ->
    case find_peer(PeerId) of
        {ok, Pid} ->
            #{id => PeerId,
              identity => vpn_peer:identity_info(Pid),
              config => vpn_peer:config(Pid)};
        {error, not_found} ->
            {error, not_found}
    end.

peer_stats(PeerId) ->
    case find_peer(PeerId) of
        {ok, Pid} ->
            vpn_peer:stats(Pid);
        {error, not_found} ->
            {error, not_found}
    end.

running_peer_status(PeerId) ->
    case peer_info(PeerId) of
        #{identity := Identity, config := Config} ->
            case peer_stats(PeerId) of
                #{id := _PeerId} = Stats ->
                    #{running => true,
                      identity => Identity,
                      config => Config,
                      stats => Stats,
                      certificate => certificate_summary(Identity)};
                {error, Reason} ->
                    #{running => false,
                      error => Reason}
            end;
        {error, Reason} ->
            #{running => false,
              error => Reason}
    end.

running_certificate_status(PeerId) ->
    case peer_info(PeerId) of
        #{identity := Identity} ->
            certificate_entry(PeerId, true, Identity);
        {error, Reason} ->
            #{peer_id => PeerId,
              running => false,
              error => Reason}
    end.

stopped_certificate_status(PeerId) ->
    case find_peer_config(PeerId) of
        {ok, PeerConfig} ->
            case vpn_identity:load(PeerConfig) of
                {ok, Identity} ->
                    certificate_entry(PeerId, false, vpn_identity:safe_info(Identity));
                {error, Reason} ->
                    #{peer_id => PeerId,
                      running => false,
                      error => Reason}
            end;
        {error, not_found} ->
            #{peer_id => PeerId,
              running => false,
              error => not_found}
    end.

certificate_entry(PeerId, Running, Identity) ->
    Certificate = maps:get(certificate, Identity, #{}),
    #{peer_id => PeerId,
      running => Running,
      trusted => maps:get(trusted, Identity, false),
      key_match => maps:get(key_match, Identity, false),
      subject => maps:get(subject, Certificate, undefined),
      issuer => maps:get(issuer, Certificate, undefined),
      serial_number => maps:get(serial_number, Certificate, undefined),
      not_before => maps:get(not_before, Certificate, undefined),
      not_after => maps:get(not_after, Certificate, undefined),
      certificate_path => maps:get(certificate_path, Identity, undefined)}.

certificate_summary(Identity) ->
    Certificate = maps:get(certificate, Identity, #{}),
    #{trusted => maps:get(trusted, Identity, false),
      key_match => maps:get(key_match, Identity, false),
      subject => maps:get(subject, Certificate, undefined),
      issuer => maps:get(issuer, Certificate, undefined),
      serial_number => maps:get(serial_number, Certificate, undefined),
      not_before => maps:get(not_before, Certificate, undefined),
      not_after => maps:get(not_after, Certificate, undefined),
      certificate_path => maps:get(certificate_path, Identity, undefined)}.

reload_config() ->
    ConfiguredIds = configured_peer_ids(),
    RunningIds = running_peer_ids(),
    ToStop = RunningIds -- ConfiguredIds,
    ToStart = ConfiguredIds -- RunningIds,
    Unchanged = RunningIds -- ToStop,
    StopResult = collect_stop_results(ToStop, #{started => [], stopped => [], failed => []}),
    StartResult = collect_start_results(ToStart, StopResult),
    StartResult#{unchanged => Unchanged}.

start_peer(PeerId) ->
    case peer_running(PeerId) of
        true ->
            {error, already_started};
        false ->
            start_configured_peer(PeerId)
    end.

stop_peer(PeerId) ->
    case find_peer(PeerId) of
        {ok, _Pid} ->
            vpn_peer_sup:stop_peer(PeerId);
        {error, not_found} ->
            {error, not_found}
    end.

peer_running(PeerId) ->
    case find_peer(PeerId) of
        {ok, _Pid} ->
            true;
        {error, not_found} ->
            false
    end.

find_peer(PeerId) ->
    case [Pid || {{vpn_peer, Id}, Pid, worker, _Modules} <- peer_children(),
                 Id =:= PeerId,
                 is_pid(Pid)] of
        [Pid | _] ->
            {ok, Pid};
        [] ->
            {error, not_found}
    end.

start_configured_peer(PeerId) ->
    case find_peer_config(PeerId) of
        {ok, PeerConfig} ->
            case vpn_peer_sup:start_peer(PeerConfig) of
                {ok, Pid} ->
                    {ok, Pid};
                {ok, Pid, _Info} ->
                    {ok, Pid};
                {error, {already_started, _Pid}} ->
                    {error, already_started};
                {error, {Reason, {child, _Pid, _Id, _Start, _Restart, _Significant, _Shutdown, _Type, _Modules}}} ->
                    {error, Reason};
                {error, Reason} ->
                    {error, Reason}
            end;
        {error, not_found} ->
            {error, not_found}
    end.

find_peer_config(PeerId) ->
    case [PeerConfig || PeerConfig <- configured_peers(),
                        maps:get(id, PeerConfig) =:= PeerId] of
        [PeerConfig | _] ->
            {ok, PeerConfig};
        [] ->
            {error, not_found}
    end.

collect_stop_results([], Acc) ->
    Acc;
collect_stop_results([PeerId | Rest], Acc) ->
    case stop_peer(PeerId) of
        ok ->
            collect_stop_results(Rest, append_result(stopped, PeerId, Acc));
        {error, Reason} ->
            collect_stop_results(Rest, append_result(failed, {PeerId, Reason}, Acc))
    end.

collect_start_results([], Acc) ->
    Acc;
collect_start_results([PeerId | Rest], Acc) ->
    case start_peer(PeerId) of
        {ok, _Pid} ->
            collect_start_results(Rest, append_result(started, PeerId, Acc));
        {error, Reason} ->
            collect_start_results(Rest, append_result(failed, {PeerId, Reason}, Acc))
    end.

append_result(Key, Value, Acc) ->
    maps:update_with(Key, fun(Values) -> Values ++ [Value] end, [Value], Acc).

configured_peer_ids() ->
    lists:sort([maps:get(id, PeerConfig) || PeerConfig <- configured_peers()]).

running_peer_ids() ->
    lists:sort([PeerId || {{vpn_peer, PeerId}, Pid, worker, _Modules} <- peer_children(),
                          is_pid(Pid)]).

configured_peers() ->
    application:get_env(vpn, peers, []).

peer_children() ->
    try supervisor:which_children(vpn_peer_sup) of
        Children ->
            Children
    catch
        exit:{noproc, _} ->
            []
    end.