Skip to main content

src/vpn_peer.erl

%%%-------------------------------------------------------------------
%% @doc Runtime peer abstraction over vpn_link.
%%%-------------------------------------------------------------------
-module(vpn_peer).

-behaviour(gen_server).

-export([start_link/1, stop/1, stats/1, reset_stats/1, identity/1, identity_info/1, config/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).

start_link(Config) ->
    gen_server:start_link(?MODULE, Config, []).

stop(Pid) ->
    gen_server:stop(Pid).

stats(Pid) ->
    gen_server:call(Pid, stats).

reset_stats(Pid) ->
    gen_server:call(Pid, reset_stats).

identity(Pid) ->
    gen_server:call(Pid, identity).

identity_info(Pid) ->
    gen_server:call(Pid, identity_info).

config(Pid) ->
    gen_server:call(Pid, config).

init(Config) ->
    process_flag(trap_exit, true),
    case validate_config(Config) of
        ok ->
            start_link_with_identity(Config);
        {error, Reason} ->
            {stop, Reason}
    end.

handle_call(stats, _From, State = #{id := Id, link_pid := LinkPid}) ->
    LinkStats = vpn_link:stats(LinkPid),
    {reply, #{id => Id, link => LinkStats}, State};
handle_call(reset_stats, _From, State = #{link_pid := LinkPid}) ->
    {reply, vpn_link:reset_stats(LinkPid), State};
handle_call(identity, _From, State = #{identity := Identity}) ->
    {reply, Identity, State};
handle_call(identity_info, _From, State = #{identity_info := IdentityInfo}) ->
    {reply, vpn_identity:safe_info(IdentityInfo), State};
handle_call(config, _From, State = #{config := Config}) ->
    {reply, runtime_config(Config), State};
handle_call(_Request, _From, State) ->
    {reply, {error, not_implemented}, State}.

handle_cast(_Request, State) ->
    {noreply, State}.

handle_info({'EXIT', LinkPid, Reason}, State = #{link_pid := LinkPid}) ->
    {stop, {link_exit, Reason}, State};
handle_info(_Message, State) ->
    {noreply, State}.

terminate(_Reason, State) ->
    stop_link(maps:get(link_pid, State, undefined)),
    ok.

start_link_with_identity(Config) ->
    case vpn_identity:load(Config) of
        {ok, IdentityInfo} ->
            start_link_from_config(Config, IdentityInfo);
        {error, Reason} ->
            {stop, Reason}
    end.

start_link_from_config(Config, IdentityInfo) ->
    Id = maps:get(id, Config),
    Mode = maps:get(mode, Config),
    IfName = maps:get(ifname, Config),
    Ip = maps:get(ip, Config),
    LocalUdpPort = maps:get(local_udp_port, Config),
    RemoteIp = maps:get(remote_ip, Config),
    RemoteUdpPort = maps:get(remote_udp_port, Config),
    RemotePeerId = maps:get(remote_peer_id, Config),
    Psk = maps:get(psk, Config),
    Identity = identity_from_config(Config),
    case vpn_link:start_link(IfName,
                             Ip,
                             Mode,
                             LocalUdpPort,
                             RemoteIp,
                             RemoteUdpPort,
                             Id,
                             RemotePeerId,
                             Psk) of
        {ok, LinkPid} ->
            logger:info("vpn_peer started: ~p", [Id]),
            {ok, #{id => Id,
                   config => Config,
                   identity => Identity,
                   identity_info => IdentityInfo,
                   link_pid => LinkPid}};
        {error, Reason} ->
            {stop, Reason}
    end.

validate_config(Config) when is_map(Config) ->
    case missing_key(Config) of
        none ->
            validate_mode(maps:get(mode, Config));
        {missing, Key} ->
            {error, {missing_config_key, Key}}
    end;
validate_config(_Config) ->
    {error, invalid_config}.

missing_key(Config) ->
    Required = [id,
                mode,
                ifname,
                ip,
                local_udp_port,
                remote_ip,
                remote_udp_port,
                remote_peer_id,
                psk,
                certificate_path,
                private_key_path,
                ca_certificate_path],
    missing_key(Config, Required).

missing_key(_Config, []) ->
    none;
missing_key(Config, [Key | Rest]) ->
    case maps:is_key(Key, Config) of
        true ->
            missing_key(Config, Rest);
        false ->
            {missing, Key}
    end.

validate_mode(tap) ->
    ok;
validate_mode(tun) ->
    ok;
validate_mode(Mode) ->
    {error, {invalid_mode, Mode}}.

identity_from_config(Config) ->
    #{id => maps:get(id, Config),
      name => maps:get(name, Config, undefined),
      certificate_path => maps:get(certificate_path, Config, undefined),
      private_key_path => maps:get(private_key_path, Config, undefined),
      ca_certificate_path => maps:get(ca_certificate_path, Config, undefined)}.

runtime_config(Config) ->
    maps:with([id,
               mode,
               ifname,
               ip,
               local_udp_port,
               remote_ip,
               remote_udp_port,
               remote_peer_id],
              Config).

stop_link(undefined) ->
    ok;
stop_link(LinkPid) ->
    case is_process_alive(LinkPid) of
        true ->
            _ = vpn_link:stop(LinkPid),
            ok;
        false ->
            ok
    end.