src/sut.erl

%%% @copyright 2012-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 sut, an IPv6 in IPv4 Userlspace Tunnel (RFC 4213)
%%
%% == SETUP ==
%%
%% * Sign up for an IPv6 tunnel with Hurricane Electric
%%
%%   [http://tunnelbroker.net/]
%%
%% * Start the IPv6 tunnel:
%%
%% ```
%%   * Serverv4 = HE IPv4 tunnel end
%%
%%   * Clientv4 = Your local IP address
%%
%%   * Clientv6 = The IPv6 address assigned by HE to your end of the tunnel
%% '''
%%
%% ```
%% sut:start([
%%            {serverv4, "216.66.22.2"},
%%            {clientv4, "192.168.1.72"},
%%            {clientv6, "2001:3:3:3::2"}
%%           ]).
%% '''
%%
%% * Set up MTU and routing (as root)
%%
%% ```
%% ifconfig sut-ipv6 mtu 1480
%% ip route add ::/0 dev sut-ipv6
%% '''
%%
%% * Test the tunnel!
%%
%% ```
%% ping6 ipv6.google.com
%% '''
%%
%% == EXAMPLES ==
%%
%% To compile:
%%
%% ```
%% erlc -I deps -o ebin examples/*.erl
%% '''
%%
%% === basic_firewall ===
%%
%% An example of setting up a stateless packet filter.
%%
%% The rules are:
%%
%% ```
%% * icmp: all
%% * udp: none
%% * tcp:
%%     * outgoing: 22, 80, 443
%%     * incoming: 22
%% '''
%%
%% Start the tunnel with the filter:
%%
%% ```
%% sut:start([
%%     {filter_out, fun(Packet, State) -> basic_firewall:out(Packet, State) end},
%%     {filter_in, fun(Packet, State) -> basic_firewall:in(Packet, State) end},
%%
%%     {serverv4, Server4},
%%     {clientv4, Client4},
%%     {clientv6, Client6}
%%     ]).
%% '''
%%
%% === tunnel_activity ===
%%
%% Flashes LEDs attached to an Arduino to signal tunnel activity. Requires:
%%
%% [https://github.com/msantos/srly]
%%
%% Upload a sketch to the Arduino:
%%
%% [https://github.com/msantos/srly/blob/master/examples/strobe/strobe.pde]
%%
%% Then start the tunnel:
%%
%% ```
%% tunnel_activity:start("/dev/ttyUSB0",
%%         [{led_in, 3},
%%          {led_out, 4},
%%
%%         {serverv4, Server4},
%%         {clientv4, Client4},
%%         {clientv6, Client6}]).
%% '''
-module(sut).
-include("sut.hrl").
-behaviour(gen_server).

-export([
    start/1,
    start_link/1,
    destroy/1
]).

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

-type sut_state() :: #sut_state{}.

-type filter() :: fun((binary(), sut_state()) -> ok | {ok, Packet :: binary()} | {error, any()}).
-type errorfun() :: fun((any()) -> ok | {error, any()}).

-type options() ::
    {ifname, string() | binary()}
    | {serverv4, string() | inet:ip4_address()}
    | {serverv6, string() | inet:ip6_address()}
    | {clientv4, string() | inet:ip4_address()}
    | {clientv6, string() | inet:ip6_address()}
    | {filter_out, filter()}
    | {filter_in, filter()}
    | {error_out, errorfun()}
    | {error_in, errorfun()}.

-export_type([
    filter/0,
    errorfun/0,
    options/0,
    sut_state/0
]).

-record(state, {
    ifname = <<"sut-ipv6">> :: binary(),
    serverv4 = {127, 0, 0, 1} :: string() | inet:ip4_address(),
    clientv4 = {127, 0, 0, 1} :: string() | inet:ip4_address(),
    clientv6 = {0, 0, 0, 0, 0, 0, 0, 1} :: string() | inet:ip6_address(),

    filter_out = fun(_Packet, _State) -> ok end :: filter(),
    filter_in = fun(_Packet, _State) -> ok end :: filter(),

    error_out = fun
        (ok) -> ok;
        (Error) -> Error
    end :: errorfun(),
    error_in = fun
        (ok) -> ok;
        (Error) -> Error
    end :: errorfun(),

    s :: undefined | inet:socket(),
    fd :: undefined | integer(),
    dev :: undefined | pid()
}).

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

%% @doc Shutdown the tunnel. On Linux, the tunnel device will be removed.
-spec destroy(pid()) -> ok.
destroy(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, destroy).

%% @doc Start an IPv6 over IPv4 configured tunnel.
%% @see start_link/1
-spec start([options()]) -> 'ignore' | {'error', _} | {'ok', pid()}.
start(Opt) when is_list(Opt) ->
    gen_server:start(?MODULE, [options(Opt)], []).

%% @doc Start an IPv6 over IPv4 configured tunnel.
%%
%% The default tun device is named `sut-ipv6'. To specify the name,
%% use `{ifname, <<"devname">>}'. Note the user running the tunnel
%% must have sudo permissions to configure this device.
%%
%% `{serverv4, Server4}' is the IPv4 address of the peer.
%%
%% `{clientv4, Client4}' is the IPv4 address of the local end. If the
%% client is on a private network (the tunnel will be NAT'ed by
%% the gateway), specify the private IPv4 address here.
%%
%% `{clientv6, Client6}' is the IPv6 address of the local end. This
%% address will usually be assigned by the tunnel broker.
%%
%% `{filter_in, Fun}' allows filtering and arbitrary transformation
%% of IPv6 packets received from the network. All packets undergo
%% the mandatory checks specified by RFC 4213 before being passed
%% to user checks.
%%
%% `{filter_out, Fun}' allows filtering and manipulation of IPv6
%% packets received from the tun device.
%%
%% Filtering functions take 2 arguments: the packet payload (a binary)
%% and the tunnel state:
%%
%% ```
%%     -include("sut.hrl").
%%
%%     -record(sut_state, {
%%         serverv4,
%%         clientv4,
%%         clientv6
%%         }.
%% '''
%%
%% Filtering functions should return `ok' to allow the packet or
%% `{ok, binary()}' if the packet has been altered by the function.
%%
%% Any other return value causes the packet to be dropped. The
%% default filter for both incoming and outgoing packets is a noop:
%%
%% ```
%%     fun(_Packet, _State) -> ok end.
%% '''
-spec start_link([options()]) -> 'ignore' | {'error', _} | {'ok', pid()}.
start_link(Opt) when is_list(Opt) ->
    gen_server:start_link(?MODULE, [options(Opt)], []).

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

%% @private
init([
    #state{
        serverv4 = Server,
        clientv4 = Client4,
        clientv6 = Client6,
        ifname = Ifname
    } = State
]) ->
    process_flag(trap_exit, true),

    {ok, FD} = procket:open(0, [
        {protocol, ?IPPROTO_IPV6},
        {type, raw},
        {family, inet}
    ]),

    {ok, Socket} = gen_udp:open(0, [
        binary,
        {fd, FD}
    ]),

    {ok, Dev} = tuncer:create(Ifname, [
        tun,
        no_pi,
        {active, true}
    ]),

    ok = tuncer:up(Dev, Client6),

    {ok, State#state{
        fd = FD,
        s = Socket,
        dev = Dev,
        serverv4 = aton(Server),
        clientv4 = aton(Client4),
        clientv6 = aton(Client6)
    }}.

%% @private
handle_call(destroy, _From, State) ->
    {stop, normal, ok, State}.

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

%%
%% IPv6 encapsulated packet read from socket
%%
%% @private
handle_info(
    {udp, Socket, {SA1, SA2, SA3, SA4}, 0,
        <<4:4, HL:4, _ToS:8, _Len:16, _Id:16, 0:1, _DF:1, _MF:1, _Off:13, _TTL:8, ?IPPROTO_IPV6:8,
            _Sum:16, SA1:8, SA2:8, SA3:8, SA4:8, DA1:8, DA2:8, DA3:8, DA4:8, Data/binary>>},
    #state{
        s = Socket,
        clientv4 = {DA1, DA2, DA3, DA4},
        serverv4 = {SA1, SA2, SA3, SA4},
        dev = Dev
    } = State
) ->
    Opt =
        case (HL - 5) * 4 of
            N when N > 0 -> N;
            _ -> 0
        end,
    <<_:Opt/bits, Payload/bits>> = Data,

    spawn(sut_fw, in, [Dev, Payload, copy(State)]),
    {noreply, State};
% Invalid packet
handle_info({udp, _Socket, Src, 0, Pkt}, State) ->
    error_logger:info_report([
        {error, invalid_packet},
        {source_address, Src},
        {packet, Pkt}
    ]),
    {noreply, State};
% Data from the tun device
handle_info(
    {tuntap, Dev, Data},
    #state{
        dev = Dev,
        s = Socket
    } = State
) ->
    spawn(sut_fw, out, [Socket, Data, copy(State)]),
    {noreply, State};
% WTF?
handle_info(Info, State) ->
    error_logger:error_report([wtf, Info]),
    {noreply, State}.

%% @private
terminate(_Reason, #state{fd = FD}) ->
    procket:close(FD),
    ok.

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

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
-spec aton(string() | inet:ip_address()) -> inet:ip_address().
aton(Address) when is_list(Address) ->
    {ok, N} = inet_parse:address(Address),
    N;
aton(Address) when is_tuple(Address) ->
    Address.

options(Opt) ->
    Fun = ?PROPLIST_TO_RECORD(state),
    Fun(Opt).

copy(#state{
    serverv4 = Serverv4,
    clientv4 = Clientv4,
    clientv6 = Clientv6,

    filter_out = Fout,
    filter_in = Fin,

    error_out = Eout,
    error_in = Ein
}) ->
    #sut_state{
        serverv4 = Serverv4,
        clientv4 = Clientv4,
        clientv6 = Clientv6,

        filter_out = Fout,
        filter_in = Fin,

        error_out = Eout,
        error_in = Ein
    }.