diff --git a/apps/dns_proxy/src/dns_cache.erl b/apps/dns_proxy/src/dns_cache.erl new file mode 100644 index 0000000..23627cb --- /dev/null +++ b/apps/dns_proxy/src/dns_cache.erl @@ -0,0 +1,17 @@ +-module(dns_cache). +-export([init/0, lookup/1, insert/2]). + +-define(TABLE, dns_cache). + +init() -> + ets:new(?TABLE, [named_table, set, public, {read_concurrency, true}]). + +lookup(Key) -> + case ets:lookup(?TABLE, Key) of + [{_Key, Value}] -> {hit, Value}; + [] -> miss + end. + +insert(Key, DNSMsg) -> + ets:insert(?TABLE, {Key, DNSMsg}), + ok. diff --git a/apps/dns_proxy/src/dns_handler.erl b/apps/dns_proxy/src/dns_handler.erl new file mode 100644 index 0000000..51e12a5 --- /dev/null +++ b/apps/dns_proxy/src/dns_handler.erl @@ -0,0 +1,48 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @doc +%%% +%%% @end +%%% Created : 03. 12月 2025 17:27 +%%%------------------------------------------------------------------- +-module(dns_handler). +-author("anlicheng"). + +-include_lib("dns_erlang/include/dns.hrl"). +-include_lib("dns_erlang/include/dns_records.hrl"). +-include_lib("dns_erlang/include/dns_terms.hrl"). + +-export([handle_request/4]). + +handle_request(Sock, Ip, Port, Packet) -> + case dns:decode_message(Packet) of + {ok, Msg} -> + %Qname = (Msg#dns_message.questions)#dns_question.qname, + Qname = <<"www.baidu.com">>, + case dns_cache:lookup(Qname) of + {hit, R} -> + Resp = build_response(Msg, R), + gen_udp:send(Sock, Ip, Port, dns:encode_message(Resp)); + miss -> + forward_to_upstream(Sock, Ip, Port, Packet) + end; + _ -> + ok + end. + +forward_to_upstream(Sock, Ip, Port, Packet) -> + {SendSock, {UpIP, UpPort}} = dns_socket_pool:get_socket(), + ok = gen_udp:send(SendSock, UpIP, UpPort, Packet), + + inet:setopts(SendSock, [{active, once}]), + receive + {udp, SendSock, _UIp, _UPort, Resp} -> + gen_udp:send(Sock, Ip, Port, Resp) + after 2000 -> + ok + end. + +build_response(Req, RR) -> + Msg = Req, + Msg#dns_message{answers=[RR], qr=true, aa=true}. \ No newline at end of file diff --git a/apps/dns_proxy/src/dns_handler_sup.erl b/apps/dns_proxy/src/dns_handler_sup.erl new file mode 100644 index 0000000..cead1e8 --- /dev/null +++ b/apps/dns_proxy/src/dns_handler_sup.erl @@ -0,0 +1,71 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @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/4]). + +-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. +-spec(init(Args :: term()) -> + {ok, {SupFlags :: {RestartStrategy :: supervisor:strategy(), + MaxR :: non_neg_integer(), MaxT :: non_neg_integer()}, + [ChildSpec :: supervisor:child_spec()]}} + | ignore | {error, Reason :: term()}). +init([]) -> + SupFlags = #{strategy => simple_one_for_one, intensity => 0, period => 1}, + Spec = #{ + id => dns_handler, + start => {'dns_handler', dns_handler, []}, + restart => temporary, + shutdown => 2000, + type => worker, + modules => ['dns_handler'] + }, + + {ok, {SupFlags, [Spec]}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +start_handler(Sock, Ip, Port, Packet) -> + case supervisor:start_child(?MODULE, [Sock, Ip, Port, Packet]) of + {ok, Pid} -> + {ok, Pid}; + {error, {already_started, Pid}} -> + {ok, Pid}; + StartError -> + StartError + end. \ No newline at end of file diff --git a/apps/dns_proxy/src/dns_proxy.app.src b/apps/dns_proxy/src/dns_proxy.app.src index a81e9ec..9b811bd 100644 --- a/apps/dns_proxy/src/dns_proxy.app.src +++ b/apps/dns_proxy/src/dns_proxy.app.src @@ -4,7 +4,11 @@ {registered, []}, {mod, {dns_proxy_app, []}}, {applications, - [kernel, + [ + sync, + lager, + dns_erlang, + kernel, stdlib ]}, {env,[]}, diff --git a/apps/dns_proxy/src/dns_proxy_sup.erl b/apps/dns_proxy/src/dns_proxy_sup.erl index 3efdcf2..c29bb0a 100644 --- a/apps/dns_proxy/src/dns_proxy_sup.erl +++ b/apps/dns_proxy/src/dns_proxy_sup.erl @@ -26,10 +26,17 @@ start_link() -> %% type => worker(), % optional %% modules => modules()} % optional init([]) -> - SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], + SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600}, + ChildSpecs = [ + #{ + id => dns_handler_sup, + start => {dns_handler_sup, start_link, []}, + restart => permanent, + shutdown => 2000, + type => supervisor, + modules => ['dns_handler_sup'] + } + ], {ok, {SupFlags, ChildSpecs}}. %% internal functions diff --git a/apps/dns_proxy/src/dns_server.erl b/apps/dns_proxy/src/dns_server.erl new file mode 100644 index 0000000..c20a310 --- /dev/null +++ b/apps/dns_proxy/src/dns_server.erl @@ -0,0 +1,30 @@ +-module(dns_server). +-export([start/0, stop/0]). + +-define(LISTEN_PORT, 53). + +start() -> + dns_cache:init(), + dns_zone_loader:load("priv/local.zone"), + + %% 配置上游 DNS + Upstreams = [ + {{8,8,8,8}, 53}, + {{1,1,1,1}, 53} + ], + dns_socket_pool:start_link(Upstreams), + + {ok, Sock} = gen_udp:open(?LISTEN_PORT, [binary, {active, true}]), + io:format("DNS Forwarder started on UDP port ~p~n", [?LISTEN_PORT]), + loop(Sock). + +stop() -> + gen_udp:close(?LISTEN_PORT), + ok. + +loop(Sock) -> + receive + {udp, Sock, Ip, Port, Packet} -> + dns_handler_sup:start_handler(Sock, Ip, Port, Packet), + loop(Sock) + end. \ No newline at end of file diff --git a/apps/dns_proxy/src/dns_socket_pool.erl b/apps/dns_proxy/src/dns_socket_pool.erl new file mode 100644 index 0000000..47e23bc --- /dev/null +++ b/apps/dns_proxy/src/dns_socket_pool.erl @@ -0,0 +1,26 @@ +-module(dns_socket_pool). +-behaviour(gen_server). + +-export([start_link/1, get_socket/0]). +-export([init/1, handle_call/3]). + +-record(state, {sockets=[]}). + +start_link(Ups) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, Ups, []). + +get_socket() -> + gen_server:call(?MODULE, get). + +init(Upstreams) -> + %% 为多个上游 DNS 建 socket + Sockets = lists:map(fun({IP, Port}) -> + {ok, Sock} = gen_udp:open(0, [binary, {active, false}]), + {Sock, {IP, Port}} + end, Upstreams), + {ok, #state{sockets=Sockets}}. + +handle_call(get, _From, State=#state{sockets=Socks}) -> + %% 简单 round-robin + [H|T] = Socks, + {reply, H, State#state{sockets=T ++ [H]}}. diff --git a/apps/dns_proxy/src/dns_zone_loader.erl b/apps/dns_proxy/src/dns_zone_loader.erl new file mode 100644 index 0000000..64c6247 --- /dev/null +++ b/apps/dns_proxy/src/dns_zone_loader.erl @@ -0,0 +1,13 @@ +-module(dns_zone_loader). +-export([load/1]). + +-include_lib("dns_erlang/include/dns.hrl"). + +load(Path) -> + {ok, Bin} = file:read_file(Path), + {ok, Records} = dns_zone:decode(Bin), % dns_erlang 提供 + lists:foreach(fun(R) -> + Name = R#dns_rr.name, + dns_cache:insert(Name, R) + end, Records), + ok. diff --git a/config/sys.config b/config/sys.config index 36a3d4f..f8b9adf 100644 --- a/config/sys.config +++ b/config/sys.config @@ -1,3 +1,46 @@ [ - {dns_proxy, []} + {dns_proxy, [ + + %% 公共的dns域名解析服务 + {public_dns_servers, [ + {{8,8,8,8}, 53}, + {{1,1,1,1}, 53} + ]} + + ]}, + + %% 系统日志配置,系统日志为lager, 支持日志按日期自动分割 + {lager, [ + {colored, true}, + %% Whether to write a crash log, and where. Undefined means no crash logger. + {crash_log, "trade_hub.crash.log"}, + %% Maximum size in bytes of events in the crash log - defaults to 65536 + {crash_log_msg_size, 65536}, + %% Maximum size of the crash log in bytes, before its rotated, set + %% to 0 to disable rotation - default is 0 + {crash_log_size, 10485760}, + %% What time to rotate the crash log - default is no time + %% rotation. See the README for a description of this format. + {crash_log_date, "$D0"}, + %% Number of rotated crash logs to keep, 0 means keep only the + %% current one - default is 0 + {crash_log_count, 5}, + %% Whether to redirect error_logger messages into lager - defaults to true + {error_logger_redirect, true}, + + %% How big the gen_event mailbox can get before it is switched into sync mode + {async_threshold, 20}, + %% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold' + %% to async_threshold - async_threshold_window + {async_threshold_window, 5}, + + {handlers, [ + %% debug | info | warning | error, 日志级别 + {lager_console_backend, debug}, + {lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]}, + {lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]}, + {lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]}, + {lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]} + ]} + ]} ]. diff --git a/rebar.config b/rebar.config index 9049797..6074325 100644 --- a/rebar.config +++ b/rebar.config @@ -1,5 +1,10 @@ {erl_opts, [debug_info]}. -{deps, []}. +{deps, [ + {dns_erlang, ".*", {git, "https://github.com/dnsimple/dns_erlang.git", {tag, "v4.4.0"}}}, + {sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}}, + {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"}}} +]}. {relx, [{release, {dns_proxy, "0.1.0"}, [dns_proxy, @@ -30,3 +35,6 @@ %% {mode, minimal} ] }]}]}. + +{erl_opts, [{parse_transform,lager_transform}]}. +{rebar_packages_cdn, "https://hexpm.upyun.com"}. \ No newline at end of file