%%%-------------------------------------------------------------------
%% @doc Thin TUN/TAP wrapper around tuncer.
%%%-------------------------------------------------------------------
-module(vpn_tun).
-behaviour(gen_server).
-export([open/2, open/3, close/1, devname/1, write/2]).
-export([start_link/2, start_link/3, start_link/4, stop/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
open(Name, Ip) ->
open(Name, Ip, tap).
open(Name, Ip, Mode) ->
case os:type() of
{unix, darwin} ->
open_utun(Name, Ip);
{unix, linux} ->
case tuncer:create(Name, tun_options(Mode)) of
{ok, Ref} ->
up_or_destroy(Ref, Ip);
{error, Reason} ->
{error, Reason}
end;
Other ->
logger:warning("Unsupported OS ~p, attempting fallback with tuncer", [Other]),
case tuncer:create(Name, tun_options(Mode)) of
{ok, Ref} ->
up_or_destroy(Ref, Ip);
{error, Reason} ->
{error, Reason}
end
end.
close({utun_port, Port}) ->
catch erlang:port_close(Port),
ok;
close(Ref) ->
tuncer:destroy(Ref).
devname({utun_port, _Port, _Socket, Name}) ->
Name;
devname(Ref) ->
tuncer:devname(Ref).
start_link(Name, Ip) ->
start_link(Name, Ip, undefined).
start_link(Name, Ip, OwnerPid) ->
start_link(Name, Ip, OwnerPid, tap).
start_link(Name, Ip, OwnerPid, Mode) ->
gen_server:start_link(?MODULE, {Name, Ip, OwnerPid, Mode}, []).
stop(Pid) ->
gen_server:stop(Pid).
write(Pid, Packet) when is_binary(Packet) ->
gen_server:call(Pid, {write, Packet}).
init({Name, Ip, OwnerPid, Mode}) ->
case open(Name, Ip, Mode) of
{ok, {utun_port, Port, Socket, ActualName}} ->
{ok, #{ref => {utun_port, Port},
fd => Socket,
name => ActualName,
ip => Ip,
mode => Mode,
owner => OwnerPid,
mock => false}};
{ok, Ref} ->
Fd = tuncer:getfd(Ref),
{ok, #{ref => Ref,
fd => Fd,
name => Name,
ip => Ip,
mode => Mode,
owner => OwnerPid,
mock => false}};
{error, Reason} ->
logger:warning("Failed to open TUN/TAP device ~s (~p): ~p. Falling back to mock/dummy mode.", [Name, Mode, Reason]),
{ok, #{ref => undefined,
fd => undefined,
name => Name,
ip => Ip,
mode => Mode,
owner => OwnerPid,
mock => true}}
end.
handle_call({write, _Packet}, _From, State = #{mock := true}) ->
{reply, ok, State};
handle_call({write, Packet}, _From, State = #{fd := Fd, ref := {utun_port, _}}) ->
Header = family_header(Packet),
Payload = <<Header/binary, Packet/binary>>,
{reply, normalize_write_reply(tuncer:write(Fd, Payload)), State};
handle_call({write, Packet}, _From, State = #{fd := Fd}) ->
{reply, normalize_write_reply(tuncer:write(Fd, Packet)), State};
handle_call(_Request, _From, State) ->
{reply, {error, not_implemented}, State}.
handle_cast(_Request, State) ->
{noreply, State}.
handle_info({tuntap, Ref, Packet}, State = #{ref := Ref}) ->
logger:info("vpn_tun received packet: ~p bytes", [byte_size(Packet)]),
maybe_send_packet(Packet, State),
{noreply, State};
handle_info({Port, {data, Data}}, State = #{ref := {utun_port, Port}}) ->
case Data of
<<_Family:32/big, IpPacket/binary>> ->
logger:info("vpn_tun received packet (utun): ~p bytes", [byte_size(IpPacket)]),
maybe_send_packet(IpPacket, State);
_ ->
logger:warning("vpn_tun received invalid/short packet (utun): ~p bytes", [byte_size(Data)])
end,
{noreply, State};
handle_info({tuntap, _OtherRef, _Packet}, State) ->
{noreply, State};
handle_info(_Message, State) ->
{noreply, State}.
terminate(_Reason, #{ref := Ref, mock := false}) ->
_ = close(Ref),
ok;
terminate(_Reason, _State) ->
ok.
up_or_destroy(Ref, Ip) ->
case tuncer:up(Ref, Ip) of
ok ->
{ok, Ref};
{error, Reason} ->
_ = tuncer:destroy(Ref),
{error, Reason}
end.
maybe_send_packet(_Packet, #{owner := undefined}) ->
ok;
maybe_send_packet(Packet, #{owner := OwnerPid}) ->
OwnerPid ! {vpn_tun_packet, self(), Packet},
ok.
normalize_write_reply(ok) ->
ok;
normalize_write_reply({error, Reason}) ->
{error, Reason};
normalize_write_reply({ok, Size}) ->
{error, {partial_write, Size}}.
tun_options(tap) ->
[tap, no_pi, {active, true}];
tun_options(tun) ->
[tun, no_pi, {active, true}].
open_utun(Name, Ip) ->
StartUnit = parse_unit(Name),
open_utun_try(StartUnit, Name, Ip).
open_utun_try(Unit, Name, Ip) ->
case procket:socket(32, 2, 2) of
{ok, Socket} ->
NameBin = <<"com.apple.net.utun_control">>,
Padding = 96 - byte_size(NameBin),
CtlInfo = <<0:32, NameBin/binary, 0:(Padding*8)>>,
case procket:ioctl(Socket, 16#c0644e03, CtlInfo) of
{ok, <<CtlId:32/native, _/binary>>} ->
SockAddrCtl = <<32:8, 32:8, 2:16/native, CtlId:32/native, Unit:32/native, 0:(20*8)>>,
case procket:connect(Socket, SockAddrCtl) of
ok ->
case procket:getsockopt(Socket, 2, 2, <<0:256>>) of
{ok, RawIfName} ->
[ActualName | _] = binary:split(RawIfName, <<0>>),
logger:info("macOS utun interface opened: requested=~s (unit ~p) allocated=~s fd=~p", [Name, Unit, ActualName, Socket]),
case parse_ip(Ip) of
{ok, Addr} ->
IpStr = inet_parse:ntoa(Addr),
PeerAddr = calculate_peer_ip(Addr),
PeerIpStr = inet_parse:ntoa(PeerAddr),
Cmd = "sudo ifconfig " ++ binary_to_list(ActualName) ++ " " ++ IpStr ++ " " ++ PeerIpStr ++ " up",
case os:cmd(Cmd) of
[] ->
try erlang:open_port({fd, Socket, Socket}, [binary, stream]) of
Port ->
{ok, {utun_port, Port, Socket, ActualName}}
catch
error:Err ->
procket:close(Socket),
{error, {port_open_failed, Err}}
end;
Error ->
procket:close(Socket),
{error, {ip_config_failed, Error}}
end;
{error, Reason} ->
procket:close(Socket),
{error, {invalid_ip_format, Reason}}
end;
{error, Reason} ->
procket:close(Socket),
{error, {getsockopt_failed, Reason}}
end;
{error, ebusy} ->
procket:close(Socket),
logger:debug("utun unit ~p is busy, trying next unit", [Unit]),
open_utun_try(Unit + 1, Name, Ip);
{error, Reason} ->
procket:close(Socket),
{error, {connect_failed, Reason}}
end;
{error, Reason} ->
procket:close(Socket),
{error, {ioctl_failed, Reason}}
end;
{error, Reason} ->
{error, {socket_failed, Reason}}
end.
parse_unit(Name) when is_binary(Name) ->
parse_unit(binary_to_list(Name));
parse_unit(Name) when is_list(Name) ->
case re:run(Name, "u?tun([0-9]+)", [{capture, [1], list}]) of
{match, [Digits]} ->
list_to_integer(Digits) + 1;
_ ->
0
end.
parse_ip(Ip) when is_list(Ip) ->
inet_parse:address(Ip);
parse_ip(Ip) when is_tuple(Ip) ->
{ok, Ip};
parse_ip(Ip) when is_binary(Ip) ->
inet_parse:address(binary_to_list(Ip)).
family_header(<<4:4, _/bits>>) -> <<2:32/big>>;
family_header(<<6:4, _/bits>>) -> <<30:32/big>>;
family_header(_) -> <<2:32/big>>.
calculate_peer_ip({A, B, C, D}) ->
{A, B, C, D bxor 3};
calculate_peer_ip(Other) ->
Other.