%%% Copyright (c) 2011-2023 Michael Santos . 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 tunctl is an Erlang API for creating and using TUN/TAP interfaces. %% %% tunctl does the actual tun/tap device manipulation. Some functions take %% a device name, others a file descriptor. It is up to the caller to make %% sure the file descriptors are closed (the device will disappear after %% the fd is closed if the device is not persistent). -module(tunctl). -include("tuntap.hrl"). -include_lib("procket/include/ioctl.hrl"). -include_lib("procket/include/procket.hrl"). -callback create(binary(), [options()]) -> {ok, fd(), binary()} | {error, file:posix()}. -callback persist(fd(), boolean()) -> ok | {error, file:posix()}. -callback owner(fd(), integer()) -> ok | {error, file:posix()}. -callback group(fd(), integer()) -> ok | {error, file:posix()}. -export([ create/0, create/1, create/2, persist/2, owner/2, group/2, up/2, up/3, dstaddr/2, broadcast/2, down/1, mtu/3, header/1 ]). -export([ ioctl/3, cmd/1 ]). -type fd() :: integer(). -type uint16_t() :: 0..16#ffff. -type options() :: tun | tap | multi_queue | no_pi | one_queue | vnet_hdr | tun_excl | {namespace, string()}. -export_type([ fd/0, uint16_t/0, options/0 ]). %%-------------------------------------------------------------------- %%% Exports %%-------------------------------------------------------------------- %% @doc Create the tap0 device. %% %% == Examples == %% %% ``` %% 1> tunctl:create(). %% {ok,22,<<"tap0">>} %% ''' %% %% ``` %% $ ip link show tap0 %% 17: tap0: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 %% link/ether 16:ae:f3:36:be:3f brd ff:ff:ff:ff:ff:ff %% ''' -spec create() -> {ok, fd(), binary()} | {error, file:posix()}. create() -> create(<<>>). %% @doc Create a named tap device. %% %% == Examples == %% %% ``` %% 1> tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% ''' %% %% ``` %% $ ip link show tuncer %% 18: tuncer: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 %% link/ether 9e:bf:d9:4a:06:72 brd ff:ff:ff:ff:ff:ff %% ''' -spec create(binary()) -> {ok, fd(), binary()} | {error, file:posix()}. 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'). %% %% 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 %% %% The options default to `[tap, no_pi, {active, false}]'. %% %% == Examples == %% %% ``` %% 1> tunctl:create(<<"tuncer">>, [tun, no_pi]). %% {ok,22,<<"tuncer">>} %% ''' %% %% ``` %% $ ip link show tun0 %% 19: tuncer: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 500 %% link/none %% ''' -spec create(binary(), [options()]) -> {ok, fd(), binary()} | {error, file:posix()}. create(Ifname, Opt) when byte_size(Ifname) < ?IFNAMSIZ, is_list(Opt) -> Module = os(), Module:create(Ifname, Opt). %% @doc Set the interface to exist after the Erlang process exits. %% %% == Support == %% %% * Linux %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>, [tun, no_pi]). %% {ok,22,<<"tuncer">>} %% 3> tunctl:persist(FD, true). %% ok %% ''' -spec persist(fd(), boolean()) -> ok | {error, file:posix()}. persist(FD, Status) -> Module = os(), Module:persist(FD, bool(Status)). %% @doc Set the UID owning the interface. %% %% == Support == %% %% * Linux %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:owner(FD, 1000). %% ok %% ''' -spec owner(fd(), integer()) -> ok | {error, file:posix()}. owner(FD, Owner) when is_integer(FD), is_integer(Owner) -> Module = os(), Module:owner(FD, Owner). %% @doc Set the GID owning the interface. %% %% == Support == %% %% * Linux %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:group(FD, 1000). %% ok %% ''' -spec group(fd(), integer()) -> ok | {error, file:posix()}. group(FD, Group) when is_integer(FD), is_integer(Group) -> Module = os(), Module:group(FD, Group). %% @doc Configure a TUN/TAP device using the default netmask and broadcast for the network. %% %% Configure the interface just like ifconfig except with fewer features %% and no error checking. %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,8}). %% ok %% ''' %% %% ``` %% $ ip a show dev tuncer %% 24: tuncer: mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000 %% link/ether 52:0a:b2:71:f2:cd brd ff:ff:ff:ff:ff:ff %% inet 127.8.8.8/32 brd 127.255.255.255 scope host tuncer %% valid_lft forever preferred_lft forever %% inet6 fe80::500a:b2ff:fe71:f2cd/64 scope link %% valid_lft forever preferred_lft forever %% ''' -spec up(binary(), inet:socket_address()) -> ok | {error, file:posix()}. up(Dev, {A, B, C, D}) -> up(Dev, {A, B, C, D}, 32); up(Dev, {A, B, C, D, E, F, G, H}) -> up(Dev, {A, B, C, D, E, F, G, H}, 64). %% @doc Configure a TUN/TAP device with a netmask. %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,8}, 24). %% ok %% ''' %% %% ``` %% $ ip a show dev tuncer %% 25: tuncer: mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000 %% link/ether 5e:6a:6a:a2:67:0f brd ff:ff:ff:ff:ff:ff %% inet 127.8.8.8/24 brd 127.8.8.255 scope host tuncer %% valid_lft forever preferred_lft forever %% inet6 fe80::5c6a:6aff:fea2:670f/64 scope link %% valid_lft forever preferred_lft forever %% ''' -spec up(binary(), inet:socket_address(), 0..32) -> ok | {error, file:posix()}. up(Dev, Addr, Mask) when byte_size(Dev) < ?IFNAMSIZ, is_integer(Mask) -> Module = os(), case Module of tunctl_linux -> tunctl_linux:up(Dev, Addr, Mask); _ -> os_up(Dev, Addr, Mask) end. %% @doc Configure the remote address for a TUN/TAP device in point-to-point mode. %% %% == Support == %% %% * Linux (IPv4 addresses only) %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,1}). %% ok %% 3> tunctl:dstaddr(Dev, {127,8,8,2}). %% ok %% ''' %% %% ``` %% $ ip a show tun0 %% 26: tuncer: mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000 %% link/ether 8a:0f:74:4e:5e:1c brd ff:ff:ff:ff:ff:ff %% inet 127.8.8.1 peer 127.8.8.2/32 brd 127.255.255.255 scope host tuncer %% valid_lft forever preferred_lft forever %% inet6 fe80::880f:74ff:fe4e:5e1c/64 scope link %% valid_lft forever preferred_lft forever %% ''' -spec dstaddr(binary(), inet:socket_address()) -> ok | {error, file:posix()}. dstaddr(Dev, Addr) when byte_size(Dev) < ?IFNAMSIZ -> Module = os(), case Module of tunctl_linux -> tunctl_linux:dstaddr(Dev, Addr); _ -> % TODO: add support {error, enoent} end. %% @doc Set the MTU (maximum transmission unit) for the TUN/TAP device. %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,1}). %% ok %% 3> tunctl:mtu(FD, Dev, 1400). %% {ok,22,<<"tuncer">>} %% ''' -spec mtu(fd(), binary(), integer()) -> {ok, tunctl:fd(), binary()} | {error, file:posix()}. mtu(FD, Ifname, MTU) -> Module = os(), case Module of tunctl_linux -> tunctl_linux:mtu(FD, Ifname, MTU); _ -> % TODO: add support {error, enoent} end. %% @doc Configure the broadcast address for a TUN/TAP device. %% %% == Support == %% %% * Linux (IPv4 addresses only) %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,1}). %% ok %% 3> tunctl:broadcast(Dev, {127,8,8,3}). %% ok %% ''' %% %% ``` %% 28: tuncer: mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000 %% link/ether 6e:f2:e9:31:d4:3a brd ff:ff:ff:ff:ff:ff %% inet 127.8.8.1/32 brd 127.8.8.3 scope host tuncer %% valid_lft forever preferred_lft forever %% inet6 fe80::6cf2:e9ff:fe31:d43a/64 scope link %% valid_lft forever preferred_lft forever %% ''' -spec broadcast(binary(), inet:socket_address()) -> ok | {error, file:posix()}. broadcast(Dev, Addr) when byte_size(Dev) < ?IFNAMSIZ -> Module = os(), case Module of tunctl_linux -> tunctl_linux:broadcast(Dev, Addr); _ -> % TODO: add support {error, enoent} end. %% @doc Unconfigure a TUN/TAP device. %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,1}). %% ok %% 4> tunctl:down(Dev). %% ok %% ''' -spec down(binary()) -> ok | {error, file:posix()}. down(Dev) when byte_size(Dev) < ?IFNAMSIZ -> Module = os(), case Module of tunctl_linux -> tunctl_linux:down(Dev); _ -> os_down(Dev) end. %% @doc Parse TUN/TAP packet header. %% %% == Examples == %% %% ``` %% 1> {ok, FD, Dev} = tunctl:create(<<"tuncer">>). %% {ok,22,<<"tuncer">>} %% 2> tunctl:up(Dev, {127,8,8,1}). %% ok %% 3> {ok, Bin} = procket:read(FD, 1024). %% {ok,<<51,51,0,0,0,22,254,45,152,87,204,21,134,221,96,0,0, %% 0,0,36,0,1,0,0,0,0,0,...>>} %% 4> tunctl:header(Bin). %% {tun_pi,13107,0, %% <<0,22,254,45,152,87,204,21,134,221,96,0,0,0,0,36,0,1,0, %% 0,0,0,0,0,0,...>>} %% ''' -spec header(binary()) -> {tun_pi, uint16_t(), uint16_t()} | {error, file:posix()}. header(Packet) -> Module = os(), Module:header(Packet). %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- %% @private ioctl(FD, Request, Opt) -> case procket:ioctl(FD, Request, Opt) of {ok, _} -> ok; Error -> Error end. %% @private cmd(Cmd) -> case os:cmd(Cmd) of [] -> ok; Error -> {error, Error} end. bool(true) -> 1; bool(false) -> 0. %% Validate interface name contains only safe characters %% (alphanumeric, hyphen, underscore, dot) to prevent shell injection. validate_ifname(Name) -> case lists:all(fun(C) -> (C >= $a andalso C =< $z) orelse (C >= $A andalso C =< $Z) orelse (C >= $0 andalso C =< $9) orelse C =:= $- orelse C =:= $_ orelse C =:= $. end, Name) of true -> ok; false -> erlang:error(badarg) end. os() -> case os:type() of {unix, linux} -> tunctl_linux; {unix, darwin} -> tunctl_darwin; {unix, freebsd} -> tunctl_freebsd; {unix, netbsd} -> tunctl_netbsd end. %% Shell out to ifconfig on systems where ioctl requires %% root privs (or native code hasn't been written yet). os_up(Dev, {A, B, C, D}, Mask) -> DevStr = binary_to_list(Dev), ok = validate_ifname(DevStr), Cmd = "sudo ifconfig " ++ DevStr ++ " " ++ inet_parse:ntoa({A, B, C, D}) ++ "/" ++ integer_to_list(Mask) ++ " up", cmd(Cmd); os_up(Dev, {A, B, C, D, E, F, G, H}, Mask) -> DevStr = binary_to_list(Dev), ok = validate_ifname(DevStr), Cmd = "sudo ifconfig " ++ DevStr ++ " inet6 add " ++ inet_parse:ntoa({A, B, C, D, E, F, G, H}) ++ "/" ++ integer_to_list(Mask) ++ " up", cmd(Cmd). os_down(Dev) -> % BSD systems don't destroy the interface so clean up % any IPv6 addresses _ = os_ipv6_down(Dev), DevStr = binary_to_list(Dev), ok = validate_ifname(DevStr), Cmd = "sudo ifconfig " ++ DevStr ++ " down", cmd(Cmd). os_ipv6_down(Dev) -> case os:type() of {unix, linux} -> ok; {unix, BSD} when BSD == freebsd; BSD == darwin -> os_ipv6_down_1(Dev) end. os_ipv6_down_1(Ifname) -> Dev = binary_to_list(Ifname), {ok, Devs} = inet:getifaddrs(), Attr = proplists:get_value(Dev, Devs), [ os_ipv6_down_2(Dev, Addr) || Addr <- proplists:get_all_values(addr, Attr), tuple_size(Addr) == 8 ]. os_ipv6_down_2(Dev, Addr) -> Cmd = "sudo ifconfig " ++ Dev ++ " inet6 " ++ inet_parse:ntoa(Addr) ++ " -alias", cmd(Cmd).