sdlan/apps/sdlan/src/sdlan_stun.erl
2026-03-11 15:54:02 +08:00

178 lines
6.9 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @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).
-record(state, {
socket
}).
%%%===================================================================
%%% 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()),
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, <<?PACKET_STUN_REQUEST:8, Body/binary>>}, State = #state{socket = Sock}) ->
StunRequest = catch sdlan_pb:decode_msg(Body, sdl_stun_request),
%% 告知网络当前的ip对应的nat的映射关系
maybe
#sdl_stun_request{cookie = Cookie, 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(#sdl_stun_reply{
cookie = Cookie
}),
%logger:debug("[sdlan_stun] stun_request network_id: ~p, client_id: ~p, hole: ~p", [NetworkId, ClientId, {Ip, Port}])
ok = gen_udp:send(Sock, Ip, Port, <<?PACKET_STUN_REPLY:8, StunReply/binary>>)
end,
{noreply, State};
%% 网络nat类型的探测机制, 需要借助其他服务一起才能实现
%% 辅助节点没有assist的配置不支持attr = 2的探测
handle_info({udp, Sock, ClientIp, ClientPort, <<?PACKET_STUN_PROBE:8, Body/binary>>}, State = #state{socket = Sock}) ->
StunProbe = catch sdlan_pb:decode_msg(Body, sdl_stun_probe),
maybe
#sdl_stun_probe{cookie = Cookie, attr = Attr} ?= StunProbe,
logger:debug("[sdlan_stun] get stun_probe request, att: ~p", [Attr]),
ProbeReplyPkt = sdlan_pb:encode_msg(#sdl_stun_probe_reply {
cookie = Cookie,
port = ClientPort,
ip = int_ip(ClientIp)
}),
case Attr of
?STUN_ATTR_CHANGE_NONE ->
ok = gen_udp:send(Sock, ClientIp, ClientPort, <<?PACKET_STUN_PROBE_REPLY, ProbeReplyPkt/binary>>);
?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, <<?PACKET_STUN_DATA, Body/binary>>}, State = #state{socket = Sock}) ->
Data = catch sdlan_pb:decode_msg(Body, sdl_data),
maybe
#sdl_data{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} ?= Data,
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
%% 重新打包数据ttl需要减1
NData = sdlan_pb:encode_msg(Data#sdl_data{ttl = TTL - 1, is_p2p = false}),
sdlan_network:forward(NetworkPid, Sock, SrcMac, DstMac, <<?PACKET_STUN_DATA, NData/binary>>)
end,
{noreply, State};
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:32>> = <<Ip0, Ip1, Ip2, Ip3>>,
Ip.