%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2024, %%% @doc %%% %%% @end %%% Created : 09. 4月 2024 17:37 %%%------------------------------------------------------------------- -module(sdlan_stun). -author("anlicheng"). -include("sdlan.hrl"). -include("sdlan_pb.hrl"). -behaviour(gen_server). %% API -export([start_link/2]). -export([get_name/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -define(FLOW_REPORT_INTERVAL, 60 * 1000). -record(state, { socket, flow_counters = #{} }). %%%=================================================================== %%% API %%%=================================================================== -spec get_name(Id :: integer()) -> atom(). get_name(Id) when is_integer(Id) -> list_to_atom("sdlan_stun:" ++ integer_to_list(Id)). %% @doc Spawns the server and registers the local name (unique) -spec(start_link(Name :: atom(), Port :: integer()) -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). start_link(Name, Port) when is_atom(Name), is_integer(Port) -> gen_server:start_link({local, Name}, ?MODULE, [Port], []). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %% @private %% @doc Initializes the server -spec(init(Args :: term()) -> {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {stop, Reason :: term()} | ignore). init([Port]) -> %% 需要提高进程的调度优先级 erlang:process_flag(priority, high), Opts = [ binary, {reuseaddr, true}, {reuseport, true}, {active, true}, {recbuf, 5 * 1024 * 1024}, {sndbuf, 5 * 1024 * 1024} ], {ok, Socket} = gen_udp:open(Port, Opts), inet_udp:controlling_process(Socket, self()), erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker), logger:debug("[sdlan_stun] start at port: ~p", [Port]), {ok, #state{socket = Socket}}. %% @private %% @doc Handling call messages -spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, State :: #state{}) -> {reply, Reply :: term(), NewState :: #state{}} | {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), NewState :: #state{}}). handle_call(_Request, _From, State = #state{}) -> {reply, ok, State}. %% @private %% @doc Handling cast messages -spec(handle_cast(Request :: term(), State :: #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). %% 当前node下的转发,基于进程间的通讯 handle_cast(_Request, State) -> {noreply, State}. %% @private %% @doc Handling all non call/cast messages -spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). handle_info({udp, Sock, Ip, Port, <>}, State = #state{socket = Sock}) -> StunRequest = catch sdlan_pb:decode_msg(Body, 'SDLStunRequest'), %% 告知网络当前的ip对应的nat的映射关系 maybe #'SDLStunRequest'{session_token = SessionToken, client_id = ClientId, network_id = NetworkId, mac = Mac, nat_type = NatType, v6_info = V6Info} ?= StunRequest, {ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId), sdlan_network:update_hole(NetworkPid, SessionToken, ClientId, Mac, {Ip, Port}, NatType, V6Info), StunReply = sdlan_pb:encode_msg(#'SDLStunReply'{}), ok = gen_udp:send(Sock, Ip, Port, <>), logger:debug("[sdlan_stun] stun_request network_id: ~p, client_id: ~p, mac: ~p, hole: ~p, replied", [NetworkId, ClientId, sdlan_util:format_mac(Mac), {Ip, Port}]) end, {noreply, State}; %% 网络nat类型的探测机制, 需要借助其他服务一起才能实现 %% 辅助节点没有assist的配置,不支持attr = 2的探测 handle_info({udp, Sock, ClientIp, ClientPort, <>}, State = #state{socket = Sock}) -> StunProbe = catch sdlan_pb:decode_msg(Body, 'SDLStunProbe'), maybe #'SDLStunProbe'{cookie = Cookie, attr = Attr} ?= StunProbe, logger:debug("[sdlan_stun] get stun_probe request, att: ~p", [Attr]), ProbeReplyPkt = sdlan_pb:encode_msg(#'SDLStunProbeReply' { cookie = Cookie, port = ClientPort, ip = int_ip(ClientIp) }), case Attr of ?STUN_ATTR_CHANGE_NONE -> ok = gen_udp:send(Sock, ClientIp, ClientPort, <>); ?STUN_ATTR_CHANGE_PORT -> %% 切换端口和ip sdlan_stun_peer_assist:stun_relay(ClientIp, ClientPort, ProbeReplyPkt); ?STUN_ATTR_CHANGE_PEER -> %% 切换端口返回 sdlan_stun_port_assist:stun_relay(ClientIp, ClientPort, ProbeReplyPkt) end end, {noreply, State}; handle_info({udp, _, _Ip, _Port, <>}, State = #state{socket = Sock, flow_counters = FlowCounters}) -> Data = catch sdlan_pb:decode_msg(Body, 'SDLData'), NFlowCounters = maybe #'SDLData'{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} ?= Data, logger:debug("[sdlan_stun] forward data, network_id: ~p", [NetworkId]), %% 重新打包数据ttl需要减1 NData = sdlan_pb:encode_msg(Data#'SDLData'{ttl = TTL - 1, is_p2p = false}), case sdlan_network:forward_by_ets(NetworkId, Sock, SrcMac, DstMac, <>) of {ok, ForwardBytes} -> incr_flow_counter(NetworkId, ForwardBytes, FlowCounters); {error, _Reason} -> FlowCounters end else _ -> FlowCounters end, {noreply, State#state{flow_counters = NFlowCounters}}; handle_info({timeout, _, flow_report_ticker}, State = #state{flow_counters = FlowCounters}) -> erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker), maps:foreach(fun(NetworkId, ForwardBytes) -> catch sdlan_api:network_forward_report(NetworkId, ForwardBytes) end, FlowCounters), {noreply, State#state{flow_counters = #{}}}; handle_info(Info, State) -> logger:error("[sdlan_stun] get a unknown message: ~p, channel will closed", [Info]), {noreply, State}. %% @private %% @doc This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), State :: #state{}) -> term()). terminate(_Reason, _State = #state{}) -> ok. %% @private %% @doc Convert process state when code is changed -spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{}, Extra :: term()) -> {ok, NewState :: #state{}} | {error, Reason :: term()}). code_change(_OldVsn, State = #state{}, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec int_ip(tuple()) -> integer(). int_ip({Ip0, Ip1, Ip2, Ip3}) -> <> = <>, Ip. -spec incr_flow_counter(NetworkId :: integer(), ForwardBytes :: integer(), Counters :: map()) -> map(). incr_flow_counter(NetworkId, ForwardBytes, Counters) when is_integer(NetworkId), is_integer(ForwardBytes), is_map(Counters) -> maps:update_with(NetworkId, fun(V) -> V + ForwardBytes end, ForwardBytes, Counters).