简化dns逻辑
This commit is contained in:
parent
99752ddd2e
commit
ed01e7dec9
21
apps/sdlan/include/dns_proxy.hrl
Normal file
21
apps/sdlan/include/dns_proxy.hrl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 12月 2025 11:41
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-record(dns_cache, {
|
||||||
|
%% {Qname, QType, QClass}
|
||||||
|
key,
|
||||||
|
answers = [],
|
||||||
|
authority = [],
|
||||||
|
additional = [],
|
||||||
|
rc :: integer(),
|
||||||
|
flags = #{},
|
||||||
|
% unix time
|
||||||
|
expire_at :: integer()
|
||||||
|
}).
|
||||||
59
apps/sdlan/src/dns_proxy/dns_cache.erl
Normal file
59
apps/sdlan/src/dns_proxy/dns_cache.erl
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
-module(dns_cache).
|
||||||
|
-include_lib("dns_proxy.hrl").
|
||||||
|
-include_lib("dns_erlang/include/dns.hrl").
|
||||||
|
-include_lib("dns_erlang/include/dns_records.hrl").
|
||||||
|
-include_lib("dns_erlang/include/dns_terms.hrl").
|
||||||
|
|
||||||
|
-export([init/0, lookup/1, insert/2]).
|
||||||
|
|
||||||
|
-define(TABLE, dns_cache).
|
||||||
|
|
||||||
|
init() ->
|
||||||
|
ets:new(?TABLE, [named_table, set, public, {keypos, 2}, {read_concurrency, true}]).
|
||||||
|
|
||||||
|
lookup(#dns_query{name = Qname, type = QType, class = QClass}) ->
|
||||||
|
Key = {Qname, QType, QClass},
|
||||||
|
case ets:lookup(?TABLE, Key) of
|
||||||
|
[Cache = #dns_cache{expire_at = ExpireAt}] ->
|
||||||
|
Now = os:system_time(second),
|
||||||
|
case ExpireAt > Now of
|
||||||
|
true ->
|
||||||
|
{hit, Cache};
|
||||||
|
false ->
|
||||||
|
true = ets:delete(?TABLE, Key),
|
||||||
|
miss
|
||||||
|
end;
|
||||||
|
[] ->
|
||||||
|
miss
|
||||||
|
end.
|
||||||
|
|
||||||
|
insert(#dns_query{name = Qname, type = QType, class = QClass},
|
||||||
|
#dns_message{answers = Answers, authority = Authority, additional = Additional, rc = RCode, aa = AA}) ->
|
||||||
|
TTLs = lists:foldl(fun(Term, Acc) ->
|
||||||
|
case Term of
|
||||||
|
#dns_rr{ttl = TTL} ->
|
||||||
|
[TTL|Acc];
|
||||||
|
_ ->
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end, [], Answers ++ Authority ++ Additional),
|
||||||
|
case length(TTLs) > 0 of
|
||||||
|
true ->
|
||||||
|
TTL = lists:min(TTLs),
|
||||||
|
ExpireAt = os:system_time(second) + TTL,
|
||||||
|
lager:debug("min ttl is: ~p, expire_at: ~p", [TTL, ExpireAt]),
|
||||||
|
Key = {Qname, QType, QClass},
|
||||||
|
Cache = #dns_cache{
|
||||||
|
key = Key,
|
||||||
|
answers = Answers,
|
||||||
|
authority = Authority,
|
||||||
|
additional = Additional,
|
||||||
|
rc = RCode,
|
||||||
|
flags = #{aa => AA},
|
||||||
|
% unix time
|
||||||
|
expire_at = ExpireAt
|
||||||
|
},
|
||||||
|
true = ets:insert(?TABLE, Cache);
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
240
apps/sdlan/src/dns_proxy/dns_handler.erl
Normal file
240
apps/sdlan/src/dns_proxy/dns_handler.erl
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 03. 12月 2025 23:00
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(dns_handler).
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include_lib("dns_erlang/include/dns.hrl").
|
||||||
|
-include_lib("pkt/include/pkt.hrl").
|
||||||
|
-include("dns_proxy.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
-export([handle_ip_packet/5]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
-define(RESOLVER_POOL, dns_resolver_pool).
|
||||||
|
|
||||||
|
%% 协议部分
|
||||||
|
-define(TCP_PROTOCOL, 6).
|
||||||
|
-define(UDP_PROTOCOL, 17).
|
||||||
|
|
||||||
|
-record(state, {}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link(?MODULE, [], []).
|
||||||
|
|
||||||
|
handle_ip_packet(Pid, Sock, SrcIp, SrcPort, Packet) when is_pid(Pid) ->
|
||||||
|
gen_server:cast(Pid, {handle_ip_packet, Sock, SrcIp, SrcPort, Packet}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% 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([]) ->
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
%% @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{}}).
|
||||||
|
handle_cast({handle_ip_packet, Sock, SrcIp, SrcPort, IpPacket}, State) ->
|
||||||
|
{#ipv4{saddr = ReqSAddr, daddr = ReqDAddr, p = Protocol}, ReqIpPayload} = pkt:ipv4(IpPacket),
|
||||||
|
case Protocol =:= ?UDP_PROTOCOL of
|
||||||
|
true ->
|
||||||
|
{#udp{sport = ReqSPort, dport = ReqDPort}, UdpPayload} = pkt:udp(ReqIpPayload),
|
||||||
|
case resolver(UdpPayload) of
|
||||||
|
{ok, DnsResp} ->
|
||||||
|
RespIpPacket = build_ip_packet(ReqDAddr, ReqSAddr, ReqDPort, ReqSPort, DnsResp),
|
||||||
|
lager:debug("[dns_handler] ip packet: ~p", [RespIpPacket]),
|
||||||
|
gen_udp:send(Sock, SrcIp, SrcPort, RespIpPacket);
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[dns_handler] resolver get error: ~p", [Reason])
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
lager:debug("[dns_handler] resolver invalid protocol: ~p", [Protocol])
|
||||||
|
end,
|
||||||
|
{stop, normal, 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(_Info, State) ->
|
||||||
|
{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 resolver(Packet :: binary()) -> {ok, Resp :: binary()} | {error, Reason :: any()}.
|
||||||
|
resolver(Packet) when is_binary(Packet) ->
|
||||||
|
resolver0(Packet, dns:decode_message(Packet)).
|
||||||
|
resolver0(Packet, QueryMsg = #dns_message{qc = 1, questions = [Question = #dns_query{name = QName, type = QType, class = QClass}|_]}) ->
|
||||||
|
%% 查找是否是内置的域名
|
||||||
|
case sdlan_dns_resolver:resolve(QName) of
|
||||||
|
{ok, Ip} ->
|
||||||
|
Answer = #dns_rr {
|
||||||
|
name = QName,
|
||||||
|
type = QType,
|
||||||
|
class = QClass,
|
||||||
|
ttl = 300,
|
||||||
|
data = #dns_rrdata_a {
|
||||||
|
ip = Ip
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RespMsg = QueryMsg#dns_message{
|
||||||
|
qr = true,
|
||||||
|
ra = true,
|
||||||
|
anc = 1,
|
||||||
|
auc = 0,
|
||||||
|
adc = 0,
|
||||||
|
answers = [Answer],
|
||||||
|
authority = [],
|
||||||
|
additional = []
|
||||||
|
},
|
||||||
|
lager:debug("[dns_handler] inbuilt qnanme: ~p, ip: ~p", [QName, Ip]),
|
||||||
|
{ok, dns:encode_message(RespMsg)};
|
||||||
|
error ->
|
||||||
|
case dns_cache:lookup(Question) of
|
||||||
|
{hit, Cache} ->
|
||||||
|
lager:debug("[dns_handler] question: ~p, hit cache answers: ~p", [Question, Cache#dns_cache.answers]),
|
||||||
|
RespMsg = build_response(QueryMsg, Cache),
|
||||||
|
{ok, dns:encode_message(RespMsg)};
|
||||||
|
miss ->
|
||||||
|
lager:debug("[dns_handler] cache is miss"),
|
||||||
|
Ref = make_ref(),
|
||||||
|
forward_to_upstream(Ref, Packet, QueryMsg),
|
||||||
|
receive
|
||||||
|
{dns_resolver_reply, Ref, Resp} ->
|
||||||
|
case dns:decode_message(Resp) of
|
||||||
|
RespMsg = #dns_message{answers = Answers} ->
|
||||||
|
lager:debug("[dns_handler] get a response answers: ~p", [Answers]),
|
||||||
|
dns_cache:insert(Question, RespMsg),
|
||||||
|
{ok, Resp};
|
||||||
|
Error ->
|
||||||
|
lager:debug("[dns_handler] parse reply get error: ~p", [Error]),
|
||||||
|
{error, Error}
|
||||||
|
end
|
||||||
|
after 5000 ->
|
||||||
|
{error, timeout}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
resolver0(_, Error) ->
|
||||||
|
lager:warning("[dns_handler] decode dns query get error: ~p", [Error]),
|
||||||
|
{error, Error}.
|
||||||
|
|
||||||
|
-spec forward_to_upstream(Ref :: reference(), Request :: binary(), QueryMsg :: #dns_message{}) -> no_return().
|
||||||
|
forward_to_upstream(Ref, Request, QueryMsg) ->
|
||||||
|
ReceiverPid = self(),
|
||||||
|
poolboy:transaction(?RESOLVER_POOL, fun(Pid) -> dns_resolver:forward(Pid, ReceiverPid, Ref, Request, QueryMsg) end).
|
||||||
|
|
||||||
|
-spec build_response(QueryMsg :: #dns_message{}, Dns_cache :: #dns_cache{}) -> RespMsg :: #dns_message{}.
|
||||||
|
build_response(QueryMsg, #dns_cache{expire_at = ExpireAt, answers = Answers, authority = Authority, additional = Additional, rc = RCode, flags = #{aa := AA}}) ->
|
||||||
|
Now = os:system_time(second),
|
||||||
|
RemainingTTL = ExpireAt - Now,
|
||||||
|
|
||||||
|
Answers2 = [adjust_ttl(RR, RemainingTTL) || RR <- Answers],
|
||||||
|
Authority2 = [adjust_ttl(RR, RemainingTTL) || RR <- Authority],
|
||||||
|
Additional2 = [adjust_ttl(RR, RemainingTTL) || RR <- Additional],
|
||||||
|
|
||||||
|
QueryMsg#dns_message{
|
||||||
|
qr = true,
|
||||||
|
ra = true,
|
||||||
|
aa = AA,
|
||||||
|
rc = RCode,
|
||||||
|
anc = length(Answers2),
|
||||||
|
auc = length(Authority2),
|
||||||
|
adc = length(Additional2),
|
||||||
|
answers = Answers2,
|
||||||
|
authority = Authority2,
|
||||||
|
additional = Additional2
|
||||||
|
}.
|
||||||
|
|
||||||
|
-spec adjust_ttl(RR :: any(), RemainingTTL :: integer()) -> any().
|
||||||
|
adjust_ttl(RR = #dns_rr{}, RemainingTTL) ->
|
||||||
|
RR#dns_rr{ttl = max(0, RemainingTTL)};
|
||||||
|
adjust_ttl(RR, _RemainingTTL) ->
|
||||||
|
RR.
|
||||||
|
|
||||||
|
-spec build_ip_packet(SAddr :: inet:ip4_address(), DAddr :: inet:ip4_address(), SPort :: integer(), DPort :: integer(), Payload :: binary()) -> IpPacket :: binary().
|
||||||
|
build_ip_packet(SAddr, DAddr, SPort, DPort, UdpPayload) when is_integer(SPort), is_integer(DPort), is_binary(UdpPayload) ->
|
||||||
|
ULen = 8 + byte_size(UdpPayload),
|
||||||
|
RespUdpHeader = pkt:udp(#udp{
|
||||||
|
sport = SPort,
|
||||||
|
dport = DPort,
|
||||||
|
ulen = ULen,
|
||||||
|
sum = dns_utils:udp_checksum(SAddr, DAddr, SPort, DPort, UdpPayload)
|
||||||
|
}),
|
||||||
|
IpPayload = <<RespUdpHeader/binary, UdpPayload/binary>>,
|
||||||
|
|
||||||
|
IpPacket0 = #ipv4{
|
||||||
|
len = 20 + ULen,
|
||||||
|
ttl = 64,
|
||||||
|
off = 0,
|
||||||
|
mf = 0,
|
||||||
|
sum = 0,
|
||||||
|
p = ?UDP_PROTOCOL,
|
||||||
|
saddr = SAddr,
|
||||||
|
daddr = DAddr,
|
||||||
|
opt = <<>>
|
||||||
|
},
|
||||||
|
IpCheckSum = dns_utils:ip_checksum(IpPacket0),
|
||||||
|
IpHeader = pkt:ipv4(IpPacket0#ipv4{sum = IpCheckSum}),
|
||||||
|
|
||||||
|
<<IpHeader/binary, IpPayload/binary>>.
|
||||||
66
apps/sdlan/src/dns_proxy/dns_handler_sup.erl
Normal file
66
apps/sdlan/src/dns_proxy/dns_handler_sup.erl
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 03. 12月 2025 17:29
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(dns_handler_sup).
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% Supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
-export([start_handler/0]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc Starts the supervisor
|
||||||
|
-spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Supervisor callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3],
|
||||||
|
%% this function is called by the new process to find out about
|
||||||
|
%% restart strategy, maximum restart frequency and child
|
||||||
|
%% specifications.
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{strategy => simple_one_for_one, intensity => 0, period => 1},
|
||||||
|
Spec = #{
|
||||||
|
id => dns_handler,
|
||||||
|
start => {'dns_handler', start_link, []},
|
||||||
|
restart => temporary,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['dns_handler']
|
||||||
|
},
|
||||||
|
|
||||||
|
{ok, {SupFlags, [Spec]}}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
start_handler() ->
|
||||||
|
case supervisor:start_child(?MODULE, []) of
|
||||||
|
{ok, Pid} ->
|
||||||
|
{ok, Pid};
|
||||||
|
{error, {already_started, Pid}} ->
|
||||||
|
{ok, Pid};
|
||||||
|
StartError ->
|
||||||
|
StartError
|
||||||
|
end.
|
||||||
50
apps/sdlan/src/dns_proxy/dns_proxy_sup.erl
Normal file
50
apps/sdlan/src/dns_proxy/dns_proxy_sup.erl
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%% @doc dns_proxy top level supervisor.
|
||||||
|
%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(dns_proxy_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
|
%% sup_flags() = #{strategy => strategy(), % optional
|
||||||
|
%% intensity => non_neg_integer(), % optional
|
||||||
|
%% period => pos_integer()} % optional
|
||||||
|
%% child_spec() = #{id => child_id(), % mandatory
|
||||||
|
%% start => mfargs(), % mandatory
|
||||||
|
%% restart => restart(), % optional
|
||||||
|
%% shutdown => shutdown(), % optional
|
||||||
|
%% type => worker(), % optional
|
||||||
|
%% modules => modules()} % optional
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||||
|
|
||||||
|
Port = 15353,
|
||||||
|
Specs = [
|
||||||
|
#{
|
||||||
|
id => dns_handler_sup,
|
||||||
|
start => {dns_handler_sup, start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => supervisor,
|
||||||
|
modules => ['dns_handler_sup']
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
id => dns_server,
|
||||||
|
start => {dns_server, start_link, [Port]},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['dns_server']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
{ok, {SupFlags, Specs}}.
|
||||||
150
apps/sdlan/src/dns_proxy/dns_resolver.erl
Normal file
150
apps/sdlan/src/dns_proxy/dns_resolver.erl
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 03. 12月 2025 18:26
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(dns_resolver).
|
||||||
|
-author("anlicheng").
|
||||||
|
-include_lib("dns_erlang/include/dns.hrl").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/1]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-export([forward/5]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
-define(REQUEST_TTL, 5000).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
socket,
|
||||||
|
tid,
|
||||||
|
dns_servers = []
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
forward(Pid, ReceiverPid, Ref, Request, QueryMsg) ->
|
||||||
|
gen_server:cast(Pid, {forward, ReceiverPid, Ref, Request, QueryMsg}).
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link(Args :: list()) ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link(Args) when is_list(Args) ->
|
||||||
|
gen_server:start_link(?MODULE, [], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% 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([]) ->
|
||||||
|
{ok, DnsServers} = application:get_env(dns_proxy, public_dns_servers),
|
||||||
|
|
||||||
|
{ok, Sock} = gen_udp:open(0, [binary, {active, true}]),
|
||||||
|
%% 通过ets来保存映射关系
|
||||||
|
Tid = ets:new(random_table(), [set, {read_concurrency, true}, {write_concurrency, true}, private]),
|
||||||
|
|
||||||
|
{ok, #state{socket = Sock, tid = Tid, dns_servers = DnsServers}}.
|
||||||
|
|
||||||
|
%% @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{}}).
|
||||||
|
handle_cast({forward, ReceiverPid, Ref, Request, #dns_message{id = TxId, questions = [#dns_query{name = QName, type = QType, class = QClass}|_]}}, State = #state{socket = Socket, tid = Tid, dns_servers = DnsServers}) ->
|
||||||
|
Keys = lists:foldl(fun({DnsIp, DnsPort}, Acc) ->
|
||||||
|
ok = gen_udp:send(Socket, DnsIp, DnsPort, Request),
|
||||||
|
Key = {TxId, DnsIp, DnsPort, QName, QType, QClass},
|
||||||
|
lager:debug("[dns_resolver] key: ~p, send to: ~p, packet: ~p", [Key, {DnsIp, DnsPort}, Request]),
|
||||||
|
true = ets:insert(Tid, {Key, Ref, ReceiverPid}),
|
||||||
|
[Key|Acc]
|
||||||
|
end, [], DnsServers),
|
||||||
|
erlang:start_timer(?REQUEST_TTL, self(), {clean_ticker, Keys}),
|
||||||
|
|
||||||
|
{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, Socket, TargetIp, TargetPort, Resp}, State = #state{tid = Tid, socket = Socket}) ->
|
||||||
|
case dns:decode_message(Resp) of
|
||||||
|
#dns_message{id = TxId, questions = [#dns_query{name = QName, type = QType, class = QClass}|_]} ->
|
||||||
|
Key = {TxId, TargetIp, TargetPort, QName, QType, QClass},
|
||||||
|
Records = ets:take(Tid, Key),
|
||||||
|
resolver_reply(Records, Resp);
|
||||||
|
_ ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_info({timeout, _, {clean_ticker, Keys}}, State = #state{tid = Tid}) ->
|
||||||
|
lists:foreach(fun(Key) -> ets:delete(Tid, Key) end, Keys),
|
||||||
|
{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 random_table() -> atom().
|
||||||
|
random_table() ->
|
||||||
|
list_to_atom("udp_ets:" ++ integer_to_list(erlang:unique_integer([monotonic, positive]))).
|
||||||
|
|
||||||
|
-spec resolver_reply(list(), Resp :: binary()) -> no_return().
|
||||||
|
resolver_reply([{_, Ref, ReceiverPid}], Resp) when is_binary(Resp) ->
|
||||||
|
case is_process_alive(ReceiverPid) of
|
||||||
|
true ->
|
||||||
|
ReceiverPid ! {dns_resolver_reply, Ref, Resp};
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end;
|
||||||
|
resolver_reply(_, _) ->
|
||||||
|
ok.
|
||||||
24
apps/sdlan/src/dns_proxy/dns_server.erl
Normal file
24
apps/sdlan/src/dns_proxy/dns_server.erl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-module(dns_server).
|
||||||
|
-export([start_link/1, init/1]).
|
||||||
|
|
||||||
|
start_link(Port) when is_integer(Port) ->
|
||||||
|
{ok, spawn_link(?MODULE, init, [Port])}.
|
||||||
|
|
||||||
|
init(Port) ->
|
||||||
|
dns_cache:init(),
|
||||||
|
{ok, Sock} = gen_udp:open(Port, [binary, {active, true}]),
|
||||||
|
io:format("DNS Forwarder started on UDP port ~p~n", [Port]),
|
||||||
|
loop(Sock).
|
||||||
|
|
||||||
|
loop(Sock) ->
|
||||||
|
receive
|
||||||
|
{udp, Sock, Ip, Port, Packet} ->
|
||||||
|
lager:debug("[dns_server] ip: ~p, get a packet: ~p", [{Ip, Port}, Packet]),
|
||||||
|
case dns_handler_sup:start_handler() of
|
||||||
|
{ok, HandlerPid} ->
|
||||||
|
dns_handler:handle_ip_packet(HandlerPid, Sock, Ip, Port, Packet);
|
||||||
|
Error ->
|
||||||
|
lager:debug("[dns_server] start handler get error: ~p", [Error])
|
||||||
|
end,
|
||||||
|
loop(Sock)
|
||||||
|
end.
|
||||||
131
apps/sdlan/src/dns_proxy/dns_utils.erl
Normal file
131
apps/sdlan/src/dns_proxy/dns_utils.erl
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 05. 12月 2025 18:06
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(dns_utils).
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-include_lib("pkt/include/pkt.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([ends_with/2, parse_address/1, checksum/1, udp_checksum/5, ip_checksum/1]).
|
||||||
|
-export([test/0]).
|
||||||
|
|
||||||
|
-spec ends_with(Bin :: binary(), Suffix :: binary()) -> boolean().
|
||||||
|
ends_with(Bin, Suffix) when is_binary(Bin), is_binary(Suffix) ->
|
||||||
|
case binary:match(Bin, Suffix) of
|
||||||
|
{Pos, Len} ->
|
||||||
|
Pos + Len =:= byte_size(Bin);
|
||||||
|
nomatch ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec parse_address(Ip :: any()) -> {ok, IpAddress :: inet:ip4_address()} | {error, Reason :: any()}.
|
||||||
|
parse_address(Ip = {Ip0, Ip1, Ip2, Ip3}) when is_integer(Ip0), is_integer(Ip1), is_integer(Ip2), is_integer(Ip3) ->
|
||||||
|
{ok, Ip};
|
||||||
|
parse_address(Bin) when is_binary(Bin) ->
|
||||||
|
inet:parse_address(binary_to_list(Bin)).
|
||||||
|
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% @doc
|
||||||
|
%% Calculate 16-bit one's-complement checksum.
|
||||||
|
%%
|
||||||
|
%% Input:
|
||||||
|
%% Bin :: binary()
|
||||||
|
%%
|
||||||
|
%% Output:
|
||||||
|
%% Checksum :: 0..16#FFFF
|
||||||
|
%%
|
||||||
|
%% Usage:
|
||||||
|
%% Checksum = checksum(Bin).
|
||||||
|
%%
|
||||||
|
%% Notes:
|
||||||
|
%% - Bin is treated as big-endian 16-bit words
|
||||||
|
%% - If Bin length is odd, a zero byte is padded
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-spec checksum(binary()) -> non_neg_integer().
|
||||||
|
checksum(Bin) when is_binary(Bin) ->
|
||||||
|
Sum = checksum_sum(Bin, 0),
|
||||||
|
%% fold carry bits
|
||||||
|
Folded = fold16(Sum),
|
||||||
|
%% one's complement
|
||||||
|
(bnot Folded) band 16#FFFF.
|
||||||
|
checksum_sum(<<>>, Acc) ->
|
||||||
|
Acc;
|
||||||
|
checksum_sum(<<Word:16/big, Rest/binary>>, Acc) ->
|
||||||
|
checksum_sum(Rest, Acc + Word);
|
||||||
|
checksum_sum(<<Byte:8>>, Acc) ->
|
||||||
|
%% odd length: pad low byte with zero
|
||||||
|
checksum_sum(<<>>, Acc + (Byte bsl 8)).
|
||||||
|
|
||||||
|
fold16(S) when S > 16#FFFF ->
|
||||||
|
fold16((S band 16#FFFF) + (S bsr 16));
|
||||||
|
fold16(S) ->
|
||||||
|
S.
|
||||||
|
|
||||||
|
-spec udp_checksum(SAddr :: inet:ip4_address(), DAddr :: inet:ip4_address(), SPort :: integer(), DPort :: integer(), UDPPayload :: binary()) -> non_neg_integer().
|
||||||
|
udp_checksum({SA1, SA2, SA3, SA4}, {DA1, DA2, DA3, DA4}, SPort, DPort, UDPPayload) when is_integer(SPort), is_integer(DPort), is_binary(UDPPayload) ->
|
||||||
|
ULen = 8 + byte_size(UDPPayload),
|
||||||
|
PseudoHeader = <<SA1, SA2, SA3, SA4,
|
||||||
|
DA1, DA2, DA3, DA4,
|
||||||
|
0:8, 17:8,
|
||||||
|
ULen:16>>,
|
||||||
|
UDPHeader = <<SPort:16, DPort:16, ULen:16, 0:16>>,
|
||||||
|
CheckSum = checksum(<<PseudoHeader/binary, UDPHeader/binary, UDPPayload/binary>>),
|
||||||
|
case CheckSum of
|
||||||
|
0 ->
|
||||||
|
16#FFFF;
|
||||||
|
_ ->
|
||||||
|
CheckSum
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec ip_checksum(Ipv4 :: #ipv4{}) -> non_neg_integer().
|
||||||
|
ip_checksum(#ipv4{hl = HL, tos = ToS, len = Len,
|
||||||
|
id = Id, df = DF, mf = MF,
|
||||||
|
off = Off, ttl = TTL, p = P,
|
||||||
|
saddr = {SA1, SA2, SA3, SA4},
|
||||||
|
daddr = {DA1, DA2, DA3, DA4},
|
||||||
|
opt = Opt}) ->
|
||||||
|
|
||||||
|
IPBinForChecksum =
|
||||||
|
<<4:4, HL:4, %% Version=4 + IHL
|
||||||
|
ToS:8, %% Type of Service
|
||||||
|
Len:16/big, %% Total Length
|
||||||
|
Id:16/big, %% Identification
|
||||||
|
DF:1, MF:1, Off:14, %% Flags + Fragment offset
|
||||||
|
TTL:8, %% TTL
|
||||||
|
P:8, %% Protocol
|
||||||
|
0:16, %% checksum field set to 0 for calculation
|
||||||
|
SA1:8, SA2:8, SA3:8, SA4:8, %% Source IP
|
||||||
|
DA1:8, DA2:8, DA3:8, DA4:8, %% Dest IP
|
||||||
|
Opt/binary>>, %% Options (可选)
|
||||||
|
CheckSum = checksum(IPBinForChecksum),
|
||||||
|
case CheckSum of
|
||||||
|
0 ->
|
||||||
|
16#FFFF;
|
||||||
|
_ ->
|
||||||
|
CheckSum
|
||||||
|
end.
|
||||||
|
|
||||||
|
test() ->
|
||||||
|
%Bin = <<69,0,0,77,48,179,0,0,64,17,28,168,100,123,0,2,100,100,100,100,252,230,0,53,0,57,6,92,152,24,1,0,0,1,0,0,0,0,0,0,2,100,98,7,95,100,110,115,45,115,100,4,95,117,100,112,8,112,117,110,99,104,110,101,116,2,116,115,3,110,101,116,0,0,12,0,1>>,
|
||||||
|
Bin = <<69,0,0,93,0,0,0,0,64,6,77,86,100,100,100,100,100,123,0,2,0,53,196,102,0,73,39,7,215,192,129,128,0,1,0,1,0,0,0,0,2,108,98,7,95,100,110,115,45,115,100,4,95,117,100,112,8,112,117,110,99,104,110,101,116,2,116,115,3,110,101,116,0,0,12,0,1,192,12,0,12,0,1,0,0,1,44,0,4,192,168,1,101>>,
|
||||||
|
|
||||||
|
{IPPacket = #ipv4{
|
||||||
|
saddr = SAddr,
|
||||||
|
daddr = DAddr,
|
||||||
|
sum = IpSum
|
||||||
|
}, UdpPacket} = pkt:ipv4(Bin),
|
||||||
|
|
||||||
|
{UDP = #udp{sport = SPort, dport = DPort, sum = CheckSum}, UDPPayload} = pkt:udp(UdpPacket),
|
||||||
|
|
||||||
|
X = udp_checksum(SAddr, DAddr, SPort, DPort, UDPPayload),
|
||||||
|
|
||||||
|
lager:debug("ip_sum: ~p, =: ~p, udp: ~p, checkSum: ~p, =: ~p", [IpSum, ip_checksum(IPPacket), UDP, CheckSum, X]),
|
||||||
|
|
||||||
|
dns:decode_message(UDPPayload).
|
||||||
@ -14,8 +14,9 @@
|
|||||||
jiffy,
|
jiffy,
|
||||||
hackney,
|
hackney,
|
||||||
gpb,
|
gpb,
|
||||||
dns_proxy,
|
|
||||||
throttle,
|
throttle,
|
||||||
|
dns_erlang,
|
||||||
|
pkt,
|
||||||
parse_trans,
|
parse_trans,
|
||||||
mnesia,
|
mnesia,
|
||||||
erts,
|
erts,
|
||||||
|
|||||||
@ -16,7 +16,6 @@ start(_StartType, _StartArgs) ->
|
|||||||
%% 加速内存的回收
|
%% 加速内存的回收
|
||||||
erlang:system_flag(fullsweep_after, 16),
|
erlang:system_flag(fullsweep_after, 16),
|
||||||
|
|
||||||
start_dns_resolver(),
|
|
||||||
start_http_server(),
|
start_http_server(),
|
||||||
start_tcp_server(),
|
start_tcp_server(),
|
||||||
sdlan_sup:start_link().
|
sdlan_sup:start_link().
|
||||||
@ -74,8 +73,4 @@ start_tcp_server() ->
|
|||||||
},
|
},
|
||||||
{ok, _} = ranch:start_listener('sdlan/tcp_server', ranch_tcp, TransOpts, sdlan_channel, []),
|
{ok, _} = ranch:start_listener('sdlan/tcp_server', ranch_tcp, TransOpts, sdlan_channel, []),
|
||||||
|
|
||||||
lager:debug("[sdlan_app] the tcp server start at: ~p", [Port]).
|
lager:debug("[sdlan_app] the tcp server start at: ~p", [Port]).
|
||||||
|
|
||||||
%% 启动dns的解析服务
|
|
||||||
start_dns_resolver() ->
|
|
||||||
ok = dns_proxy:start_proxy(15353, {sdlan_dns_resolver, resolve, []}).
|
|
||||||
@ -23,6 +23,12 @@
|
|||||||
|
|
||||||
% {stun_servers, [{'sdlan_stun:2:1', 1265}, {'sdlan_stun:2:2', 1266}]},
|
% {stun_servers, [{'sdlan_stun:2:1', 1265}, {'sdlan_stun:2:2', 1266}]},
|
||||||
|
|
||||||
|
%% 公共的dns域名解析服务
|
||||||
|
{public_dns_servers, [
|
||||||
|
{{114, 114, 114, 114}, 53},
|
||||||
|
{{8,8,8,8}, 53}
|
||||||
|
]},
|
||||||
|
|
||||||
{pools, [
|
{pools, [
|
||||||
%% mysql连接池配置
|
%% mysql连接池配置
|
||||||
{mysql_sdlan,
|
{mysql_sdlan,
|
||||||
@ -37,6 +43,11 @@
|
|||||||
{database, "sdlan"},
|
{database, "sdlan"},
|
||||||
{queries, [<<"set names utf8">>]}
|
{queries, [<<"set names utf8">>]}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{dns_resolver_pool,
|
||||||
|
[{size, 20}, {max_overflow, 100}, {worker_module, dns_resolver}],
|
||||||
|
[]
|
||||||
}
|
}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
@ -49,22 +60,6 @@
|
|||||||
{access_context, sync_transaction}
|
{access_context, sync_transaction}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
{dns_proxy, [
|
|
||||||
|
|
||||||
%% 公共的dns域名解析服务
|
|
||||||
{public_dns_servers, [
|
|
||||||
{{114, 114, 114, 114}, 53},
|
|
||||||
{{8,8,8,8}, 53}
|
|
||||||
]},
|
|
||||||
|
|
||||||
{dns_resolver_pool, [
|
|
||||||
{size, 20},
|
|
||||||
{max_overflow, 100},
|
|
||||||
{worker_module, dns_resolver}
|
|
||||||
]}
|
|
||||||
|
|
||||||
]},
|
|
||||||
|
|
||||||
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||||
{lager, [
|
{lager, [
|
||||||
{colored, true},
|
{colored, true},
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
{deps, [
|
{deps, [
|
||||||
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
|
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
|
||||||
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
|
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
|
||||||
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
|
||||||
{jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}},
|
{jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}},
|
||||||
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.12.0"}}},
|
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.12.0"}}},
|
||||||
{mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}},
|
{mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}},
|
||||||
{gpb, ".*", {git, "https://github.com/tomas-abrahamsson/gpb.git", {tag, "4.21.1"}}},
|
{gpb, ".*", {git, "https://github.com/tomas-abrahamsson/gpb.git", {tag, "4.21.1"}}},
|
||||||
{dns_proxy, ".*", {git, "https://gitea.s5s8.com/anlicheng/dns_proxy.git", {branch, "main"}}},
|
|
||||||
{throttle, ".*", {git, "https://github.com/lambdaclass/throttle.git", {tag, "0.3.0"}}},
|
{throttle, ".*", {git, "https://github.com/lambdaclass/throttle.git", {tag, "0.3.0"}}},
|
||||||
|
{dns_erlang, ".*", {git, "https://github.com/dnsimple/dns_erlang.git", {tag, "v4.4.0"}}},
|
||||||
|
{pkt, ".*", {git, "https://github.com/msantos/pkt.git", {tag, "0.6.0"}}},
|
||||||
|
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
||||||
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},
|
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},
|
||||||
{lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}
|
{lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user