%%% -*- erlang -*- %%% This file is part of erlang-nat released under the MIT license. %%% See the NOTICE for more information. %%% %%% Copyright (c) 2016-2018 Benoît Chesneau -module(natpmp). -export([get_device_address/1]). -export([get_external_address/1]). -export([get_external_address_with_epoch/1]). -export([get_internal_address/1]). -export([discover/0]). -export([add_port_mapping/4, add_port_mapping/5]). -export([delete_port_mapping/4]). -export([delete_all_mappings/2]). -export([system_gateways/0, potential_gateways/0]). -include("nat.hrl"). -define(NAT_PMP_PORT, 5351). -type natpmp_error() :: unsupported_version | not_authorized | network_failure | out_of_resource | unsupported_opcode | bad_response. -export_types([natpmp_error/0]). -spec get_device_address(Gateway) -> {ok, Ip} | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), Ip :: inet:ip_address() | inet:hostname(), Reason :: natpmp_error(). get_device_address(Gateway) -> {ok, Gateway}. %% @doc get external ip -spec get_external_address(Gateway) -> {ok, ExternalIp} | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), ExternalIp :: inet:ip_address() | inet:hostname(), Reason :: natpmp_error(). get_external_address(Gateway) -> case get_external_address_with_epoch(Gateway) of {ok, Ip, _Since} -> {ok, inet:ntoa(Ip)}; Error -> Error end. %% @doc get external ip with epoch time for freshness detection -spec get_external_address_with_epoch(Gateway) -> {ok, ExternalIp, Since} | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), ExternalIp :: inet:ip_address(), Since :: non_neg_integer(), Reason :: natpmp_error(). get_external_address_with_epoch(Gateway) -> Msg = << 0, 0 >>, nat_rpc(Gateway, Msg, 0). %% @doc get internal address used for this gateway -spec get_internal_address(Gateway) -> {ok, InternalIp} when Gateway :: inet:ip_address() | inet:hostname(), InternalIp :: inet:ip_address() | inet:hostname(). get_internal_address(Gateway) -> {ok, inet_ext:get_internal_address(Gateway)}. discover_with_addr(Parent, Ref, Addr) -> case (catch natpmp:get_external_address(Addr)) of {ok, _Ip} -> Parent ! {nat, Ref, self(), Addr}; _Else -> ok end. potential_gateways() -> Net_10 = inet_cidr:parse("10.0.0.0/8"), Net_172_16 = inet_cidr:parse("172.16.0.0/12"), Net_192_168 = inet_cidr:parse("192.168.0.0/16"), Networks = [Net_10, Net_172_16, Net_192_168], lists:foldl(fun({_, {Addr, Mask}}, Acc) -> case inet_ext:is_private_address(Networks) of true -> case inet_cidr:is_ipv4(Addr) of true -> Ip0 = mask(Addr, Mask), Ip = setelement(4, Ip0, element(4, Ip0) bor 1), [Ip | Acc]; false -> Acc end; false -> Acc end end, [], inet_ext:routes()). system_gateways() -> [Ip || {_, Ip} <- inet_ext:gateways(), is_list(Ip), Ip =/= []]. %% @doc discover a Nat gateway -spec discover() -> {ok, Gateway} | {error, any()} when Gateway :: inet:ip_address(). discover() -> IPs = case system_gateways() of [] -> potential_gateways(); Gateways -> Gateways end, Ref = make_ref(), Self = self(), Workers = lists:foldl(fun(Ip, Acc) -> Pid = spawn_link(fun() -> discover_with_addr(Self, Ref, Ip) end), erlang:monitor(process, Pid), [Pid | Acc] end, [], lists:usort(IPs)), discover_wait(Workers, Ref). discover_wait([], _Ref) -> {error, no_nat}; discover_wait(Workers, Ref) -> receive {nat, Ref, WorkerPid, GatewayIp} -> lists:foreach(fun(Pid) -> catch unlink(Pid), catch exit(Pid, shutdown), receive {'DOWN', _, _, Pid, _} -> ok end end, Workers -- [WorkerPid]), {ok, GatewayIp}; {'DOWN', _MRef, _Type, WorkerPid, _Info} -> discover_wait(Workers -- [WorkerPid], Ref) end. %% @doc add a port mapping with default lifetime -spec add_port_mapping(Gateway, Protocol, InternalPort, ExternalPortRequest) -> {ok, Since, InternalPort, ExternalPort, MappingLifetime} | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), Protocol :: tcp | udp, InternalPort :: non_neg_integer(), ExternalPortRequest :: non_neg_integer(), Since :: non_neg_integer(), ExternalPort :: non_neg_integer(), MappingLifetime :: non_neg_integer(), Reason :: natpmp_error(). add_port_mapping(Gateway, Protocol, InternalPort, ExternalPort) -> add_port_mapping(Gateway, Protocol, InternalPort, ExternalPort, ?RECOMMENDED_MAPPING_LIFETIME_SECONDS). %% @doc add a port mapping -spec add_port_mapping(Gateway, Protocol, InternalPort, ExternalPortRequest, Lifetime) -> {ok, Since, InternalPort, ExternalPort, MappingLifetime} | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), Protocol :: tcp | udp, InternalPort :: non_neg_integer(), ExternalPortRequest :: non_neg_integer(), Lifetime :: non_neg_integer(), Since :: non_neg_integer(), ExternalPort :: non_neg_integer(), MappingLifetime :: non_neg_integer(), Reason :: natpmp_error(). add_port_mapping(Gateway, Protocol, InternalPort, ExternalPort, Lifetime) -> OpCode = case Protocol of udp -> 1; tcp -> 2; _ -> error(unknown_protocol) end, Msg = << 0, OpCode, 0:16, InternalPort:16/unsigned-integer, ExternalPort:16/unsigned-integer, Lifetime:32/unsigned-integer >>, nat_rpc(Gateway, Msg, OpCode). %% @doc delete a port mapping -spec delete_port_mapping(Gateway, Protocol, InternalPort, ExternalPortRequest) -> ok | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), Protocol :: tcp | udp, InternalPort :: non_neg_integer(), ExternalPortRequest :: non_neg_integer(), Reason :: natpmp_error(). delete_port_mapping(Gateway, Protocol, InternalPort, _ExternalPort) -> %% From https://tools.ietf.org/html/rfc6886#section-3.4 : %% %% A client requests explicit deletion of a mapping by sending a message to %% the NAT gateway requesting the mapping, with the Requested Lifetime in %% Seconds set to zero. The Suggested External Port MUST be set to zero by %% the client on sending, and MUST be ignored by the gateway on reception. case add_port_mapping(Gateway, Protocol, InternalPort, 0, 0) of {ok, _, InternalPort, 0, 0} -> ok; {ok, _, _, _, _} -> {error, bad_response}; Error -> Error end. %% @doc delete all port mappings for a protocol %% RFC 6886 Section 3.4: To delete all mappings for a protocol, %% set internal_port=0, external_port=0, lifetime=0 -spec delete_all_mappings(Gateway, Protocol) -> ok | {error, Reason} when Gateway :: inet:ip_address() | inet:hostname(), Protocol :: tcp | udp, Reason :: natpmp_error(). delete_all_mappings(Gateway, Protocol) -> case add_port_mapping(Gateway, Protocol, 0, 0, 0) of {ok, _, 0, 0, 0} -> ok; {ok, _, _, _, _} -> {error, bad_response}; Error -> Error end. %% --------------------- %% - private functions - %% --------------------- %% nat_rpc(Gateway0, Msg, OpCode) -> Gateway = inet_ext:parse_address(Gateway0), {ok, Sock} = gen_udp:open(0, [{active, once}, inet, binary]), try nat_rpc1(Sock, Gateway, Msg, OpCode, 0) after gen_udp:close(Sock) end. nat_rpc1(_Sock, _Gateway, _Msg, _OpCode, ?NAT_TRIES) -> timeout; nat_rpc1(Sock, Gateway, Msg, OpCode, Tries) -> inet:setopts(Sock, [{active, once}]), Timeout = ?NAT_INITIAL_MS bsl Tries, case gen_udp:send(Sock, Gateway, ?NAT_PMP_PORT, Msg) of ok -> receive {udp, _Sock, Gateway, _Port, Packet} -> parse_response(Packet, OpCode); {udp, _, _, _, _} -> nat_rpc1(Sock, Gateway, Msg, OpCode, Tries + 1) after Timeout -> nat_rpc1(Sock, Gateway, Msg, OpCode, Tries + 1) end; _Error -> nat_rpc1(Sock, Gateway, Msg, OpCode, Tries + 1) end. parse_response(<< _Version, ResponseCode, Status:16/unsigned-integer, Since:32/unsigned-integer, A, B, C, D >>, OpCode) -> ExpectedCode = OpCode + 128, if ExpectedCode =:= ResponseCode -> case parse_status(Status) of ok -> {ok, {A, B, C, D}, Since}; Error -> Error end; true -> {error, bad_response} end; parse_response(<< _Version, ResponseCode, Status:16/unsigned-integer, Since:32/unsigned-integer, InternalPort:16/unsigned-integer, ExternalPort:16/unsigned-integer, Lifetime:32/unsigned-integer >>, OpCode) -> ExpectedCode = OpCode + 128, if ExpectedCode =:= ResponseCode -> case parse_status(Status) of ok -> {ok, Since, InternalPort, ExternalPort, Lifetime}; Error -> Error end; true -> {error, bad_response} end; parse_response(_, _) -> {error, bad_response}. parse_status(0) -> ok; parse_status(1) -> {error, unsupported_version}; parse_status(2) -> {error, not_authorized}; parse_status(3) -> {error, network_failure}; parse_status(4) -> {error, out_of_resource}; parse_status(5) -> {error, unsupported_opcode}. %% apply mask to the ip mask({A, B, C, D}, {E, F, G, H}) -> {A band E, B band F, C band G, D band H}.