%%%---------------------------------------------------------------------- %%% File : stun_acceptor.erl %%% Author : Holger Weiss %%% Purpose : STUN/TURN acceptor %%% Created : 3 Jul 2022 by Holger Weiss %%% %%% %%% Copyright (C) 2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%---------------------------------------------------------------------- -module(stun_acceptor). -author('holger@zedat.fu-berlin.de'). -export([start/4, stop/1]). -export([start_link/4]). -export([init/4]). -include("stun_logger.hrl"). -define(TCP_SEND_TIMEOUT, 10000). -define(UDP_READ_PACKETS, 100). -define(UDP_RECBUF, 1024 * 1024). % 1 MiB -type ip() :: inet:ip_address(). -type port_number() :: inet:port_number(). -type udp_socket() :: gen_udp:socket(). -type tcp_socket() :: gen_tcp:socket(). -type transport() :: udp | tcp | tls | auto. -type opts() :: proplists:proplist(). %% API. -spec start(ip(), port_number(), transport(), opts()) -> {ok, pid()} | {error, term()}. start(IP, Port, Transport, Opts) -> supervisor:start_child(stun_acceptor_sup, [IP, Port, Transport, Opts]). -spec stop(pid()) -> ok | {error, term()}. stop(Pid) -> supervisor:terminate_child(stun_acceptor_sup, Pid). -spec start_link(ip(), port_number(), transport(), opts()) -> {ok, pid()} | {error, term()}. start_link(IP, Port, Transport, Opts) -> proc_lib:start_link(?MODULE, init, [IP, Port, Transport, Opts]). -spec init(ip(), port_number(), transport(), opts()) -> no_return(). init(IP, Port, Transport0, Opts0) when Transport0 == tcp; Transport0 == tls; Transport0 == auto -> {Transport, Opts} = case {Transport0, proplists:get_value(tls, Opts0)} of {tcp, false} -> {tcp, Opts0}; {tcp, true} -> {tls, Opts0}; {tcp, optional} -> {auto, Opts0}; {tls, undefined} -> {tls, [tls | Opts0]}; {auto, undefined} -> {auto, [{tls, optional} | Opts0]}; {_Transport, _TLS} -> {Transport0, Opts0} end, case listen(Transport, Port, [binary, {ip, IP}, {packet, 0}, {active, false}, {reuseaddr, true}, {nodelay, true}, {keepalive, true}, {send_timeout, ?TCP_SEND_TIMEOUT}, {send_timeout_close, true}]) of {ok, ListenSocket} -> Opts1 = stun:tcp_init(ListenSocket, Opts), proc_lib:init_ack({ok, self()}), accept(Transport, ListenSocket, Opts1); {error, Reason} -> log_error(IP, Port, Transport, Opts, Reason), exit(Reason) end; init(IP, Port, udp, Opts) -> case gen_udp:open(Port, [binary, {ip, IP}, {active, false}, {recbuf, ?UDP_RECBUF}, {read_packets, ?UDP_READ_PACKETS}, {reuseaddr, true}]) of {ok, Socket} -> stun_logger:set_metadata(listener, udp), Opts1 = stun:udp_init(Socket, Opts), proc_lib:init_ack({ok, self()}), udp_recv(Socket, Opts1); {error, Reason} -> log_error(IP, Port, udp, Opts, Reason), exit(Reason) end. %% Internal functions. -spec listen(transport(), port_number(), opts()) -> {ok, tcp_socket()} | {error, term()}. -ifdef(USE_OLD_INET_BACKEND). listen(_Transport, Port, Opts) -> gen_tcp:listen(Port, Opts). -else. listen(auto, Port, Opts) -> gen_tcp:listen(Port, [{inet_backend, socket} | Opts]); listen(_Transport, Port, Opts) -> gen_tcp:listen(Port, Opts). -endif. -spec accept(transport(), tcp_socket(), opts()) -> no_return(). accept(Transport, ListenSocket, Opts) -> Proxy = proplists:get_bool(proxy_protocol, Opts), ID = stun_logger:make_id(), Opts1 = [{session_id, ID} | Opts], stun_logger:set_metadata(listener, Transport, ID), case gen_tcp:accept(ListenSocket) of {ok, Socket} when Proxy -> case p1_proxy_protocol:decode(gen_tcp, Socket, 10000) of {{Addr, Port}, {PeerAddr, PeerPort}} = SP -> Opts2 = [{sock_peer_name, SP} | Opts1], ?LOG_INFO("Accepting proxied connection: ~s -> ~s", [stun_logger:encode_addr({PeerAddr, PeerPort}), stun_logger:encode_addr({Addr, Port})]), case stun:start({gen_tcp, Socket}, Opts2) of {ok, Pid} -> gen_tcp:controlling_process(Socket, Pid); {error, Reason} -> ?LOG_NOTICE("Cannot start connection: ~s", [Reason]), gen_tcp:close(Socket) end; {error, Reason} -> ?LOG_ERROR("Cannot parse proxy protocol: ~s", [inet:format_error(Reason)]), gen_tcp:close(Socket); {undefined, undefined} -> ?LOG_ERROR("Cannot parse proxy protocol: unknown protocol"), gen_tcp:close(Socket) end; {ok, Socket} -> case {inet:peername(Socket), inet:sockname(Socket)} of {{ok, {PeerAddr, PeerPort}}, {ok, {Addr, Port}}} -> ?LOG_INFO("Accepting connection: ~s -> ~s", [stun_logger:encode_addr({PeerAddr, PeerPort}), stun_logger:encode_addr({Addr, Port})]), case stun:start({gen_tcp, Socket}, Opts1) of {ok, Pid} -> gen_tcp:controlling_process(Socket, Pid); {error, Reason} -> ?LOG_NOTICE("Cannot start connection: ~s", [Reason]), gen_tcp:close(Socket) end; Err -> ?LOG_NOTICE("Cannot fetch peername: ~p", [Err]), gen_tcp:close(Socket) end; {error, Reason} when Reason /= closed -> ?LOG_NOTICE("Cannot accept connection: ~s", [inet:format_error(Reason)]) end, accept(Transport, ListenSocket, Opts). -spec udp_recv(udp_socket(), opts()) -> no_return(). udp_recv(Socket, Opts) -> case gen_udp:recv(Socket, 0) of {ok, {Addr, Port, Packet}} -> NewOpts = stun:udp_recv(Socket, Addr, Port, Packet, Opts), udp_recv(Socket, NewOpts); {error, Reason} when Reason /= closed -> ?LOG_NOTICE("Cannot receive UDP packet: ~s", [inet:format_error(Reason)]), udp_recv(Socket, Opts) end. -spec log_error(ip(), port_number(), transport(), opts(), term()) -> any(). log_error(IP, Port, Transport, Opts, Reason) -> ?LOG_DEBUG("Cannot start listener:~n" "** IP: ~s~n" "** Port: ~B~n" "** Transport: ~s~n" "** Options: ~p~n" "** Reason: ~s", [stun_logger:encode_addr(IP), Port, stun_logger:encode_transport(Transport), Opts, inet:format_error(Reason)]).