src/tuncer.erl

%%% Copyright (c) 2011-2023 Michael Santos <michael.santos@gmail.com>. All
%%% rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions
%%% are met:
%%%
%%% 1. Redistributions of source code must retain the above copyright notice,
%%% this list of conditions and the following disclaimer.
%%%
%%% 2. Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in the
%%% documentation and/or other materials provided with the distribution.
%%%
%%% 3. Neither the name of the copyright holder nor the names of its
%%% contributors may be used to endorse or promote products derived from
%%% this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
%%% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
%%% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
%%% A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
%%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
%%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
%%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
%%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
%%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
%%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

%% @doc tuncer manages tuntap devices.
-module(tuncer).

-behaviour(gen_server).

-export([
    create/0, create/1, create/2,
    devname/1,
    flags/1,
    getfd/1,
    destroy/1,

    persist/2,
    owner/2,
    group/2,

    read/1, read/2,
    write/2,

    send/2,
    recv/1, recv/2,

    header/1,

    up/2, up/3,
    dstaddr/2,
    broadcast/2,
    down/1,
    mtu/1, mtu/2,

    controlling_process/2,
    setopt/2
]).

-export([start_link/2]).
-export([
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3
]).

-type dev() :: pid().
-type port_options() ::
    {parallelism, boolean()}
    | {busy_limits_port, {non_neg_integer(), non_neg_integer()} | disabled}
    | {busy_limits_msgq, {non_neg_integer(), non_neg_integer()} | disabled}.
%% port_options() are the supported subset of port options passed directly to [https://www.erlang.org/doc/man/erlang#open_port-2].

-type options() ::
    tunctl:options()
    | {active, boolean()}
    | {port_options, port_options()}.

-export_type([
    dev/0,
    port_options/0,
    options/0
]).

-record(state, {
    % false, port
    port,
    % port options
    port_options :: [port_options()],
    % PID of controlling process
    pid :: pid(),
    % TUN/TAP file descriptor
    fd,
    % device name
    dev,
    % TUNSETIFF ifr flags
    flag,
    persist = false :: boolean()
}).

-define(IFNAMSIZ, 16).

%%--------------------------------------------------------------------
%%% Exports
%%--------------------------------------------------------------------

%% @doc Create the tap0 device.
%%
%% == Examples ==
%%
%% ```
%% 1> tuncer:create().
%% {ok,<0.175.0>}
%% '''
%%
%% ```
%% $ ip link show tap0
%% 2: tap0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
%%     link/ether 32:a9:6a:5b:48:c3 brd ff:ff:ff:ff:ff:ff
%% '''
%%
%% @see create/2
-spec create() -> gen_server:start_ret().
create() ->
    create(<<>>).

%% @doc Create a named tap device.
%%
%% == Examples ==
%%
%% ```
%% 1> tuncer:create("foo").
%% {ok,<0.175.0>}
%% '''
%%
%% ```
%% $ ip link show foo
%% 3: foo: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
%%     link/ether 9e:f1:90:e2:2c:66 brd ff:ff:ff:ff:ff:ff
%% '''
%%
%% @see create/2
-spec create(Ifname :: binary() | string()) -> gen_server:start_ret().
create(Ifname) ->
    create(Ifname, [tap, no_pi]).

%% @doc Create a tuntap device.
%%
%%  Device is the TUN/TAP device name. If a device name is empty,
%%  the TUN/TAP driver will choose one (for tap devices,
%%  starting from `tap0'; for tun devices, beginning from `tun0').
%%
%%  When the device is in `{active, true}' mode, data is sent as
%%  messages:
%%
%%      `{tuntap, PID, binary()}'
%%
%%  If an error is encountered:
%%
%%      `{tuntap_error, PID, posix()}'
%%
%%  Retrieving data from devices in `{active, false}' mode can be done
%%  using recv/1,2 or read/1,2.
%%
%%  Options contains a list of flags.
%%
%%      `tun': create a tun interface
%%
%%      `tap': create a tap interface
%%
%%      `no_pi': do not prepend the data with a 4 byte header describing
%%             the physical interface
%%
%%      `{port_options, port_options()}': options passed to
%%             `erlang:open_port/2' when using `active' mode. Defaults to disabling
%%             port busy checks: `[{busy_limits_port, disabled}, {busy_limits_msgq, disabled}]'
%%
%%  The options default to `[tap, no_pi, {active, false}]'.
%%
%% == Examples ==
%%
%% ```
%% 1> tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.181.0>}
%% '''
%%
%% ```
%% $ ip link show tun0
%% 4: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 500
%%     link/none
%% '''
-spec create(Ifname :: binary() | string(), Opt :: [options()]) -> gen_server:start_ret().
create(Ifname, Opt) when is_list(Ifname) ->
    create(list_to_binary(Ifname), Opt);
create(Ifname, Opt) when byte_size(Ifname) < ?IFNAMSIZ, is_list(Opt) ->
    start_link(Ifname, Opt);
create(_, _) ->
    {error, badargs}.

%% @doc Parse TUN/TAP packet header.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> {ok, Bin} = tuncer:recv(Dev).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,247,188,77,
%%       238,78,171,184,107,255,2,0,...>>}
%% 4> tuncer:header(Bin).
%% {tun_pi,96,0,
%%         <<0,8,58,255,254,128,0,0,0,0,0,0,247,188,77,238,78,171,
%%           184,107,255,2,0,0,0,...>>}
%% '''
-spec header(binary()) -> {tun_pi, tunctl:uint16_t(), tunctl:uint16_t()} | {error, file:posix()}.
header(Buf) when byte_size(Buf) > 4 ->
    tunctl:header(Buf).

%% @doc Get the TUN/TAP device name.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.175.0>}
%% 2> tuncer:devname(Dev).
%% <<"tap0">>
%% '''
-spec devname(dev()) -> binary().
devname(Ref) when is_pid(Ref) ->
    getstate(Ref, dev).

%% @doc Returns an integer holding the interface creation flags.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.175.0>}
%% 2> tuncer:flags(Dev).
%% [tap,no_pi]
%% '''
-spec flags(dev()) -> integer().
flags(Ref) when is_pid(Ref) ->
    getstate(Ref, flag).

%% @doc Get TUN/TAP device file descriptor.
%%
%% Get the file descriptor associated with the process. Use getfd/1
%% with read/1,2 and write/2 to interact directly with the tuntap device
%% (bypassing the gen_server).
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.175.0>}
%% 2> tuncer:getfd(Dev).
%% 22
%% '''
-spec getfd(dev()) -> integer().
getfd(Ref) when is_pid(Ref) ->
    getstate(Ref, fd).

%% @doc Remove the TUN/TAP device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.175.0>}
%% 2> tuncer:destroy(Dev).
%% ok
%% '''
-spec destroy(dev()) -> ok.
destroy(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, destroy, infinity).

%% @doc Set the interface to exist after the Erlang process exits.
%%
%% == Support ==
%%
%% * Linux
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.201.0>}
%% 2> tuncer:persist(Dev, true).
%% ok
%% '''
-spec persist(dev(), boolean()) -> ok | {error, file:posix()}.
persist(Ref, Bool) when is_pid(Ref), is_boolean(Bool) ->
    gen_server:call(Ref, {persist, Bool}, infinity).

%% @doc Set the UID owning the interface.
%%
%% == Support ==
%%
%% * Linux
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.201.0>}
%% 2> tuncer:owner(Dev, 1000).
%% ok
%% '''
-spec owner(dev(), integer()) -> ok | {error, file:posix()}.
owner(Ref, Owner) when is_pid(Ref), is_integer(Owner) ->
    gen_server:call(Ref, {owner, Owner}, infinity).

%% @doc Set the GID owning the interface.
%%
%% == Support ==
%%
%% * Linux
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.201.0>}
%% 2> tuncer:group(Dev, 1000).
%% ok
%% '''
-spec group(dev(), integer()) -> ok | {error, file:posix()}.
group(Ref, Group) when is_pid(Ref), is_integer(Group) ->
    gen_server:call(Ref, {group, Group}, infinity).

%% @doc Configure a TUN/TAP device using the default netmask and broadcast for the network.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create().
%% {ok,<0.201.0>}
%% 2> tuncer:up(Dev, "127.8.8.8").
%% ok
%% '''
%%
%% ```
%% # ip a show dev tap0
%% 7: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000
%%    link/ether 6e:fb:29:46:df:00 brd ff:ff:ff:ff:ff:ff
%%    inet 127.8.8.8/32 brd 127.255.255.255 scope host tap0
%%    valid_lft forever preferred_lft forever
%%    inet6 fe80::6cfb:29ff:fe46:df00/64 scope link
%%    valid_lft forever preferred_lft forever
%% '''
-spec up(dev(), inet:socket_address() | inet:hostname()) -> ok | {error, file:posix()}.
up(Ref, Addr) when is_pid(Ref), is_list(Addr) ->
    case inet_parse:address(Addr) of
        {ok, IPv4} -> up(Ref, IPv4);
        {error, _} = Error -> Error
    end;
up(Ref, Addr) when is_pid(Ref), is_tuple(Addr) ->
    gen_server:call(Ref, {up, Addr}, infinity).

%% @doc Configure a TUN/TAP device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.197.0>}
%% 2> tuncer:up(Dev, "10.1.1.1", 24).
%% ok
%% '''
%%
%% ```
%% 16: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
%%     link/none
%%     inet 10.1.1.1/24 scope global tun0
%%        valid_lft forever preferred_lft forever
%%     inet6 fe80::5472:aa66:7afa:64b9/64 scope link stable-privacy
%%        valid_lft forever preferred_lft forever
%% '''
-spec up(dev(), inet:socket_address() | inet:hostname(), 0..32) -> ok | {error, file:posix()}.
up(Ref, Addr, Mask) when is_pid(Ref), is_list(Addr), Mask >= 0, Mask =< 32 ->
    case inet_parse:address(Addr) of
        {ok, IPv4} -> up(Ref, IPv4, Mask);
        {error, _} = Error -> Error
    end;
up(Ref, Addr, Mask) when is_pid(Ref), is_tuple(Addr), Mask >= 0, Mask =< 32 ->
    gen_server:call(Ref, {up, Addr, Mask}, infinity).

%% @doc Configure the remote address for a TUN/TAP device in point-to-point mode.
%%
%% == Support ==
%%
%% * Linux (IPv4 addresses only)
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.197.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:dstaddr(Dev, "10.1.1.2").
%% ok
%% '''
%%
%% ```
%% $ ip a show tun0
%% 4: tun0: <POINTOPOINT,MULTICAST,NOARP,NOTRAILERS,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
%%     link/none
%%     inet 10.1.1.1 peer 10.1.1.2/32 scope global tun0
%%        valid_lft forever preferred_lft forever
%%     inet6 fe80::e0f7:c90f:e2d8:91e8/64 scope link stable-privacy
%%        valid_lft forever preferred_lft forever
%% '''
-spec dstaddr(dev(), inet:socket_address() | inet:hostname()) -> ok | {error, file:posix()}.
dstaddr(Ref, Addr) when is_pid(Ref), is_list(Addr) ->
    case inet_parse:address(Addr) of
        {ok, IP} ->
            dstaddr(Ref, IP);
        {error, _} = Error ->
            Error
    end;
dstaddr(Ref, Addr) when is_pid(Ref), is_tuple(Addr) ->
    gen_server:call(Ref, {dstaddr, Addr}, infinity).

%% @doc Configure the broadcast address for a TUN/TAP device.
%%
%% == Support ==
%%
%% * Linux (IPv4 addresses only)
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:broadcast(Dev, "10.1.1.3").
%% ok
%% '''
%%
%% ```
%% 6: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
%%     link/none
%%     inet 10.1.1.1/32 brd 10.1.1.3 scope global tun0
%%        valid_lft forever preferred_lft forever
%%     inet6 fe80::9d37:9d1:efba:75a1/64 scope link stable-privacy
%%        valid_lft forever preferred_lft forever
%% '''
-spec broadcast(dev(), inet:socket_address() | inet:hostname()) -> ok | {error, file:posix()}.
broadcast(Ref, Addr) when is_pid(Ref), is_list(Addr) ->
    case inet_parse:address(Addr) of
        {ok, IP} ->
            broadcast(Ref, IP);
        {error, _} = Error ->
            Error
    end;
broadcast(Ref, Addr) when is_pid(Ref), is_tuple(Addr) ->
    gen_server:call(Ref, {broadcast, Addr}, infinity).

%% @doc Unconfigure a TUN/TAP device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:down(Dev).
%% ok
%% '''
-spec down(dev()) -> ok | {error, file:posix()}.
down(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, down, infinity).

%% @doc Get the MTU (maximum transmission unit) for the TUN/TAP device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:mtu(Dev).
%% 1500
%% '''
-spec mtu(dev()) -> integer().
mtu(Ref) when is_pid(Ref) ->
    Dev = binary_to_list(devname(Ref)),
    {ok, MTU} = inet:ifget(Dev, [mtu]),
    proplists:get_value(mtu, MTU).

%r @doc Set the MTU (maximum transmission unit) for the TUN/TAP device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:mtu(Dev).
%% 1500
%% 4> tuncer:mtu(Dev, 1400).
%% {ok,22,<<"tun0">>}
%% 5> tuncer:mtu(Dev).
%% 1400
%% '''
-spec mtu(dev(), integer()) -> {ok, tunctl:fd(), binary()} | {error, file:posix()}.
mtu(Ref, MTU) when is_pid(Ref), is_integer(MTU) ->
    gen_server:call(Ref, {mtu, MTU}, infinity).

%% @doc Read data from the tuntap device file descriptor.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.197.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> FD = tuncer:getfd(Dev).
%% 22
%% 4> tuncer:read(FD).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,36,14,228,
%%       30,140,106,15,112,255,2,0,...>>}
%% '''
-spec read(tunctl:fd()) -> {ok, binary()} | {error, file:posix()}.
read(FD) ->
    read(FD, 16#FFFF).

%% @doc Read up to the specified number of bytes from the tuntap device file
%% descriptor.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.197.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> FD = tuncer:getfd(Dev).
%% 22
%% 4> tuncer:read(FD, 16).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0>>}
%% '''
-spec read(tunctl:fd(), integer()) -> {ok, binary()} | {error, file:posix()}.
read(FD, Len) when is_integer(FD), is_integer(Len) ->
    procket:read(FD, Len).

%% @doc Write data to the tuntap device file descriptor.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> FD = tuncer:getfd(Dev).
%% 22
%% 4> {ok, Data} = tuncer:read(FD, 16).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0>>}
%% 6> tuncer:write(FD, Data).
%% ok
%% '''
-spec write(tunctl:fd(), binary()) -> ok | {error, file:posix()}.
write(FD, Data) when is_integer(FD), is_binary(Data) ->
    procket:write(FD, Data).

%% @doc Write data to the tuntap device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> {ok, Data} = tuncer:recv(Dev).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,89,201,218,
%%       161,25,116,199,243,255,2,0,...>>}
%% 4> tuncer:send(Dev, Data).
%% ok
%% '''
send(Ref, Data) when is_pid(Ref), is_binary(Data) ->
    gen_server:call(Ref, {send, Data}, infinity).

%% @doc Read data to the tuntap device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> {ok, D} = tuncer:recv(Dev).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,89,201,218,
%%       161,25,116,199,243,255,2,0,...>>}
%% '''
recv(Ref) ->
    recv(Ref, 16#FFFF).

%% @doc Read up to the specified number of bytes from the tuntap device.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:recv(Dev, 16).
%% {ok,<<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0>>}
%% '''
recv(Ref, Len) when is_pid(Ref), is_integer(Len) ->
    gen_server:call(Ref, {recv, Len}, infinity).

%% @doc Change the controlling process of the TUN/TAP device.
%%
%% Change the process owning the socket. Allows another process to
%% send and receive packets from the TUN/TAP device.
controlling_process(Ref, Pid) when is_pid(Ref), is_pid(Pid) ->
    Owner = self(),
    case gen_server:call(Ref, {state, [pid, port]}, infinity) of
        [{pid, Owner}, {port, false}] ->
            controlling_process_1(Ref, Pid, false, ok);
        [{pid, Owner}, {port, _}] ->
            controlling_process_1(Ref, Pid, true, setopt(Ref, {active, false}));
        _ ->
            {error, not_owner}
    end.

controlling_process_1(Ref, Pid, Mode, ok) ->
    case gen_server:call(Ref, {controlling_process, Pid}, infinity) of
        ok ->
            flush_events(Ref, Pid),
            setopt(Ref, {active, Mode});
        Error ->
            Error
    end;
controlling_process_1(_Ref, _Pid, _Mode, Error) ->
    Error.

%% @doc Set TUN/TAP device option.
%%
%% setopt/2 currently supports toggling `active' mode for performing
%% flow control.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, Dev} = tuncer:create("tun0", [tun, no_pi, {active, false}]).
%% {ok,<0.205.0>}
%% 2> tuncer:up(Dev, "10.1.1.1").
%% ok
%% 3> tuncer:setopt(Dev, {active, true}).
%% ok
%% 4> tuncer:setopt(Dev, {active, false}).
%% ok
%% 5> flush().
%% Shell got {tuntap,<0.205.0>,
%%                   <<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,44,191,1,21,92,
%%                     149,243,132,255,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,133,0,255,
%%                     72,0,0,0,0>>}
%% Shell got {tuntap,<0.205.0>,
%%                   <<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,44,191,1,21,92,
%%                     149,243,132,255,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,133,0,255,
%%                     72,0,0,0,0>>}
%% Shell got {tuntap,<0.205.0>,
%%                   <<96,0,0,0,0,8,58,255,254,128,0,0,0,0,0,0,44,191,1,21,92,
%%                     149,243,132,255,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,133,0,255,
%%                     72,0,0,0,0>>}
%% ok
%% '''
setopt(Ref, Option) when is_tuple(Option) ->
    gen_server:call(Ref, {setopt, Option}, infinity).

%% @private
start_link(Ifname, Opt) when is_binary(Ifname), is_list(Opt) ->
    Pid = self(),
    gen_server:start_link(?MODULE, [Pid, Ifname, Opt], []).

%%--------------------------------------------------------------------
%%% Callbacks
%%--------------------------------------------------------------------

%% @private
init([Pid, Ifname, Flag]) ->
    process_flag(trap_exit, true),

    % if Dev is NULL, the tuntap driver will choose an
    % interface name
    case tunctl:create(Ifname, Flag) of
        {ok, FD, Dev} ->
            Active = proplists:get_value(active, Flag, false),
            Port_options = proplists:get_value(port_options, Flag, [
                {busy_limits_port, disabled}, {busy_limits_msgq, disabled}
            ]),

            Port =
                case Active of
                    true -> set_mode(active, FD, Port_options);
                    false -> false
                end,

            {ok, #state{
                port = Port,
                port_options = Port_options,
                pid = Pid,
                fd = FD,
                dev = Dev,
                flag = Flag
            }};
        Else ->
            Else
    end.

%%
%% retrieve/modify gen_server state
%%
%% @private
handle_call({state, Field}, _From, State) ->
    {reply, state(Field, State), State};
handle_call({controlling_process, Owner}, {Owner, _}, #state{pid = Owner} = State) ->
    {reply, ok, State};
handle_call({controlling_process, Pid}, {Owner, _}, #state{pid = Owner} = State) ->
    link(Pid),
    unlink(Owner),
    {reply, ok, State#state{pid = Pid}};
handle_call({controlling_process, _}, _, State) ->
    {reply, {error, not_owner}, State};
handle_call(
    {setopt, {active, true}}, _From, #state{port = false, fd = FD, port_options = Port_opt} = State
) ->
    try set_mode(active, FD, Port_opt) of
        Port ->
            {reply, ok, State#state{port = Port}}
    catch
        error:Error ->
            {reply, {error, Error}, State}
    end;
handle_call({setopt, {active, true}}, _From, State) ->
    {reply, ok, State};
handle_call({setopt, {active, false}}, _From, #state{port = false} = State) ->
    {reply, ok, State};
handle_call({setopt, {active, false}}, _From, #state{port = Port} = State) ->
    Reply = set_mode(passive, Port),
    {reply, Reply, State#state{port = false}};
handle_call({setopt, _}, _From, State) ->
    {reply, {error, badarg}, State};
%%
%% manipulate the tun/tap device
%%
handle_call({send, Data}, _From, #state{port = false, fd = FD} = State) ->
    Reply = procket:write(FD, Data),
    {reply, Reply, State};
handle_call({send, Data}, _From, #state{port = Port} = State) ->
    Reply =
        try erlang:port_command(Port, Data) of
            true ->
                ok
        catch
            error:Error ->
                {error, Error}
        end,
    {reply, Reply, State};
handle_call({recv, Len}, _From, #state{port = false, fd = FD} = State) ->
    Reply = procket:read(FD, Len),
    {reply, Reply, State};
handle_call({recv, _Len}, _From, State) ->
    {reply, {error, einval}, State};
handle_call({persist, Status}, _From, #state{fd = FD} = State) ->
    Reply = tunctl:persist(FD, Status),
    {reply, Reply, State#state{persist = Status}};
handle_call({owner, Owner}, _From, #state{fd = FD} = State) ->
    Reply = tunctl:owner(FD, Owner),
    {reply, Reply, State};
handle_call({group, Group}, _From, #state{fd = FD} = State) ->
    Reply = tunctl:group(FD, Group),
    {reply, Reply, State};
handle_call({up, IP}, _From, #state{dev = Dev} = State) ->
    Reply = tunctl:up(Dev, IP),
    {reply, Reply, State};
handle_call({up, IP, Mask}, _From, #state{dev = Dev} = State) ->
    Reply = tunctl:up(Dev, IP, Mask),
    {reply, Reply, State};
handle_call({dstaddr, IP}, _From, #state{dev = Dev} = State) ->
    Reply = tunctl:dstaddr(Dev, IP),
    {reply, Reply, State};
handle_call({broadcast, IP}, _From, #state{dev = Dev} = State) ->
    Reply = tunctl:broadcast(Dev, IP),
    {reply, Reply, State};
handle_call(down, _From, #state{dev = Dev} = State) ->
    Reply = tunctl:down(Dev),
    {reply, Reply, State};
handle_call({mtu, MTU}, _From, #state{dev = Dev, fd = FD} = State) ->
    Reply = tunctl:mtu(FD, Dev, MTU),
    {reply, Reply, State};
handle_call(destroy, _From, State) ->
    {stop, normal, ok, State}.

%% @private
handle_cast(_Msg, State) ->
    {noreply, State}.

%%
%% {active, true} mode
%%
%% @private
handle_info({'EXIT', _, normal}, State) ->
    {noreply, State};
handle_info({'EXIT', _, _}, #state{port = false} = State) ->
    {noreply, State};
handle_info(
    {'EXIT', Port, Error},
    #state{port = Port, pid = Pid, fd = FD, dev = Dev, persist = Persist} = State
) ->
    Pid ! {tuntap_error, self(), Error},
    _ =
        case Persist of
            true ->
                ok;
            false ->
                _ = tunctl:down(Dev),
                tunctl:persist(FD, false)
        end,
    procket:close(FD),
    {stop, normal, State};
handle_info({Port, {data, Data}}, #state{port = Port, pid = Pid} = State) ->
    Pid ! {tuntap, self(), Data},
    {noreply, State};
% WTF?
handle_info(Info, State) ->
    error_logger:error_report([wtf, Info]),
    {noreply, State}.

%% @private
terminate(_Reason, #state{fd = FD, dev = Dev, port = Port, persist = Persist}) ->
    terminate_port(Port),
    ok = tun_down(Dev, Persist),
    procket:close(FD).

terminate_port(Port) when is_port(Port) ->
    catch erlang:port_close(Port);
terminate_port(_Port) ->
    ok.

tun_down(_Dev, true = _Persist) ->
    ok;
tun_down(Dev, false = _Persist) ->
    tunctl:down(Dev).

%% @private
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
set_mode(active, FD, Opt) ->
    open_port({fd, FD, FD}, [stream, binary] ++ Opt).
set_mode(passive, Port) ->
    try erlang:port_close(Port) of
        true ->
            ok
    catch
        error:Error ->
            {error, Error}
    end.

flush_events(Ref, Pid) ->
    receive
        {tuntap, Ref, _} = Event ->
            Pid ! Event,
            flush_events(Ref, Pid)
    after 0 -> ok
    end.

getstate(Ref, Key) when is_atom(Key) ->
    [{Key, Value}] = gen_server:call(Ref, {state, [Key]}, infinity),
    Value.

state(Fields, State) ->
    state(Fields, State, []).

state([], _State, Acc) ->
    lists:reverse(Acc);
state([Field | Fields], State, Acc) ->
    state(Fields, State, [{Field, field(Field, State)} | Acc]).

field(port, #state{port = Port}) -> Port;
field(pid, #state{pid = Pid}) -> Pid;
field(fd, #state{fd = FD}) -> FD;
field(dev, #state{dev = Dev}) -> Dev;
field(flag, #state{flag = Flag}) -> Flag;
field(_, _) -> unsupported.