From 0dea502b663aaea839f39f4316a070c40d9cb5a4 Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Mon, 16 Nov 2020 12:55:31 +0100 Subject: [PATCH 1/8] pass the requested IP protocol options into IP assigment As preparation to support other IP assigment methodes (e.g. DHCP), normalize and pass the request protocol configuration options into the assigment logic. Also, unify PCO handling in the library code. --- src/ergw_gsn_lib.erl | 22 ++++---- src/ergw_gtp_gsn_lib.erl | 106 +++++++++++++++++++++++++++++++++++++-- src/ggsn_gn.erl | 59 +++++----------------- src/pgw_s5s8.erl | 59 +++++----------------- src/saegw_s11.erl | 59 +++++----------------- 5 files changed, 152 insertions(+), 153 deletions(-) diff --git a/src/ergw_gsn_lib.erl b/src/ergw_gsn_lib.erl index 541f3022..833b610c 100644 --- a/src/ergw_gsn_lib.erl +++ b/src/ergw_gsn_lib.erl @@ -33,7 +33,7 @@ pcc_events_to_charging_rule_report/1, make_gy_credit_request/3]). -export([apn/1, apn/2, select_vrf/2, - allocate_ips/7, release_context_ips/1]). + allocate_ips/8, release_context_ips/1]). -export([init_tunnel/4, assign_tunnel_teid/3, reassign_tunnel_teid/1, @@ -643,8 +643,8 @@ request_alloc({ReqIPv6, PrefixLen}, #{'Framed-Pool' := Pool} = Opts) request_alloc(_ReqIP, _Opts) -> skip. -request_ip_alloc(ReqIPs, Opts,# tunnel{local = #fq_teid{teid = TEI}}) -> - Req = [request_alloc(IP, Opts) || IP <- ReqIPs], +request_ip_alloc(ReqIPs, PCO, #tunnel{local = #fq_teid{teid = TEI}}) -> + Req = [request_alloc(IP, PCO) || IP <- ReqIPs], ergw_ip_pool:send_request(TEI, Req). ip_alloc_result(skip, Acc) -> @@ -731,10 +731,12 @@ session_ip_alloc(_, _, SessionOpts, _, {PDNType, ReqMSv4, ReqMSv6}) -> MSv6 = session_ipv6_alloc(SessionOpts, ReqMSv6), {PDNType, MSv4, MSv6}. -%% allocate_ips/3 -allocate_ips(ReqIPs, SOpts0, Tunnel) -> - ReqIds = request_ip_alloc(ReqIPs, SOpts0, Tunnel), - wait_ip_alloc_results(ReqIds, SOpts0). +%% allocate_ips/4 +allocate_ips(ReqIPs, PCO0, SOpts, Tunnel) -> + RequestSOpts = ['Framed-Pool', 'Framed-Interface-Id'], + PCO = maps:merge(maps:with(RequestSOpts, SOpts), PCO0), + ReqIds = request_ip_alloc(ReqIPs, PCO, Tunnel), + wait_ip_alloc_results(ReqIds, SOpts). allocate_ips_result(ReqPDNType, BearerType, #ue_ip{v4 = MSv4, v6 = MSv6}) -> allocate_ips_result(ReqPDNType, BearerType, ergw_ip_pool:ip(MSv4), @@ -760,10 +762,10 @@ allocate_ips_result('IPv4v6', _, undefined, IPv6) when IPv6 /= undefined -> allocate_ips_result(_, _, _, _) -> {error, ?CTX_ERR(?FATAL, preferred_pdn_type_not_supported)}. -%% allocate_ips/7 +%% allocate_ips/8 allocate_ips(AllocInfo, #{bearer_type := BearerType, prefered_bearer_type := PrefBearer} = APNOpts, - SOpts0, DualAddressBearerFlag, Tunnel, Bearer, Context) -> + SOpts0, DualAddressBearerFlag, PCO, Tunnel, Bearer, Context) -> {ReqPDNType, ReqMSv4, ReqMSv6} = session_ip_alloc(BearerType, PrefBearer, SOpts0, DualAddressBearerFlag, AllocInfo), @@ -771,7 +773,7 @@ allocate_ips(AllocInfo, SOpts2 = init_session_ue_ifid(APNOpts, SOpts1), ReqIPs = [normalize_ipv4(ReqMSv4), normalize_ipv6(ReqMSv6)], - {Result0, {UeIP, SOpts3}} = allocate_ips(ReqIPs, SOpts2, Tunnel), + {Result0, {UeIP, SOpts3}} = allocate_ips(ReqIPs, PCO, SOpts2, Tunnel), case Result0 of ok -> case allocate_ips_result(ReqPDNType, BearerType, UeIP) of diff --git a/src/ergw_gtp_gsn_lib.erl b/src/ergw_gtp_gsn_lib.erl index 2b113541..09e4938d 100644 --- a/src/ergw_gtp_gsn_lib.erl +++ b/src/ergw_gtp_gsn_lib.erl @@ -10,10 +10,11 @@ -compile([{parse_transform, do}, {parse_transform, cut}]). --export([connect_upf_candidates/4, create_session/10]). +-export([connect_upf_candidates/4, create_session/11]). -export([triggered_charging_event/4, usage_report/3, close_context/2]). -export([update_tunnel_endpoint/3, handle_peer_change/3, update_tunnel_endpoint/2, apply_bearer_change/5]). +-export([pco_requested_opts/1, session_to_pco/2]). -include_lib("kernel/include/logger.hrl"). -include_lib("gtplib/include/gtp_packet.hrl"). @@ -34,15 +35,17 @@ connect_upf_candidates(APN, Services, NodeSelect, PeerUpNode) -> {ok, {Candidates, SxConnectId}}. -create_session(APN, PAA, DAF, UPSelInfo, Session, SessionOpts, Context, LeftTunnel, LeftBearer, PCC) -> +create_session(APN, PAA, DAF, PCO, UPSelInfo, Session, + SessionOpts, Context, LeftTunnel, LeftBearer, PCC) -> try - create_session_fun(APN, PAA, DAF, UPSelInfo, Session, SessionOpts, Context, LeftTunnel, LeftBearer, PCC) + create_session_fun(APN, PAA, DAF, PCO, UPSelInfo, Session, + SessionOpts, Context, LeftTunnel, LeftBearer, PCC) catch throw:Error -> {error, Error} end. -create_session_fun(APN, PAA, DAF, {Candidates, SxConnectId}, Session, +create_session_fun(APN, PAA, DAF, PCO, {Candidates, SxConnectId}, Session, SessionOpts0, Context0, LeftTunnel, LeftBearer, PCC0) -> ergw_sx_node:wait_connect(SxConnectId), @@ -72,7 +75,8 @@ create_session_fun(APN, PAA, DAF, {Candidates, SxConnectId}, Session, end, {Result6, {Cause, SessionOpts3, RightBearer, Context1}} = - ergw_gsn_lib:allocate_ips(PAA, APNOpts, SessionOpts2, DAF, LeftTunnel, RightBearer0, Context0), + ergw_gsn_lib:allocate_ips(PAA, APNOpts, SessionOpts2, DAF, PCO, + LeftTunnel, RightBearer0, Context0), case Result6 of ok -> ok; {error, Err6} -> throw(Err6#ctx_err{context = Context1, tunnel = LeftTunnel}) @@ -206,6 +210,98 @@ apply_bearer_change(Bearer, URRActions, SendEM, PCtx0, PCC) -> Error end. +%%%=================================================================== +%%% Protocol Configuation Options +%%%=================================================================== + +pco_ppp_ipcp_requested_opts({ms_dns1, <<0,0,0,0>>}, Opts) -> + Opts#{'MS-Primary-DNS-Server' => true}; +pco_ppp_ipcp_requested_opts({ms_dns2, <<0,0,0,0>>}, Opts) -> + Opts#{'MS-Secondary-DNS-Server' => true}; +pco_ppp_ipcp_requested_opts({ms_wins1, <<0,0,0,0>>}, Opts) -> + Opts#{'MS-Primary-NBNS-Server' => true}; +pco_ppp_ipcp_requested_opts({ms_wins2, <<0,0,0,0>>}, Opts) -> + Opts#{'MS-Secondary-NBNS-Server' => true}; +pco_ppp_ipcp_requested_opts(_, Opts) -> + Opts. + +pco_ppp_requested_opts({ipcp,'CP-Configure-Request', _Id, CpReqOpts}, Opts) -> + lists:foldr(fun pco_ppp_ipcp_requested_opts/2, Opts, CpReqOpts); +pco_ppp_requested_opts({?'PCO-DNS-Server-IPv4-Address', <<>>}, Opts) -> + Opts#{'MS-Primary-DNS-Server' => true, + 'MS-Secondary-DNS-Server' => true}; +pco_ppp_requested_opts({?'PCO-DNS-Server-IPv6-Address', <<>>}, Opts) -> + Opts#{'DNS-Server-IPv6-Address' => true}; +pco_ppp_requested_opts({?'PCO-P-CSCF-IPv4-Address', <<>>}, Opts) -> + Opts#{'SIP-Servers-IPv4-Address-List' => true}; +pco_ppp_requested_opts({?'PCO-P-CSCF-IPv6-Address', <<>>}, Opts) -> + Opts#{'SIP-Servers-IPv6-Address-List' => true}; +pco_ppp_requested_opts(_, Opts) -> + Opts. + +pco_requested_opts({0, Requested}) -> + lists:foldr(fun pco_ppp_requested_opts/2, #{}, Requested); +pco_requested_opts(_) -> + []. + +session_to_ppp_ipcp_conf_resp(Verdict, Opt, IPCP) -> + maps:update_with(Verdict, fun(O) -> [Opt|O] end, [Opt], IPCP). + +session_to_ppp_ipcp_conf(#{'MS-Primary-DNS-Server' := DNS}, {ms_dns1, <<0,0,0,0>>}, IPCP) -> + session_to_ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns1, ergw_inet:ip2bin(DNS)}, IPCP); +session_to_ppp_ipcp_conf(#{'MS-Secondary-DNS-Server' := DNS}, {ms_dns2, <<0,0,0,0>>}, IPCP) -> + session_to_ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns2, ergw_inet:ip2bin(DNS)}, IPCP); +session_to_ppp_ipcp_conf(#{'MS-Primary-NBNS-Server' := DNS}, {ms_wins1, <<0,0,0,0>>}, IPCP) -> + session_to_ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins1, ergw_inet:ip2bin(DNS)}, IPCP); +session_to_ppp_ipcp_conf(#{'MS-Secondary-NBNS-Server' := DNS}, {ms_wins2, <<0,0,0,0>>}, IPCP) -> + session_to_ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins2, ergw_inet:ip2bin(DNS)}, IPCP); + +session_to_ppp_ipcp_conf(_SessionOpts, Opt, IPCP) -> + session_to_ppp_ipcp_conf_resp('CP-Configure-Reject', Opt, IPCP). + +session_to_ppp_pco(SessionOpts, {pap, 'PAP-Authentication-Request', Id, _Username, _Password}, Opts) -> + [{pap, 'PAP-Authenticate-Ack', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; +session_to_ppp_pco(SessionOpts, {chap, 'CHAP-Response', Id, _Value, _Name}, Opts) -> + [{chap, 'CHAP-Success', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; +session_to_ppp_pco(SessionOpts, {ipcp,'CP-Configure-Request', Id, CpReqOpts}, Opts) -> + CpRespOpts = lists:foldr(session_to_ppp_ipcp_conf(SessionOpts, _, _), #{}, CpReqOpts), + maps:fold(fun(K, V, O) -> [{ipcp, K, Id, V} | O] end, Opts, CpRespOpts); + +session_to_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv6-Address', <<>>}, Opts) -> + [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} + || DNS <- maps:get('DNS-Server-IPv6-Address', SessionOpts, [])] + ++ [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} + || DNS <- maps:get('3GPP-IPv6-DNS-Servers', SessionOpts, [])] + ++ Opts; +session_to_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv4-Address', <<>>}, Opts) -> + lists:foldr(fun(Key, O) -> + case maps:find(Key, SessionOpts) of + {ok, DNS} -> + [{?'PCO-DNS-Server-IPv4-Address', ergw_inet:ip2bin(DNS)} | O]; + _ -> + O + end + end, Opts, ['MS-Secondary-DNS-Server', 'MS-Primary-DNS-Server']); +session_to_ppp_pco(SessionOpts, {?'PCO-P-CSCF-IPv4-Address', <<>>}, Opts) -> + [{?'PCO-P-CSCF-IPv4-Address', ergw_inet:ip2bin(IP)} + || IP <- maps:get('SIP-Servers-IPv4-Address-List', SessionOpts, [])] + ++ Opts; +session_to_ppp_pco(SessionOpts, {?'PCO-P-CSCF-IPv6-Address', <<>>}, Opts) -> + [{?'PCO-P-CSCF-IPv6-Address', ergw_inet:ip2bin(IP)} + || IP <- maps:get('SIP-Servers-IPv6-Address-List', SessionOpts, [])] + ++ Opts; +session_to_ppp_pco(_SessionOpts, PPPReqOpt, Opts) -> + ?LOG(debug, "Apply PPP Opt: ~p", [PPPReqOpt]), + Opts. + +session_to_pco(SessionOpts, {0, Requested}) -> + case lists:foldr(session_to_ppp_pco(SessionOpts, _, _), [], Requested) of + [] -> undefined; + Opts -> {0, Opts} + end; +session_to_pco(_, _) -> + undefined. + %%==================================================================== %% Charging API %%==================================================================== diff --git a/src/ggsn_gn.erl b/src/ggsn_gn.erl index d647b740..cd5ab575 100644 --- a/src/ggsn_gn.erl +++ b/src/ggsn_gn.erl @@ -143,6 +143,7 @@ handle_request(ReqKey, EUA = maps:get(?'End User Address', IEs, undefined), DAF = proplists:get_bool('Dual Address Bearer Flag', gtp_v1_c:get_common_flags(IEs)), + PCO = pco_requested_opts(IEs), Context1 = update_context_from_gtp_req(Request, Context0), @@ -161,7 +162,7 @@ handle_request(ReqKey, SessionOpts2 = init_session_qos(IEs, SessionOpts1), {Cause, SessionOpts, Context, Bearer, PCC4, PCtx} = - case ergw_gtp_gsn_lib:create_session(APN, pdp_alloc(EUA), DAF, UpSelInfo, Session, + case ergw_gtp_gsn_lib:create_session(APN, pdp_alloc(EUA), DAF, PCO, UpSelInfo, Session, SessionOpts2, Context1, LeftTunnel, LeftBearer1, PCC0) of {ok, Result} -> Result; {error, Err} -> throw(Err) @@ -791,53 +792,19 @@ delete_context(undefined, _, _, _) -> delete_context(From, _, _, _) -> {keep_state_and_data, [{reply, From, ok}]}. -ppp_ipcp_conf_resp(Verdict, Opt, IPCP) -> - maps:update_with(Verdict, fun(O) -> [Opt|O] end, [Opt], IPCP). - -ppp_ipcp_conf(#{'MS-Primary-DNS-Server' := DNS}, {ms_dns1, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns1, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Secondary-DNS-Server' := DNS}, {ms_dns2, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns2, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Primary-NBNS-Server' := DNS}, {ms_wins1, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins1, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Secondary-NBNS-Server' := DNS}, {ms_wins2, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins2, ergw_inet:ip2bin(DNS)}, IPCP); - -ppp_ipcp_conf(_SessionOpts, Opt, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Reject', Opt, IPCP). - -pdp_ppp_pco(SessionOpts, {pap, 'PAP-Authentication-Request', Id, _Username, _Password}, Opts) -> - [{pap, 'PAP-Authenticate-Ack', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; -pdp_ppp_pco(SessionOpts, {chap, 'CHAP-Response', Id, _Value, _Name}, Opts) -> - [{chap, 'CHAP-Success', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; -pdp_ppp_pco(SessionOpts, {ipcp,'CP-Configure-Request', Id, CpReqOpts}, Opts) -> - CpRespOpts = lists:foldr(ppp_ipcp_conf(SessionOpts, _, _), #{}, CpReqOpts), - maps:fold(fun(K, V, O) -> [{ipcp, K, Id, V} | O] end, Opts, CpRespOpts); - -pdp_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv6-Address', <<>>}, Opts) -> - [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} - || DNS <- maps:get('DNS-Server-IPv6-Address', SessionOpts, [])] - ++ [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} - || DNS <- maps:get('3GPP-IPv6-DNS-Servers', SessionOpts, [])] - ++ Opts; -pdp_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv4-Address', <<>>}, Opts) -> - lists:foldr(fun(Key, O) -> - case maps:find(Key, SessionOpts) of - {ok, DNS} -> - [{?'PCO-DNS-Server-IPv4-Address', ergw_inet:ip2bin(DNS)} | O]; - _ -> - O - end - end, Opts, ['MS-Secondary-DNS-Server', 'MS-Primary-DNS-Server']); -pdp_ppp_pco(_SessionOpts, PPPReqOpt, Opts) -> - ?LOG(debug, "Apply PPP Opt: ~p", [PPPReqOpt]), - Opts. +pco_requested_opts(#{?'Protocol Configuration Options' := + #protocol_configuration_options{config = Requested}}) -> + ergw_gtp_gsn_lib:pco_requested_opts(Requested); +pco_requested_opts(_) -> + []. pdp_pco(SessionOpts, #{?'Protocol Configuration Options' := - #protocol_configuration_options{config = {0, PPPReqOpts}}}, IE) -> - case lists:foldr(pdp_ppp_pco(SessionOpts, _, _), [], PPPReqOpts) of - [] -> IE; - Opts -> [#protocol_configuration_options{config = {0, Opts}} | IE] + #protocol_configuration_options{config = Requested}}, IE) -> + case ergw_gtp_gsn_lib:session_to_pco(SessionOpts, Requested) of + undefined -> + IE; + Opts -> + [#protocol_configuration_options{config = Opts} | IE] end; pdp_pco(_SessionOpts, _RequestIEs, IE) -> IE. diff --git a/src/pgw_s5s8.erl b/src/pgw_s5s8.erl index 627396c6..4b2da3b2 100644 --- a/src/pgw_s5s8.erl +++ b/src/pgw_s5s8.erl @@ -179,6 +179,7 @@ handle_request(ReqKey, PAA = maps:get(?'PDN Address Allocation', IEs, undefined), DAF = proplists:get_bool('DAF', gtp_v2_c:get_indication_flags(IEs)), + PCO = pco_requested_opts(IEs), Context1 = update_context_from_gtp_req(Request, Context0), @@ -197,7 +198,7 @@ handle_request(ReqKey, %% SessionOpts = init_session_qos(ReqQoSProfile, SessionOpts1), {Cause, SessionOpts, Context, Bearer, PCC4, PCtx} = - case ergw_gtp_gsn_lib:create_session(APN, pdn_alloc(PAA), DAF, UpSelInfo, Session, + case ergw_gtp_gsn_lib:create_session(APN, pdn_alloc(PAA), DAF, PCO, UpSelInfo, Session, SessionOpts1, Context1, LeftTunnel, LeftBearer1, PCC0) of {ok, Result} -> Result; {error, Err} -> throw(Err) @@ -910,53 +911,19 @@ delete_context(undefined, _, _, _) -> delete_context(From, _, _, _) -> {keep_state_and_data, [{reply, From, ok}]}. -ppp_ipcp_conf_resp(Verdict, Opt, IPCP) -> - maps:update_with(Verdict, fun(O) -> [Opt|O] end, [Opt], IPCP). - -ppp_ipcp_conf(#{'MS-Primary-DNS-Server' := DNS}, {ms_dns1, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns1, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Secondary-DNS-Server' := DNS}, {ms_dns2, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns2, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Primary-NBNS-Server' := DNS}, {ms_wins1, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins1, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Secondary-NBNS-Server' := DNS}, {ms_wins2, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins2, ergw_inet:ip2bin(DNS)}, IPCP); - -ppp_ipcp_conf(_SessionOpts, Opt, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Reject', Opt, IPCP). - -pdn_ppp_pco(SessionOpts, {pap, 'PAP-Authentication-Request', Id, _Username, _Password}, Opts) -> - [{pap, 'PAP-Authenticate-Ack', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; -pdn_ppp_pco(SessionOpts, {chap, 'CHAP-Response', Id, _Value, _Name}, Opts) -> - [{chap, 'CHAP-Success', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; -pdn_ppp_pco(SessionOpts, {ipcp,'CP-Configure-Request', Id, CpReqOpts}, Opts) -> - CpRespOpts = lists:foldr(ppp_ipcp_conf(SessionOpts, _, _), #{}, CpReqOpts), - maps:fold(fun(K, V, O) -> [{ipcp, K, Id, V} | O] end, Opts, CpRespOpts); - -pdn_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv6-Address', <<>>}, Opts) -> - [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} - || DNS <- maps:get('DNS-Server-IPv6-Address', SessionOpts, [])] - ++ [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} - || DNS <- maps:get('3GPP-IPv6-DNS-Servers', SessionOpts, [])] - ++ Opts; -pdn_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv4-Address', <<>>}, Opts) -> - lists:foldr(fun(Key, O) -> - case maps:find(Key, SessionOpts) of - {ok, DNS} -> - [{?'PCO-DNS-Server-IPv4-Address', ergw_inet:ip2bin(DNS)} | O]; - _ -> - O - end - end, Opts, ['MS-Secondary-DNS-Server', 'MS-Primary-DNS-Server']); -pdn_ppp_pco(_SessionOpts, PPPReqOpt, Opts) -> - ?LOG(debug, "Apply PPP Opt: ~p", [PPPReqOpt]), - Opts. +pco_requested_opts(#{?'Protocol Configuration Options' := + #v2_protocol_configuration_options{config = Requested}}) -> + ergw_gtp_gsn_lib:pco_requested_opts(Requested); +pco_requested_opts(_) -> + []. pdn_pco(SessionOpts, #{?'Protocol Configuration Options' := - #v2_protocol_configuration_options{config = {0, PPPReqOpts}}}, IE) -> - case lists:foldr(pdn_ppp_pco(SessionOpts, _, _), [], PPPReqOpts) of - [] -> IE; - Opts -> [#v2_protocol_configuration_options{config = {0, Opts}} | IE] + #v2_protocol_configuration_options{config = Requested}}, IE) -> + case ergw_gtp_gsn_lib:session_to_pco(SessionOpts, Requested) of + undefined -> + IE; + Opts -> + [#v2_protocol_configuration_options{config = Opts} | IE] end; pdn_pco(_SessionOpts, _RequestIEs, IE) -> IE. diff --git a/src/saegw_s11.erl b/src/saegw_s11.erl index 6bb0045a..58ad3979 100644 --- a/src/saegw_s11.erl +++ b/src/saegw_s11.erl @@ -145,6 +145,7 @@ handle_request(ReqKey, PAA = maps:get(?'PDN Address Allocation', IEs, undefined), DAF = proplists:get_bool('DAF', gtp_v2_c:get_indication_flags(IEs)), + PCO = pco_requested_opts(IEs), Context1 = update_context_from_gtp_req(Request, Context0), @@ -163,7 +164,7 @@ handle_request(ReqKey, %% SessionOpts = init_session_qos(ReqQoSProfile, SessionOpts1), {Cause, SessionOpts, Context, Bearer, PCC4, PCtx} = - case ergw_gtp_gsn_lib:create_session(APN, pdn_alloc(PAA), DAF, UpSelInfo, Session, + case ergw_gtp_gsn_lib:create_session(APN, pdn_alloc(PAA), DAF, PCO, UpSelInfo, Session, SessionOpts1, Context1, LeftTunnel, LeftBearer1, PCC0) of {ok, Result} -> Result; {error, Err} -> throw(Err) @@ -731,53 +732,19 @@ delete_context(undefined, _, _, _) -> delete_context(From, _, _, _) -> {keep_state_and_data, [{reply, From, ok}]}. -ppp_ipcp_conf_resp(Verdict, Opt, IPCP) -> - maps:update_with(Verdict, fun(O) -> [Opt|O] end, [Opt], IPCP). - -ppp_ipcp_conf(#{'MS-Primary-DNS-Server' := DNS}, {ms_dns1, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns1, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Secondary-DNS-Server' := DNS}, {ms_dns2, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_dns2, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Primary-NBNS-Server' := DNS}, {ms_wins1, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins1, ergw_inet:ip2bin(DNS)}, IPCP); -ppp_ipcp_conf(#{'MS-Secondary-NBNS-Server' := DNS}, {ms_wins2, <<0,0,0,0>>}, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Nak', {ms_wins2, ergw_inet:ip2bin(DNS)}, IPCP); - -ppp_ipcp_conf(_SessionOpts, Opt, IPCP) -> - ppp_ipcp_conf_resp('CP-Configure-Reject', Opt, IPCP). - -pdn_ppp_pco(SessionOpts, {pap, 'PAP-Authentication-Request', Id, _Username, _Password}, Opts) -> - [{pap, 'PAP-Authenticate-Ack', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; -pdn_ppp_pco(SessionOpts, {chap, 'CHAP-Response', Id, _Value, _Name}, Opts) -> - [{chap, 'CHAP-Success', Id, maps:get('Reply-Message', SessionOpts, <<>>)}|Opts]; -pdn_ppp_pco(SessionOpts, {ipcp,'CP-Configure-Request', Id, CpReqOpts}, Opts) -> - CpRespOpts = lists:foldr(ppp_ipcp_conf(SessionOpts, _, _), #{}, CpReqOpts), - maps:fold(fun(K, V, O) -> [{ipcp, K, Id, V} | O] end, Opts, CpRespOpts); - -pdn_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv6-Address', <<>>}, Opts) -> - [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} - || DNS <- maps:get('DNS-Server-IPv6-Address', SessionOpts, [])] - ++ [{?'PCO-DNS-Server-IPv6-Address', ergw_inet:ip2bin(DNS)} - || DNS <- maps:get('3GPP-IPv6-DNS-Servers', SessionOpts, [])] - ++ Opts; -pdn_ppp_pco(SessionOpts, {?'PCO-DNS-Server-IPv4-Address', <<>>}, Opts) -> - lists:foldr(fun(Key, O) -> - case maps:find(Key, SessionOpts) of - {ok, DNS} -> - [{?'PCO-DNS-Server-IPv4-Address', ergw_inet:ip2bin(DNS)} | O]; - _ -> - O - end - end, Opts, ['MS-Secondary-DNS-Server', 'MS-Primary-DNS-Server']); -pdn_ppp_pco(_SessionOpts, PPPReqOpt, Opts) -> - ?LOG(debug, "Apply PPP Opt: ~p", [PPPReqOpt]), - Opts. +pco_requested_opts(#{?'Protocol Configuration Options' := + #v2_protocol_configuration_options{config = Requested}}) -> + ergw_gtp_gsn_lib:pco_requested_opts(Requested); +pco_requested_opts(_) -> + []. pdn_pco(SessionOpts, #{?'Protocol Configuration Options' := - #v2_protocol_configuration_options{config = {0, PPPReqOpts}}}, IE) -> - case lists:foldr(pdn_ppp_pco(SessionOpts, _, _), [], PPPReqOpts) of - [] -> IE; - Opts -> [#v2_protocol_configuration_options{config = {0, Opts}} | IE] + #v2_protocol_configuration_options{config = Requested}}, IE) -> + case ergw_gtp_gsn_lib:session_to_pco(SessionOpts, Requested) of + undefined -> + IE; + Opts -> + [#v2_protocol_configuration_options{config = Opts} | IE] end; pdn_pco(_SessionOpts, _RequestIEs, IE) -> IE. From 427d1cb466ebe208d5a91d261d33252f7a09f3bb Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Mon, 16 Nov 2020 15:15:59 +0100 Subject: [PATCH 2/8] add DHCPv4 pool support Request and release IPs from DHCP server --- rebar.config | 1 + rebar.lock | 4 +++ src/ergw.app.src | 1 + src/ergw_config.erl | 1 + src/ergw_ip_pool.erl | 4 ++- src/ergw_ip_pool_sup.erl | 22 +++++++++++- src/ergw_socket.erl | 6 +++- test/ergw_pgw_test_lib.erl | 7 ++-- test/ergw_test_lib.erl | 4 +-- test/pgw_SUITE.erl | 71 +++++++++++++++++++++++++++++++++++--- 10 files changed, 109 insertions(+), 12 deletions(-) diff --git a/rebar.config b/rebar.config index 315af1a7..060a117a 100644 --- a/rebar.config +++ b/rebar.config @@ -11,6 +11,7 @@ {prometheus_cowboy, "0.1.8"}, {erlando, {git, "https://github.com/travelping/erlando.git", {tag, "1.0.3"}}}, {netdata, {git, "https://github.com/RoadRunnr/erl_netdata.git", {ref, "cbd6eaf"}}}, + {dhcp, {git, "https://github.com/RoadRunnr/dhcp.git", {branch, "feature/modernize"}}}, {gtplib, {git, "https://github.com/travelping/gtplib.git", {branch, "master"}}}, {pfcplib, {git, "https://github.com/travelping/pfcplib.git", {branch, "master"}}}, {ergw_aaa, {git, "git://github.com/travelping/ergw_aaa", {tag, "3.6.2"}}}, diff --git a/rebar.lock b/rebar.lock index c73c0fbd..0718ce7c 100644 --- a/rebar.lock +++ b/rebar.lock @@ -2,6 +2,10 @@ [{<<"accept">>,{pkg,<<"accept">>,<<"0.3.5">>},2}, {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.8.0">>},0}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.9.1">>},1}, + {<<"dhcp">>, + {git,"https://github.com/RoadRunnr/dhcp.git", + {ref,"31c51ec0014d363f435055e58574e6f725703940"}}, + 0}, {<<"eradius">>, {git,"https://github.com/travelping/eradius.git", {ref,"7147d879177f3a9ad88f909a12e41e1c565269b0"}}, diff --git a/src/ergw.app.src b/src/ergw.app.src index 4a5beacd..55b833ab 100644 --- a/src/ergw.app.src +++ b/src/ergw.app.src @@ -8,6 +8,7 @@ prometheus, cowboy, prometheus_cowboy, prometheus_diameter_collector, jsx, compiler, os_mon, jobs]}, + {included_applications, [dhcp]}, {mod, {ergw_app, []}}, {registered, []} ]}. diff --git a/src/ergw_config.erl b/src/ergw_config.erl index 9cbb0531..8275d1a1 100644 --- a/src/ergw_config.erl +++ b/src/ergw_config.erl @@ -14,6 +14,7 @@ validate_options/4, validate_apn_name/1, check_unique_keys/2, + check_unique_elements/2, validate_ip_cfg_opt/2, opts_fold/3, get_opt/3 diff --git a/src/ergw_ip_pool.erl b/src/ergw_ip_pool.erl index 33644426..281a57b2 100644 --- a/src/ergw_ip_pool.erl +++ b/src/ergw_ip_pool.erl @@ -73,6 +73,8 @@ validate_options(Options) -> case ergw_config:get_opt(handler, Options, ergw_local_pool) of ergw_local_pool -> ergw_local_pool:validate_options(Options); + ergw_dhcp_pool -> + ergw_dhcp_pool:validate_options(Options); Handler -> throw({error, {options, {handler, Handler}}}) end. @@ -102,7 +104,7 @@ static_ip_info(opts, _) -> #{}; static_ip_info(release, _) -> ok. with_pool(Pool, Fun) -> - case application:get_env(ip_pools) of + case application:get_env(ergw, ip_pools) of {ok, #{Pool := #{handler := Handler}}} -> Fun(Handler); _ -> diff --git a/src/ergw_ip_pool_sup.erl b/src/ergw_ip_pool_sup.erl index 874f7fb6..05a6a53c 100644 --- a/src/ergw_ip_pool_sup.erl +++ b/src/ergw_ip_pool_sup.erl @@ -10,7 +10,7 @@ -behaviour(supervisor). %% API --export([start_link/0, start_local_pool_sup/0]). +-export([start_link/0, start_local_pool_sup/0, start_dhcp_pool_sup/0]). %% Supervisor callbacks -export([init/1]). @@ -41,6 +41,26 @@ start_local_pool_sup() -> [supervisor:start_child(?SERVER, Cs) || Cs <- ChildSpecs], ok. +start_dhcp_pool_sup() -> + ChildSpecs = + [#{id => ergw_dhcp_socket, + start => {ergw_dhcp_socket, start_link, []}, + restart => permanent, + type => worker, + modules => [ergw_dhcp_pool_reg]}, + #{id => ergw_dhcp_pool_reg, + start => {ergw_dhcp_pool_reg, start_link, []}, + restart => permanent, + type => worker, + modules => [ergw_dhcp_pool_reg]}, + #{id => ergw_dhcp_pool_sup, + start => {ergw_dhcp_pool_sup, start_link, []}, + restart => permanent, + type => supervisor, + modules => [ergw_dhcp_pool_sup]}], + [supervisor:start_child(?SERVER, Cs) || Cs <- ChildSpecs], + ok. + %% =================================================================== %% Supervisor callbacks %% =================================================================== diff --git a/src/ergw_socket.erl b/src/ergw_socket.erl index 06f90a29..373d560e 100644 --- a/src/ergw_socket.erl +++ b/src/ergw_socket.erl @@ -20,7 +20,9 @@ start_link('gtp-c', Opts) -> start_link('gtp-u', Opts) -> ergw_gtp_u_socket:start_link(Opts); start_link('pfcp', Opts) -> - ergw_sx_socket:start_link(Opts). + ergw_sx_socket:start_link(Opts); +start_link('dhcp', Opts) -> + ergw_dhcp_socket:start_link(Opts). %%%=================================================================== %%% Options Validation @@ -43,6 +45,8 @@ validate_option(Name, Values) ergw_gtp_socket:validate_options(Name, Values); 'pfcp' -> ergw_sx_socket:validate_options(Name, Values); + 'dhcp' -> + ergw_dhcp_socket:validate_options(Name, Values); _ -> throw({error, {options, {Name, Values}}}) end; diff --git a/test/ergw_pgw_test_lib.erl b/test/ergw_pgw_test_lib.erl index 838ccd6d..f79ea916 100644 --- a/test/ergw_pgw_test_lib.erl +++ b/test/ergw_pgw_test_lib.erl @@ -310,6 +310,8 @@ validate_pdn_type(ipv6, IEs) -> validate_pdn_cfg(ipv6, ip validate_pdn_type({default, static_ipv6}, IEs) -> validate_pdn_cfg(ipv6, ipv6, IEs); validate_pdn_type({default, static_host_ipv6}, IEs) -> validate_pdn_cfg(ipv6, ipv6, IEs); +validate_pdn_type({Req, false, _}, IEs) -> validate_pdn_cfg(Req, Req, IEs); + validate_pdn_type(_Type, IEs) -> validate_pdn_cfg(ipv4v6, ipv4v6, IEs). @@ -772,7 +774,7 @@ validate_cause(_Type, {_, _, _} = SubType, {{ipv4v6, false, prefV4}, new_pdn_type_due_to_single_address_bearer_only}, {{ipv4v6, false, prefV6}, new_pdn_type_due_to_single_address_bearer_only} ], - ExpectedCause = proplists:get_value(SubType, CauseList), + ExpectedCause = proplists:get_value(SubType, CauseList, request_accepted), ?equal(ExpectedCause, Cause); validate_cause(_Type, _SubType, Response) -> @@ -1161,7 +1163,8 @@ apn(proxy_apn) -> ?'APN-PROXY'; apn(async_sx) -> [<<"async-sx">>]; apn({_, _, APN}) when APN =:= v4only; APN =:= prefV4; - APN =:= v6only; APN =:= prefV6 -> + APN =:= v6only; APN =:= prefV6; + APN =:= dhcp -> [atom_to_binary(APN, latin1)]; apn([Label|_] = APN) when is_binary(Label) -> APN; apn(_) -> ?'APN-ExAmPlE'. diff --git a/test/ergw_test_lib.erl b/test/ergw_test_lib.erl index 164fdab7..8a97f297 100644 --- a/test/ergw_test_lib.erl +++ b/test/ergw_test_lib.erl @@ -74,7 +74,7 @@ lib_init_per_suite(Config0) -> {_, AppCfg} = lists:keyfind(app_cfg, 1, Config0), %% let it crash if undefined Config = init_ets(Config0), - [application:load(App) || App <- [cowboy, ergw, ergw_aaa]], + [application:load(App) || App <- [cowboy, ergw, ergw_aaa, dhcp]], meck_init(Config), load_config(AppCfg), {ok, _} = application:ensure_all_started(ergw), @@ -101,7 +101,7 @@ lib_end_per_suite(Config) -> ok = ergw_test_sx_up:stop('sgw-u'), ok = ergw_test_sx_up:stop('tdf-u'), ?config(table_owner, Config) ! stop, - [application:stop(App) || App <- [ranch, cowboy, ergw, ergw_aaa]], + [application:stop(App) || App <- [ranch, cowboy, ergw, ergw_aaa, dhcp]], ok. load_config(AppCfg) -> diff --git a/test/pgw_SUITE.erl b/test/pgw_SUITE.erl index 8583ab55..c634a734 100644 --- a/test/pgw_SUITE.erl +++ b/test/pgw_SUITE.erl @@ -44,6 +44,26 @@ ]} ]}, + {dhcp, + [{server_id, {127,0,0,1}}, + {next_server, {127,0,0,1}}, + {interface, <<"lo">>}, + {authoritative, true}, + {lease_file, "/var/run/dhcp_leases.dets"}, + {subnets, + [{subnet, + {172,20,48,0}, %% Network, + {255,255,255,0}, %% Netmask, + {{172,20,48,5},{172,20,48,100}}, %% Range, + [{1, {255,255,255,0}}, %% Subnet Mask, + {28, {172,20,48,255}}, %% Broadcast Address, + {3, [{172,20,48,1}]}, %% Router, + {15, "wlan"}, %% Domain Name, + {6, [{172,20,48,1}, {172,20,48,1}]}, %% Domain Name Server, + {51, 3600}]} %% Address Lease Time, + ]} + ]}, + {ergw, [{'$setup_vars', [{"ORIGIN", {value, "epc.mnc001.mcc001.3gppnetwork.org"}}]}, {sockets, @@ -66,7 +86,15 @@ {socket, 'cp-socket'}, {ip, ?MUST_BE_UPDATED}, {reuseaddr, true} - ]} + ]}, + + {'dhcp-v4', + [{type, dhcp}, + %%{ip, ?MUST_BE_UPDATED}, + {ip, {127,100,0,1}}, + {port, random}, + {reuseaddr, true} + ]} ]}, {ip_pools, @@ -102,7 +130,13 @@ {'DNS-Server-IPv6-Address', [{16#2001, 16#4860, 16#4860, 0, 0, 0, 0, 16#8888}, {16#2001, 16#4860, 16#4860, 0, 0, 0, 0, 16#8844}]} - ]} + ]}, + + {'pool-DHCP', [{handler, ergw_dhcp_pool}, + {ipv4, [{socket, 'dhcp-v4'}, + {id, {172,20,48,1}}, + {servers, [broadcast]}]} + ]} ]}, {handlers, @@ -200,7 +234,10 @@ {prefered_bearer_type, 'IPv4'}]}, {[<<"async-sx">>], [{vrf, sgi}, - {ip_pools, ['pool-A']}]} + {ip_pools, ['pool-A']}]}, + {[<<"dhcp">>], + [{vrf, sgi}, + {ip_pools, ['pool-DHCP']}]} %% {'_', [{vrf, wildcard}]} ]}, @@ -294,7 +331,7 @@ {irx, [{features, ['Access']}]}, {sgi, [{features, ['SGi-LAN']}]} ]}, - {ip_pools, ['pool-A']}] + {ip_pools, ['pool-A', 'pool-DHCP']}] }, {"topon.sx.prox01.$ORIGIN", [connect]}, {"topon.sx.prox03.$ORIGIN", [connect, {ip_pools, ['pool-B', 'pool-C']}]} @@ -666,8 +703,11 @@ common() -> sx_fail() -> [sx_connect_fail]. +ipv4_only() -> + [dhcp_ipv4_pool]. + groups() -> - [{ipv4, [], common()}, + [{ipv4, [], common() ++ ipv4_only()}, {ipv6, [], common()}, {sx_fail, [{ipv4, [], sx_fail()}, {ipv6, [], sx_fail()}] @@ -887,6 +927,10 @@ init_per_testcase(gtp_idle_timeout, Config) -> set_apn_key('Idle-Timeout', 300), setup_per_testcase(Config), Config; +init_per_testcase(dhcp_ipv4_pool, Config) -> + setup_per_testcase(Config), + {ok, _} = application:ensure_all_started(dhcp), + Config; init_per_testcase(_, Config) -> setup_per_testcase(Config), Config. @@ -5040,6 +5084,23 @@ up_inactivity_timer(Config) -> ?equal([], outstanding_requests()), ok = meck:wait(?HUT, terminate, '_', ?TIMEOUT), + meck_validate(Config), + ok. + +%%-------------------------------------------------------------------- +dhcp_ipv4_pool() -> + [{doc, "Check simple Create Session, Delete Session sequence with DHCP IPv4 pool"}]. +dhcp_ipv4_pool(Config) -> + {GtpC, _, _} = create_session({ipv4, false, dhcp}, Config), + + ct:sleep({seconds, 10}), + + delete_session(GtpC), + + ?equal([], outstanding_requests()), + ok = meck:wait(?HUT, terminate, '_', ?TIMEOUT), + + meck_validate(Config), ok. From 5c6721caf8452010a3d098f90641557f651ac90d Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Fri, 7 Aug 2020 17:08:36 +0200 Subject: [PATCH 3/8] run travis tests with sudo --- .travis.yml | 4 +- src/ergw_dhcp_pool.erl | 384 ++++++++++++++++++++++++++++++ src/ergw_dhcp_pool_reg.erl | 85 +++++++ src/ergw_dhcp_pool_sup.erl | 44 ++++ src/ergw_dhcp_socket.erl | 465 +++++++++++++++++++++++++++++++++++++ test/dhcp_pool_SUITE.erl | 235 +++++++++++++++++++ 6 files changed, 1216 insertions(+), 1 deletion(-) create mode 100644 src/ergw_dhcp_pool.erl create mode 100644 src/ergw_dhcp_pool_reg.erl create mode 100644 src/ergw_dhcp_pool_sup.erl create mode 100644 src/ergw_dhcp_socket.erl create mode 100644 test/dhcp_pool_SUITE.erl diff --git a/.travis.yml b/.travis.yml index 71682fc0..a3baa224 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ env: - secure: "JpJScMSO4Sqj4odjSFEpshqFk93ZyVkhMNNc9xh2yfRpkHFNXXnhLYhKLA/nr1gcd1f9jnWz++1cqa4MQXRGanDT3+iGNO12R/M3ZVT26ywV0QEmj6z/acsv5wC34hje8/zWAgKWBsIxswFVT+RRPzNOQNVq6JPLZSp014qX5P+ChwmPeCG2kY/od9fsftp7ZjqyhqOOlXGJeEInvF5SD1RqnVMYP2OEQnGQyAHg9aoczO1cZnpZSRQFTqtDzwG1lp21oqsk2IVSCTqXdD1+GNSZCV4oHddXwGJICN9klSHUnxKb7/rFwbVh090+wP7PA+4eqCOOCGIIePFRMDiux6wX07p4wFhtt6/ZGmOs+1kV9ZR4W9FP+rv/0LUlMpmd52WoWkn1kGA/fEr/Jff3n+PjcXd25W/ASeKciahhvXLudeoauP3/wB/3gfFBSnaVR1FvU10rAdL9X+W13z+UiL5C+1qDRjs/6OFpq0T4KfWkxxy1+9eG5J5u8gsKQ+1THvSerddGX77ZvnHu1m1A8z8fGF6toMUvR7EJvr/wpeVwRO/SOB03JYUYdeRnGYXYbOt4UE2ovh/g34mnxQptKW3Pk9aqT2x/Uamsn661tRJL7kVCGOnIUohh4Ynf2tniLbWsRorZlltcIxK4kaKuNfsV/gpvAaguDkeY0ul5AqM=" before_script: + - sudo sed -i '/^Defaults\tsecure_path.*$/ d' /etc/sudoers - wget https://s3.amazonaws.com/rebar3/rebar3 - chmod u+x ./rebar3 # Add an IPv6 config - see the corresponding Travis issue @@ -49,7 +50,8 @@ script: - ip -br addr - source test/env.sh - ./rebar3 compile - - ./rebar3 do xref, ct + - ./rebar3 xref + - sudo -E ./rebar3 ct - (./rebar3 as test do coveralls send || /bin/true) - (cd _build/test/logs/; zip -1r ../../../ct-logs.zip . ) diff --git a/src/ergw_dhcp_pool.erl b/src/ergw_dhcp_pool.erl new file mode 100644 index 00000000..f210eafe --- /dev/null +++ b/src/ergw_dhcp_pool.erl @@ -0,0 +1,384 @@ +%% Copyright 2020, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(ergw_dhcp_pool). + +-behavior(gen_server). +-behavior(ergw_ip_pool). + +-compile([{parse_transform, cut}]). + +%% API +-export([start_ip_pool/2, send_pool_request/2, wait_pool_response/1, release/1, ip/1, opts/1]). +-export([start_link/3, start_link/4]). +-export([validate_options/1]). + +-ignore_xref([start_link/3, start_link/4]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(pool, {id, socket, servers}). +-record(state, {name, ipv4, ipv6, outstanding}). +%% -record(lease, {ip, client_id}). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("dhcp/include/dhcp.hrl"). + +-define(IS_IPv4(X), (is_tuple(X) andalso tuple_size(X) == 4)). +-define(IS_IPv6(X), (is_tuple(X) andalso tuple_size(X) == 8)). + +-define(ZERO_IPv4, {0,0,0,0}). +-define(ZERO_IPv6, {0,0,0,0,0,0,0,0}). +-define(UE_INTERFACE_ID, {0,0,0,0,0,0,0,1}). + +-define(IPv4Opts, ['Framed-Pool', + 'MS-Primary-DNS-Server', + 'MS-Secondary-DNS-Server', + 'MS-Primary-NBNS-Server', + 'MS-Secondary-NBNS-Server']). +-define(IPv6Opts, ['Framed-IPv6-Pool', + 'DNS-Server-IPv6-Address', + '3GPP-IPv6-DNS-Servers']). + +%%==================================================================== +%% API +%%==================================================================== + +start_ip_pool(Name, Opts0) + when is_binary(Name) -> + Opts = validate_options(Opts0), + ergw_ip_pool_sup:start_dhcp_pool_sup(), + ergw_dhcp_pool_sup:start_ip_pool(Name, Opts). + +start_link(PoolName, Pool, Opts) -> + gen_server:start_link(?MODULE, [PoolName, Pool], Opts). + +start_link(ServerName, PoolName, Pool, Opts) -> + gen_server:start_link(ServerName, ?MODULE, [PoolName, Pool], Opts). + +send_pool_request(ClientId, {Pool, IP, PrefixLen, Opts}) when is_pid(Pool) -> + send_request(Pool, {get, ClientId, IP, PrefixLen, Opts}); +send_pool_request(ClientId, {Pool, IP, PrefixLen, Opts}) -> + send_request(ergw_dhcp_pool_reg:lookup(Pool), {get, ClientId, IP, PrefixLen, Opts}). + +wait_pool_response(ReqId) -> + case wait_response(ReqId, 1000) of + %% {reply, {error, _}} -> + %% undefined; + {reply, Reply} -> + Reply; + timeout -> + {error, timeout}; + {error, _} = Error -> + Error + end. + +release({_, Server, {IP, _}, SrvId, _Opts}) -> + %% see alloc_reply + gen_server:cast(Server, {release, IP, SrvId}). + +-if(?OTP_RELEASE >= 23). +send_request(Server, Request) -> + gen_server:send_request(Server, Request). + +wait_response(Mref, Timeout) -> + gen_server:wait_response(Mref, Timeout). +-else. +send_request(Server, Request) -> + ReqF = fun() -> exit({reply, gen_server:call(Server, Request)}) end, + try spawn_monitor(ReqF) of + {_, Mref} -> Mref + catch + error: system_limit = E -> + %% Make send_request async and fake a down message + Ref = erlang:make_ref(), + self() ! {'DOWN', Ref, process, Server, {error, E}}, + Ref + end. + +wait_response(Mref, Timeout) + when is_reference(Mref) -> + receive + {'DOWN', Mref, _, _, Reason} -> + Reason + after Timeout -> + timeout + end. + +-endif. + +ip({?MODULE, _, IP, _, _}) -> IP. +opts({?MODULE, _, _, _, Opts}) -> Opts. + +%%==================================================================== +%%% Options Validation +%%%=================================================================== + +-define(DefaultOptions, [{handler, ?MODULE}]). +-define(DefaultIPv4Opts, [{socket, "undefined"}, {servers, []}, {id, undefined}]). +-define(DefaultIPv6Opts, [{socket, "undefined"}, {servers, []}, {id, undefined}]). + +validate_options(Options) -> + ?LOG(debug, "IP Pool Options: ~p", [Options]), + ergw_config:validate_options(fun validate_option/2, Options, ?DefaultOptions, map). + +validate_server(ipv4, IP) when ?IS_IPv4(IP) -> + IP; +validate_server(ipv6, IP) when ?IS_IPv6(IP) -> + IP; +validate_server(_, broadcast = Value) -> + Value; +validate_server(Type, Value) -> + throw({error, {options, {Type, {servers, Value}}}}). + +validate_ip_option(_, socket, Value) when is_atom(Value)-> + Value; +validate_ip_option(Type, servers, Servers) + when is_list(Servers), length(Servers) /= 0 -> + ergw_config:check_unique_elements(servers, Servers), + [validate_server(Type, Server) || Server <- Servers]; +validate_ip_option(ipv4, id, Value) when ?IS_IPv4(Value) -> + Value; +validate_ip_option(ipv6, id, Value) when ?IS_IPv6(Value) -> + Value; +validate_ip_option(Type, Opt, Value) -> + throw({error, {options, {Type, {Opt, Value}}}}). + +validate_option(handler, Value) -> + Value; +validate_option(ipv4, Opts) -> + ergw_config:validate_options(validate_ip_option(ipv4, _, _), Opts, ?DefaultIPv4Opts, map); +validate_option(ipv6, Opts) -> + ergw_config:validate_options(validate_ip_option(ipv6, _, _), Opts, ?DefaultIPv6Opts, map); +validate_option(Opt, Value) -> + throw({error, {options, {Opt, Value}}}). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([Name, Opts]) -> + ergw_dhcp_pool_reg:register(Name), + State = #state{name = Name, + ipv4 = init_pool(maps:get(ipv4, Opts, undefined)), + ipv6 = init_pool(maps:get(ipv6, Opts, undefined)), + outstanding = #{}}, + ?LOG(debug, "init Pool state: ~p", [State]), + {ok, State}. + +handle_call({get, ClientId, ipv4, PrefixLen, ReqOpts}, From, State) -> + dhcpv4_init(ClientId, undefined, PrefixLen, ReqOpts, From, State); +handle_call({get, ClientId, {_,_,_,_} = IP, PrefixLen, ReqOpts}, From, State) -> + dhcpv4_init(ClientId, IP, PrefixLen, ReqOpts, From, State); + +handle_call({get, _ClientId, IP, PrefixLen, _ReqOpts}, _From, State) -> + Error = {unsupported, {IP, PrefixLen}}, + {reply, {error, Error}, State}; + +handle_call(Request, _From, State) -> + ?LOG(warning, "handle_call: ~p", [Request]), + {reply, error, State}. + +handle_cast({release, IP, SrvId}, State) -> + dhcpv4_release(IP, SrvId, State), + {noreply, State}; + +handle_cast(Msg, State) -> + ?LOG(debug, "handle_cast: ~p", [Msg]), + {noreply, State}. + +handle_info({'DOWN', Mref, _, _, Reason}, #state{outstanding = OutS0} = State) + when is_map_key(Mref, OutS0) -> + {From, OutS} = maps:take(Mref, OutS0), + handle_reply(From, Reason), + {noreply, State#state{outstanding = OutS}}; + +handle_info(Info, State) -> + ?LOG(debug, "handle_info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +init_pool(#{id := Id, socket := Socket, servers := Srvs}) -> + #pool{id = Id, socket = Socket, servers = Srvs}; +init_pool(_) -> + undefined. + +handle_reply(From, {reply, {ok, IP, SrvId, Opts}}) -> + gen_server:reply(From, {?MODULE, self(), IP, SrvId, Opts}); +handle_reply(From, {reply, Reply}) -> + gen_server:reply(From, Reply); +handle_reply(From, Reason) -> + gen_server:reply(From, {error, Reason}). + +%%%=================================================================== +%%% DHCPv4 functions +%%%=================================================================== + +%% dhcpv4_spawn/3 +dhcpv4_spawn(Fun, From, #state{outstanding = OutS} = State) -> + ReqF = fun() -> exit({reply, Fun()}) end, + {_, Mref} = spawn_monitor(ReqF), + {noreply, State#state{outstanding = maps:put(Mref, From, OutS)}}. + +%% dhcpv4_init/6 +dhcpv4_init(ClientId, IP, PrefixLen, ReqOpts, From, #state{ipv4 = Pool} = State) + when is_record(Pool, pool) -> + ReqF = fun() -> dhcpv4_init_f(ClientId, IP, PrefixLen, ReqOpts, Pool) end, + dhcpv4_spawn(ReqF, From, State). + +dhcpv4_init_f(ClientId, ReqIP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) -> + Srv = choose_server(Srvs), + Opts = dhcpv4_opts(ClientId, ReqIP, PrefixLen, ReqOpts), + case dhcpv4_discover(Pool, Srv, Opts) of + #dhcp{} = Offer -> + dhcpv4_request(Pool, ClientId, Opts, Offer); + Other -> + Other + end. + +dhcpv4_discover(Pool, Srv, Opts) -> + DHCP = #dhcp{ + op = ?BOOTREQUEST, + options = Opts#{?DHO_DHCP_MESSAGE_TYPE => ?DHCPDISCOVER} + }, + ReqId = dhcpv4_send_request(Pool, Srv, DHCP), + + TimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + dhcpv4_offer(ReqId, {abs, TimeOut}). + +dhcpv4_offer(ReqId, Timeout) -> + case ergw_dhcp_socket:wait_response(ReqId, Timeout) of + {ok, #dhcp{options = #{?DHO_DHCP_MESSAGE_TYPE := ?DHCPOFFER}} = Answer} -> + Answer; + {error, timeout} = Error -> + Error; + {error, _} -> + dhcpv4_offer(ReqId, Timeout) + end. + +dhcpv4_request(Pool, ClientId, Opts, #dhcp{siaddr = SiAddr0} = Offer) -> + OptsFilter = [?DHO_DHCP_SERVER_IDENTIFIER], + SiAddr = maps:get(?DHO_DHCP_SERVER_IDENTIFIER, Offer#dhcp.options, SiAddr0), + + DHCP = Offer#dhcp{ + op = ?BOOTREQUEST, + ciaddr = {0, 0, 0, 0}, + yiaddr = {0, 0, 0, 0}, + siaddr = {0, 0, 0, 0}, + options = + maps:merge( + Opts#{?DHO_DHCP_MESSAGE_TYPE => ?DHCPREQUEST, + ?DHO_DHCP_REQUESTED_ADDRESS => Offer#dhcp.yiaddr}, + maps:with(OptsFilter, Offer#dhcp.options)) + }, + ReqId = dhcpv4_send_request(Pool, SiAddr, DHCP), + + ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + case dhcpv4_answer(ReqId, {abs, ReqTimeOut}) of + #dhcp{yiaddr = IP, + options = #{?DHO_DHCP_MESSAGE_TYPE := ?DHCPACK} = RespOpts} = Answer -> + SrvId = choose_next(Answer, SiAddr), + {ok, {IP, 32}, {SrvId, ClientId}, dhcpv4_resp_opts(RespOpts)}; + #dhcp{} -> + {error, failed}; + {error, _} = Error -> + Error + end. + +dhcpv4_release(IP, {Srv, ClientId}, #state{ipv4 = Pool}) -> + Opts = #{?DHO_DHCP_MESSAGE_TYPE => ?DHCPRELEASE, + ?DHO_DHCP_SERVER_IDENTIFIER => Srv, + ?DHO_DHCP_CLIENT_IDENTIFIER => client_id(ClientId)}, + DHCP = #dhcp{ + op = ?BOOTREQUEST, + ciaddr = IP, + options = Opts + }, + _ReqId = dhcpv4_send_request(Pool, Srv, DHCP). + +dhcpv4_answer(ReqId, Timeout) -> + case ergw_dhcp_socket:wait_response(ReqId, Timeout) of + {ok, #dhcp{options = #{?DHO_DHCP_MESSAGE_TYPE := Type}} = Answer} + when Type =:= ?DHCPDECLINE; + Type =:= ?DHCPACK; + Type =:= ?DHCPNAK -> + Answer; + {ok, #dhcp{} = Answer} -> + ?LOG(debug, "unexpected DHCP response ~p", [Answer]), + dhcpv4_answer(ReqId, Timeout); + {error, timeout} = Error -> + Error; + {error, _} -> + dhcpv4_answer(ReqId, Timeout) + end. + +dhcpv4_send_request(#pool{id = GiAddr, socket = Socket}, Srv, DHCP) -> + ergw_dhcp_socket:send_request(Socket, Srv, DHCP#dhcp{giaddr = GiAddr}). + +dhcpv4_req_list('MS-Primary-DNS-Server', _, Opts) -> + ordsets:add_element(?DHO_DOMAIN_NAME_SERVERS, Opts); +dhcpv4_req_list('MS-Secondary-DNS-Server', _, Opts) -> + ordsets:add_element(?DHO_DOMAIN_NAME_SERVERS, Opts); +dhcpv4_req_list('MS-Primary-NBNS-Server', _, Opts) -> + ordsets:add_element(?DHO_NETBIOS_NAME_SERVERS, Opts); +dhcpv4_req_list('MS-Secondary-NBNS-Server', _, Opts) -> + ordsets:add_element(?DHO_NETBIOS_NAME_SERVERS, Opts); +dhcpv4_req_list('SIP-Servers-IPv4-Address-List', _, Opts) -> + ordsets:add_element(?DHO_SIP_SERVERS, Opts); +dhcpv4_req_list(_, _, Opts) -> + Opts. + +dhcpv4_opts(ClientId, _ReqIP, _PrefixLen, ReqOpts) -> + ReqList0 = ordsets:from_list([?DHO_DHCP_LEASE_TIME]), + ReqList = ordsets:to_list(maps:fold(fun dhcpv4_req_list/3, ReqList0, ReqOpts)), + #{?DHO_DHCP_CLIENT_IDENTIFIER => client_id(ClientId), + ?DHO_DHCP_PARAMETER_REQUEST_LIST => ReqList}. + +choose_server(Srvs) when is_list(Srvs) -> + lists:nth(rand:uniform(length(Srvs)), Srvs). + +choose_next(#dhcp{siaddr = ?ZERO_IPv4, options = #{?DHO_DHCP_SERVER_IDENTIFIER := Srv}}, _) -> + Srv; +choose_next(#dhcp{siaddr = Srv}, _) when ?IS_IPv4(Srv) -> + Srv; +choose_next(_, Srv) -> + %% fallback, but not having siaddr and Server Id options violates RFC 2131. + Srv. + +client_id(ClientId) when is_binary(ClientId) -> + ClientId; +client_id(ClientId) when is_integer(ClientId) -> + integer_to_binary(ClientId); +client_id(ClientId) when is_list(ClientId) -> + iolist_to_binary(ClientId). + +dhcpv4_resp_opts(?DHO_DOMAIN_NAME_SERVERS, [Prim, Sec | _], Opts) -> + Opts#{'MS-Primary-DNS-Server' => Prim, 'MS-Secondary-DNS-Server' => Sec}; +dhcpv4_resp_opts(?DHO_DOMAIN_NAME_SERVERS, [Prim | _], Opts) -> + Opts#{'MS-Primary-DNS-Server' => Prim}; +dhcpv4_resp_opts(?DHO_NETBIOS_NAME_SERVERS, [Prim, Sec | _], Opts) -> + Opts#{'MS-Primary-NBNS-Server' => Prim, 'MS-Secondary-NBNS-Server' => Sec}; + dhcpv4_resp_opts(?DHO_NETBIOS_NAME_SERVERS, [Prim | _], Opts) -> + Opts#{'MS-Primary-NBNS-Server' => Prim}; +dhcpv4_resp_opts(?DHO_SIP_SERVERS, V, Opts) -> + Opts#{'SIP-Servers-IPv4-Address-List' => V}; +dhcpv4_resp_opts(_K, _V, Opts) -> + Opts. + +dhcpv4_resp_opts(Opts) -> + maps:fold(fun dhcpv4_resp_opts/3, #{}, Opts). diff --git a/src/ergw_dhcp_pool_reg.erl b/src/ergw_dhcp_pool_reg.erl new file mode 100644 index 00000000..a5790435 --- /dev/null +++ b/src/ergw_dhcp_pool_reg.erl @@ -0,0 +1,85 @@ +%% Copyright 2020, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(ergw_dhcp_pool_reg). + +-behaviour(regine_server). + +%% API +-export([start_link/0]). +-export([register/1, lookup/1]). +-export([all/0]). + +-ignore_xref([start_link/0, all/0]). + +%% regine_server callbacks +-export([init/1, handle_register/4, handle_unregister/3, handle_pid_remove/3, + handle_death/3, handle_call/3, terminate/2]). + +%% -------------------------------------------------------------------- +%% Include files +%% -------------------------------------------------------------------- +-include("include/ergw.hrl"). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% API +%%%=================================================================== + +start_link() -> + regine_server:start_link({local, ?SERVER}, ?MODULE, []). + +register(Pool) -> + regine_server:register(?SERVER, self(), Pool, undefined). + +lookup(Pool) when is_binary(Pool) -> + regine_server:call(?SERVER, {lookup, Pool}). + +all() -> + regine_server:call(?SERVER, all). + +%%%=================================================================== +%%% regine callbacks +%%%=================================================================== + +init([]) -> + {ok, #{}}. + +handle_register(_Pid, Pool, _Value, State) + when is_map_key(Pool, State) -> + {error, duplicate}; +handle_register(Pid, Pool, _Value, State) -> + {ok, [Pool], maps:put(Pool, Pid, State)}. + +handle_unregister(Pool, _Value, State) + when is_map_key(Pool, State) -> + [[maps:get(Pool, State)], maps:remove(Pool, State)]; +handle_unregister(_Pool, _Value, State) -> + {[], State}. + +handle_pid_remove(_Pid, Pools, State) -> + maps:without(Pools, State). + +handle_death(_Pid, _Reason, State) -> + State. + +handle_call({lookup, Pool}, _From, State) -> + maps:get(Pool, State, undefined); + +handle_call({match, Pools}, _From, State) -> + maps:to_list(maps:with(Pools, State)); + +handle_call(all, _From, State) -> + maps:to_list(State). + +terminate(_Reason, _State) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/ergw_dhcp_pool_sup.erl b/src/ergw_dhcp_pool_sup.erl new file mode 100644 index 00000000..74f638eb --- /dev/null +++ b/src/ergw_dhcp_pool_sup.erl @@ -0,0 +1,44 @@ +%% Copyright 2020, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(ergw_dhcp_pool_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0, start_ip_pool/2]). + +-ignore_xref([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +%% =================================================================== +%% API functions +%% =================================================================== + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +start_ip_pool(Pool, Opts) -> + supervisor:start_child(?SERVER, [Pool, Opts, []]). + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init([]) -> + ChildSpec = + #{id => dhcp_pool, + start => {ergw_dhcp_pool, start_link, []}, + restart => temporary, + shutdown => 1000, + type => worker, + modules => [ergw_dhcp_pool]}, + {ok, {{simple_one_for_one, 5, 10}, [ChildSpec]}}. diff --git a/src/ergw_dhcp_socket.erl b/src/ergw_dhcp_socket.erl new file mode 100644 index 00000000..5e80a5b4 --- /dev/null +++ b/src/ergw_dhcp_socket.erl @@ -0,0 +1,465 @@ +%% Copyright 2020, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(ergw_dhcp_socket). + +-behavior(gen_server). + +-compile({parse_transform, cut}). + +%% API +-export([validate_options/2, start_link/1]). +-export([send_request/3, wait_response/2]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("dhcp/include/dhcp.hrl"). +-include("include/ergw.hrl"). + +-type xid() :: 0 .. 16#ffffffff. + +-record(state, { + name :: term(), + ip :: inet:ip_address(), + socket :: socket:socket(), + + xid :: xid(), + pending :: gb_trees:tree(xid(), term()) + }). + +-define(SERVER, ?MODULE). +-define(DHCP_SERVER_PORT, 67). +-define(TIMEOUT, 10 * 1000). + +%%==================================================================== +%% API +%%==================================================================== + +start_link(Opts) -> + gen_server:start_link(?MODULE, Opts, []). + +%% send_request(Request, Timeout) -> +%% gen_server:send_request(?SERVER, {send, Request, Timeout}). + +%% wait_response(ReqId) -> +%% gen_server:wait_response(ReqId). + +send_request(Pid, Srv, DHCP) when is_pid(Pid) -> + Mref = monitor(process, Pid), + From = {self(), Mref}, + gen_server:cast(Pid, {request, From, Srv, DHCP}), + Mref; +send_request(Socket, Srv, DHCP) -> + case ergw_socket_reg:lookup(dhcp, Socket) of + Pid when is_pid(Pid) -> + send_request(Pid, Srv, DHCP); + _ -> + Mref = make_ref(), + self() ! {'DOWN', Mref, process, Socket, not_found}, + Mref + end. + +wait_response(_ReqId, Timeout) + when Timeout =< 0 -> + timeout; + +wait_response(Mref, Timeout) when is_integer(Timeout) -> + receive + {'DOWN', Mref, _, _, Reason} -> + {error, Reason}; + {Mref, Reply} -> + {ok, Reply}; + Other -> + {error, Other} + after Timeout -> + {error, timeout} + end; + +wait_response(ReqId, {abs, Timeout}) -> + wait_response(ReqId, Timeout - erlang:monotonic_time(millisecond)). + + +%% sockname() -> +%% gen_server:call(?SERVER, sockname). + +%%%=================================================================== +%%% Options Validation +%%%=================================================================== + +-define(SOCKET_OPTS, [netdev, netns, freebind, reuseaddr, rcvbuf]). +-define(SocketDefaults, [{ip, invalid}, {port, dhcp}]). + +validate_options(Name, Values) -> + ergw_config:validate_options(fun validate_option/2, Values, + [{name, Name}|?SocketDefaults], map). + +validate_option(type, dhcp = Value) -> + Value; +validate_option(name, Value) when is_atom(Value) -> + Value; +validate_option(ip, Value) + when is_tuple(Value) andalso + (tuple_size(Value) == 4 orelse tuple_size(Value) == 8) -> + Value; +validate_option(port, Value) when Value =:= dhcp; Value =:= random -> + Value; +validate_option(netdev, Value) when is_list(Value) -> + Value; +validate_option(netdev, Value) when is_binary(Value) -> + unicode:characters_to_list(Value, latin1); +validate_option(netns, Value) when is_list(Value) -> + Value; +validate_option(netns, Value) when is_binary(Value) -> + unicode:characters_to_list(Value, latin1); +validate_option(freebind, Value) when is_boolean(Value) -> + Value; +validate_option(reuseaddr, Value) when is_boolean(Value) -> + Value; +validate_option(rcvbuf, Value) + when is_integer(Value) andalso Value > 0 -> + Value; +validate_option(socket, Value) + when is_atom(Value) -> + Value; +validate_option(Opt, Value) -> + throw({error, {options, {Opt, Value}}}). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init(#{name := Name, ip := IP, port := PortOpt} = Opts) -> + process_flag(trap_exit, true), + + SocketOpts = maps:with(?SOCKET_OPTS, Opts), + {ok, Socket} = make_dhcp_socket(IP, PortOpt, SocketOpts), + + ergw_socket_reg:register('dhcp', Name, self()), + State = #state{ + name = Name, + ip = IP, + socket = Socket, + + xid = rand:uniform(16#ffffffff), + pending = gb_trees:empty() + }, + select(Socket), + {ok, State}. + +%% handle_call(sockname, _From, #state{socket = Socket} = State) -> +%% {reply, socket:sockname(Socket), State}; + +handle_call(Request, _From, State) -> + ?LOG(error, "handle_call: unknown ~p", [Request]), + {reply, ok, State}. + +handle_cast({request, From, Srv, #dhcp{} = DHCP}, #state{xid = XId} = State) -> + Req = DHCP#dhcp{xid = XId}, + %% message_counter(tx, State, DHCP), + send_request(Srv, Req, From, State#state{xid = (XId + 1) rem 16#100000000}); + +handle_cast(Msg, State) -> + ?LOG(error, "handle_cast: unknown ~p", [Msg]), + {noreply, State}. + +handle_info(Info = {timeout, _TRef, {request, #dhcp{xid = XId}}}, State) -> + ?LOG(debug, "handle_info: ~p", [Info]), + {noreply, remove_request(XId, State)}; + +handle_info({'$socket', Socket, select, Info}, #state{socket = Socket} = State) -> + handle_input(Socket, Info, State); + +handle_info({'$socket', Socket, abort, Info}, #state{socket = Socket} = State) -> + handle_input(Socket, Info, State); + +handle_info(Info, State) -> + ?LOG(error, "handle_info: unknown ~p, ~p", [Info, State]), + {noreply, State}. + +terminate(_Reason, #state{socket = Socket} = _State) -> + socket:close(Socket), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Socket functions +%%%=================================================================== + +family({_,_,_,_}) -> inet; +family({_,_,_,_,_,_,_,_}) -> inet6. + +port_opt(dhcp) -> ?DHCP_SERVER_PORT; +port_opt(_) -> 0. + +make_dhcp_socket(IP, Port, #{netns := NetNs} = Opts) + when is_list(NetNs) -> + {ok, Socket} = socket:open(family(IP), dgram, udp, #{netns => NetNs}), + bind_dhcp_socket(Socket, IP, Port, Opts); +make_dhcp_socket(IP, Port, Opts) -> + {ok, Socket} = socket:open(family(IP), dgram, udp), + bind_dhcp_socket(Socket, IP, Port, Opts). + +bind_dhcp_socket(Socket, {_,_,_,_} = IP, PortOpt, Opts) -> + ok = socket_ip_freebind(Socket, Opts), + ok = socket_netdev(Socket, Opts), + {ok, _} = socket:bind(Socket, #{family => inet, addr => IP, port => port_opt(PortOpt)}), + ok = socket:setopt(Socket, socket, broadcast, true), + ok = socket:setopt(Socket, ip, recverr, true), + ok = socket:setopt(Socket, ip, mtu_discover, dont), + maps:fold(fun(K, V, ok) -> ok = socket_setopts(Socket, K, V) end, ok, Opts), + {ok, Socket}; + +bind_dhcp_socket(Socket, {_,_,_,_,_,_,_,_} = IP, PortOpt, Opts) -> + ok = socket:setopt(Socket, ipv6, v6only, true), + ok = socket_netdev(Socket, Opts), + {ok, _} = socket:bind(Socket, #{family => inet6, addr => IP, port => port_opt(PortOpt)}), + ok = socket:setopt(Socket, socket, broadcast, true), + ok = socket:setopt(Socket, ipv6, recverr, true), + ok = socket:setopt(Socket, ipv6, mtu_discover, dont), + maps:fold(fun(K, V, ok) -> ok = socket_setopts(Socket, K, V) end, ok, Opts), + {ok, Socket}. + +socket_ip_freebind(Socket, #{freebind := true}) -> + socket:setopt(Socket, ip, freebind, true); +socket_ip_freebind(_, _) -> + ok. + +socket_netdev(Socket, #{netdev := Device}) -> + socket:setopt(Socket, socket, bindtodevice, Device); +socket_netdev(_, _) -> + ok. + +socket_setopts(Socket, rcvbuf, Size) when is_integer(Size) -> + case socket:setopt(Socket, socket, rcvbufforce, Size) of + ok -> ok; + _ -> socket:setopt(Socket, socket, rcvbuf, Size) + end; +socket_setopts(Socket, reuseaddr, true) -> + ok = socket:setopt(Socket, socket, reuseaddr, true); +socket_setopts(_Socket, _, _) -> + ok. + +select(Socket) -> + self() ! {'$socket', Socket, select, undefined}. + +handle_input(Socket, _Info, State0) -> + case socket:recvfrom(Socket, 0, [], nowait) of + {error, _} -> + State = handle_err_input(Socket, State0), + select(Socket), + {noreply, State}; + + {ok, {Source, Data}} -> + State = handle_message(Source, Data, State0), + select(Socket), + {noreply, State}; + + {select, _SelectInfo} -> + {noreply, State0} + end. + +-define(IP_RECVERR, 11). +-define(IPV6_RECVERR, 25). +-define(SO_EE_ORIGIN_LOCAL, 1). +-define(SO_EE_ORIGIN_ICMP, 2). +-define(SO_EE_ORIGIN_ICMP6, 3). +-define(SO_EE_ORIGIN_TXSTATUS, 4). +-define(ICMP_DEST_UNREACH, 3). %% Destination Unreachable +-define(ICMP_HOST_UNREACH, 1). %% Host Unreachable +-define(ICMP_PROT_UNREACH, 2). %% Protocol Unreachable +-define(ICMP_PORT_UNREACH, 3). %% Port Unreachable +-define(ICMP6_DST_UNREACH, 1). +-define(ICMP6_DST_UNREACH_ADDR, 3). %% address unreachable +-define(ICMP6_DST_UNREACH_NOPORT,4). %% bad port + +handle_dest_unreach(_IP, [Data|_], #state{name = _Name} = State) when is_binary(Data) -> + %% ergw_prometheus:dhcp(tx, Name, IP, unreachable), + try dhcp_lib:decode(Data, map) of + #dhcp{xid = XId} -> + case lookup_request(XId, State) of + none -> + ok; + {value, From} -> + reply(From, {error, unreachable}), + remove_request(XId, State) + end, + State + catch + Class:Error:Stack -> + ?LOG(debug, "HandleSocketError: ~p:~p @ ~p", [Class, Error, Stack]), + State + end; +handle_dest_unreach(_, _, State) -> + State. + +handle_socket_error(#{level := ip, type := ?IP_RECVERR, + data := <<_ErrNo:32/native-integer, + Origin:8, Type:8, Code:8, _Pad:8, + _Info:32/native-integer, _Data:32/native-integer, + _/binary>>}, + IP, _Port, IOV, State) + when Origin == ?SO_EE_ORIGIN_ICMP, Type == ?ICMP_DEST_UNREACH, + (Code == ?ICMP_HOST_UNREACH orelse Code == ?ICMP_PORT_UNREACH) -> + ?LOG(debug, "ICMP indication for ~s: ~p", [inet:ntoa(IP), Code]), + handle_dest_unreach(IP, IOV, State); + +handle_socket_error(#{level := ip, type := recverr, + data := #{origin := icmp, type := dest_unreach, code := Code}}, + IP, _Port, IOV, State) + when Code == host_unreach; + Code == port_unreach -> + ?LOG(debug, "ICMP indication for ~s: ~p", [inet:ntoa(IP), Code]), + handle_dest_unreach(IP, IOV, State); + +handle_socket_error(#{level := ipv6, type := ?IPV6_RECVERR, + data := <<_ErrNo:32/native-integer, + Origin:8, Type:8, Code:8, _Pad:8, + _Info:32/native-integer, _Data:32/native-integer, + _/binary>>}, + IP, _Port, IOV, State) + when Origin == ?SO_EE_ORIGIN_ICMP6, Type == ?ICMP6_DST_UNREACH, + (Code == ?ICMP6_DST_UNREACH_ADDR orelse Code == ?ICMP6_DST_UNREACH_NOPORT) -> + ?LOG(debug, "ICMPv6 indication for ~s: ~p", [inet:ntoa(IP), Code]), + handle_dest_unreach(IP, IOV, State); + +handle_socket_error(#{level := ipv6, type := recverr, + data := #{origin := icmp6, type := dest_unreach, code := Code}}, + IP, _Port, IOV, State) + when Code == addr_unreach; + Code == port_unreach -> + ?LOG(debug, "ICMPv6 indication for ~s: ~p", [inet:ntoa(IP), Code]), + handle_dest_unreach(IP, IOV, State); + +handle_socket_error(Error, IP, _Port, _IOV, State) -> + ?LOG(debug, "got unhandled error info for ~s: ~p", [inet:ntoa(IP), Error]), + State. + +handle_err_input(Socket, State) -> + case socket:recvmsg(Socket, [errqueue], nowait) of + {ok, #{addr := #{addr := IP, port := Port}, iov := IOV, ctrl := Ctrl}} -> + lists:foldl(handle_socket_error(_, IP, Port, IOV, _), State, Ctrl); + + {select, SelectInfo} -> + socket:cancel(Socket, SelectInfo), + State; + + Other -> + ?LOG(error, "got unhandled error input: ~p", [Other]), + State + end. + +%%%=================================================================== +%%% Sx Message functions +%%%=================================================================== + +handle_message(#{port := Port, addr := IP} = Source, + Data, #state{name = _Name} = State0) -> + ?LOG(debug, "handle message ~s:~w: ~p", [inet:ntoa(IP), Port, Data]), + try + Msg = dhcp_lib:decode(Data, map), + %% ergw_prometheus:dhcp(rx, Name, IP, Msg), + handle_response(Source, Msg, State0) + catch + Class:Error:Stack -> + ?LOG(debug, "UDP invalid msg: ~p:~p @ ~p", [Class, Error, Stack]), + %% ergw_prometheus:dhcp(rx, Name, IP, 'malformed-message'), + State0 + end. + +handle_response(_Source, #dhcp{xid = XId} = Msg, + #state{name = _Name} = State) -> + case lookup_request(XId, State) of + none -> %% late, drop silently + %% ergw_prometheus:dhcp(rx, Name, IP, Msg, late), + State; + + {value, From} -> + %% ergw_prometheus:dhcp(rx, Name, IP, Msg), + reply(From, Msg), + State + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +reply({Pid, Ref}, Reply) -> + Pid ! {Ref, Reply}. + +send_request(Srv, #dhcp{xid = XId} = Req0, From, State) -> + Req = dhcp_req_opts(Req0, State), + SendTo = dhcp_server(Srv), + Data = dhcp_lib:encode(Req), + case sendto(State, SendTo, Data) of + ok -> + {noreply, start_request(XId, Req, From, State)}; + {error, _} = Error -> + reply(From, Error), + {noreply, State} + end. + +start_request(XId, Req, From, #state{pending = Pending} = State) -> + erlang:start_timer(?TIMEOUT, self(), {request, Req}), + State#state{pending = gb_trees:insert(XId, From, Pending)}. + +lookup_request(Xid, #state{pending = Pending}) -> + gb_trees:lookup(Xid, Pending). + +remove_request(Xid, #state{pending = Pending} = State) -> + State#state{pending = gb_trees:delete_any(Xid, Pending)}. + +sendto(#state{socket = Socket, ip = SrcIP}, DstIP, Data) -> + Dest = #{family => family(SrcIP), + addr => DstIP, + port => ?DHCP_SERVER_PORT}, + socket:sendto(Socket, Data, Dest, nowait). + +dhcp_req_opts(#dhcp{options = Opts} = Req, _State) -> +%% when Port /= ?DHCP_SERVER_PORT -> + AgentOpts = [{?RAI_DHCPV4_RELAY_SOURCE_PORT, ?DHCP_SERVER_PORT} | + dhcp_lib:get_opt(?DHO_DHCP_AGENT_OPTIONS, Opts, [])], + Req#dhcp{options = dhcp_lib:put_opt(?DHO_DHCP_AGENT_OPTIONS, AgentOpts, Opts)}; +dhcp_req_opts(Req, _) -> + Req. + +dhcp_server(broadcast) -> + broadcast; +dhcp_server({0,0,0,0}) -> + broadcast; +dhcp_server(SendTo) -> + SendTo. + +%%%=================================================================== +%%% Metrics collections +%%%=================================================================== + +%% %% message_counter/3 +%% message_counter(Direction, #state{name = Name}, #send_req{address = IP, msg = Msg}) -> +%% %% ergw_prometheus:dhcp(Direction, Name, IP, Msg). + +%% %% message_counter/4 +%% message_counter(Direction, #state{name = Name}, #sx_request{ip = IP}, #pfcp{} = Msg) -> +%% %% ergw_prometheus:dhcp(Direction, Name, IP, Msg); +%% message_counter(Direction, #state{name = Name}, #send_req{address = IP, msg = Msg}, Verdict) +%% when is_atom(Verdict) -> +%% %% ergw_prometheus:dhcp(Direction, Name, IP, Verdict). + +%% %% measure the time it takes our peer to reply to a request +%% measure_response(#state{name = Name}, +%% #send_req{address = IP, msg = Msg, send_ts = SendTS}, ArrivalTS) -> +%% %% ergw_prometheus:dhcp_peer_response(Name, IP, Msg, SendTS - ArrivalTS). + +%% %% measure the time it takes us to generate a response to a request +%% measure_request(#state{name = Name}, +%% #sx_request{type = MsgType, arrival_ts = ArrivalTS}) -> +%% Duration = erlang:monotonic_time() - ArrivalTS, +%% %% ergw_prometheus:dhcp_request_duration(Name, MsgType, Duration). diff --git a/test/dhcp_pool_SUITE.erl b/test/dhcp_pool_SUITE.erl new file mode 100644 index 00000000..e9ce6a6e --- /dev/null +++ b/test/dhcp_pool_SUITE.erl @@ -0,0 +1,235 @@ +%% Copyright 2020, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(dhcp_pool_SUITE). + +-compile([export_all, nowarn_export_all]). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("dhcp/include/dhcp.hrl"). +-include_lib("pfcplib/include/pfcp_packet.hrl"). +-include("../include/ergw.hrl"). +-include("ergw_test_lib.hrl"). +-include("ergw_pgw_test_lib.hrl"). + +-define(TIMEOUT, 2000). + +%%%=================================================================== +%%% Config +%%%=================================================================== + +-define(TEST_CONFIG, + [ + {kernel, + [{logger, + [%% force cth_log to async mode, never block the tests + {handler, cth_log_redirect, cth_log_redirect, + #{level => all, + config => + #{sync_mode_qlen => 10000, + drop_mode_qlen => 10000, + flush_qlen => 10000} + } + } + ]} + ]}, + + {dhcp, + [{server_id, {127,0,0,1}}, + {next_server, {127,0,0,1}}, + {interface, <<"lo">>}, + {authoritative, true}, + {lease_file, "/var/run/dhcp_leases.dets"}, + {subnets, + [{subnet, + {172,20,48,0}, %% Network, + {255,255,255,0}, %% Netmask, + {{172,20,48,5},{172,20,48,100}}, %% Range, + [{1, {255,255,255,0}}, %% Subnet Mask, + {28, {172,20,48,255}}, %% Broadcast Address, + {3, [{172,20,48,1}]}, %% Router, + {15, "wlan"}, %% Domain Name, + {6, [{172,20,48,1}]}, %% Domain Name Server, + {51, 3600}]} %% Address Lease Time, + ]} + ]}, + + + {ergw, [{'$setup_vars', + [{"ORIGIN", {value, "epc.mnc001.mcc001.3gppnetwork.org"}}]}, + {sockets, + [{'cp-socket', + [{type, 'gtp-u'}, + {vrf, cp}, + {ip, ?MUST_BE_UPDATED}, + {reuseaddr, true} + ]}, + {'irx-socket', + [{type, 'gtp-c'}, + {vrf, irx}, + {ip, ?MUST_BE_UPDATED}, + {reuseaddr, true} + ]}, + + {sx, [{type, 'pfcp'}, + {node, 'ergw'}, + {name, 'ergw'}, + {socket, 'cp-socket'}, + {ip, ?MUST_BE_UPDATED}, + {reuseaddr, true} + ]}, + + {'dhcp-v4', + [{type, dhcp}, + %%{ip, ?MUST_BE_UPDATED}, + {ip, {127,100,0,1}}, + {port, random}, + {reuseaddr, true} + ]} + ]}, + + {ip_pools, + [{'pool-A', [{handler, ergw_dhcp_pool}, + {ipv4, [{socket, 'dhcp-v4'}, + {id, {172,20,48,1}}, + {servers, [broadcast]}]} + ]} + ]}, + + {handlers, + [{gn, [{handler, pgw_s5s8}, + {sockets, ['irx-socket']}, + {node_selection, [default]} + ]}, + {s5s8, [{handler, pgw_s5s8}, + {sockets, ['irx-socket']}, + {node_selection, [default]} + ]} + ]}, + + {apns, + [{'_', [{vrf, sgi}, {ip_pools, ['pool-A']}]}]}, + + {nodes, + [{default, + [{vrfs, + [{cp, [{features, ['CP-Function']}]}, + {irx, [{features, ['Access']}]}, + {sgi, [{features, ['SGi-LAN']}]} + ]}, + {ip_pools, ['pool-A']} + ]} + ]} + ]}, + + {ergw_aaa, + [ + {handlers, + [{ergw_aaa_static, + [{'NAS-Identifier', <<"NAS-Identifier">>}, + {'Node-Id', <<"PGW-001">>}, + {'Charging-Rule-Base-Name', <<"m2m0001">>}, + {'Acct-Interim-Interval', 600} + ]} + ]}, + {services, + [{'Default', + [{handler, 'ergw_aaa_static'}]}]} + ]} + ]). + +-define(CONFIG_UPDATE, + [{[sockets, 'cp-socket', ip], localhost}, + {[sockets, 'irx-socket', ip], test_gsn}, + {[sockets, sx, ip], localhost} +%% , +%% {[dhcp_socket, ip], localhost} + ]). + +%%%=================================================================== +%%% Setup +%%%=================================================================== + +suite() -> + [{timetrap,{seconds,30}}]. + +dhcp_init_per_suite(Config) -> + {_, AppCfg} = lists:keyfind(app_cfg, 1, Config), %% let it crash if undefined + + [application:load(App) || App <- [cowboy, ergw, ergw_aaa, dhcp]], + load_config(AppCfg), + {ok, _} = application:ensure_all_started(dhcp), + {ok, _} = application:ensure_all_started(ergw), + Config. + +dhcp_end_per_suite(_Config) -> + [application:stop(App) || App <- [ranch, cowboy, ergw, ergw_aaa, dhcp]], + ok. + +init_per_suite(Config) -> + logger:set_primary_config(#{level => debug}), + [{app_cfg, ?TEST_CONFIG} | Config]. + +end_per_suite(_Config) -> + ok. + +init_per_group(ipv6, Config0) -> + case ergw_test_lib:has_ipv6_test_config() of + true -> + Config = update_app_config(ipv6, ?CONFIG_UPDATE, Config0), + dhcp_init_per_suite(Config); + _ -> + {skip, "IPv6 test IPs not configured"} + end; +init_per_group(ipv4, Config0) -> + Config = update_app_config(ipv4, ?CONFIG_UPDATE, Config0), + dhcp_init_per_suite(Config). + +end_per_group(Group, Config) + when Group == ipv4; Group == ipv6 -> + ok = dhcp_end_per_suite(Config). + +groups() -> + [{ipv4, [], [dhcpv4]}]. + +all() -> + [{group, ipv4}]. + +%%%=================================================================== +%%% Tests +%%%=================================================================== + +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(_, Config) -> + Config. + +%%-------------------------------------------------------------------- +dhcpv4() -> + [{doc, "Test simple dhcpv4 requests"}]. +dhcpv4(_Config) -> + ClientId = <<"aaaaa">>, + Pool = <<"pool-A">>, + IP = ipv4, + PrefixLen = 32, + Opts = #{'MS-Primary-DNS-Server' => true, + 'MS-Secondary-DNS-Server' => true}, + + ReqId = ergw_ip_pool:send_request(ClientId, [{Pool, IP, PrefixLen, Opts}]), + [AllocInfo] = ergw_ip_pool:wait_response(ReqId), + ?match({ergw_dhcp_pool, _, {{_,_,_,_}, 32}, _, #{'MS-Primary-DNS-Server' := {_,_,_,_}}}, + AllocInfo), + + ergw_ip_pool:release([AllocInfo]), + ct:sleep(100), + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== From 9f6528c727834cf0d41ec6122e039486c8d3aa17 Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Tue, 17 Nov 2020 10:36:37 +0100 Subject: [PATCH 4/8] implement IP lease expiry logic for DHCPv4 --- src/ergw_dhcp_pool.erl | 105 ++++++++++++++++++++++++++++++++++++++- src/ergw_gsn_lib.erl | 23 ++++++++- src/ergw_ip_pool.erl | 27 ++++++---- src/ergw_local_pool.erl | 8 ++- src/gtp_context.erl | 28 +++++++++++ src/pgw_s5s8.erl | 3 +- test/dhcp_pool_SUITE.erl | 29 +++++++++-- test/pgw_SUITE.erl | 3 +- 8 files changed, 208 insertions(+), 18 deletions(-) diff --git a/src/ergw_dhcp_pool.erl b/src/ergw_dhcp_pool.erl index f210eafe..cda3ddf6 100644 --- a/src/ergw_dhcp_pool.erl +++ b/src/ergw_dhcp_pool.erl @@ -13,7 +13,8 @@ -compile([{parse_transform, cut}]). %% API --export([start_ip_pool/2, send_pool_request/2, wait_pool_response/1, release/1, ip/1, opts/1]). +-export([start_ip_pool/2, send_pool_request/2, wait_pool_response/1, release/1, + ip/1, opts/1, timeouts/1, handle_event/2]). -export([start_link/3, start_link/4]). -export([validate_options/1]). @@ -116,6 +117,13 @@ wait_response(Mref, Timeout) ip({?MODULE, _, IP, _, _}) -> IP. opts({?MODULE, _, _, _, Opts}) -> Opts. +timeouts({?MODULE, _, _, _, Opts}) -> + Keys = [renewal, rebinding, lease], + lists:foldl(fun(K, M) -> M#{K => maps:get(K, Opts, infinity)} end, #{}, Keys). + +handle_event({?MODULE, Server, _, _, _} = AI, Ev) -> + gen_server:call(Server, {handle_event, AI, Ev}). + %%==================================================================== %%% Options Validation %%%=================================================================== @@ -181,6 +189,18 @@ handle_call({get, _ClientId, IP, PrefixLen, _ReqOpts}, _From, State) -> Error = {unsupported, {IP, PrefixLen}}, {reply, {error, Error}, State}; +handle_call({handle_event, {_, _, {IP, PrefixLen}, {Srv, ClientId}, Opts}, renewal}, + From, State) -> + dhcpv4_renew(ClientId, IP, PrefixLen, Opts, Srv, From, State); + +handle_call({handle_event, {_, _, {IP, PrefixLen}, {_Srv, ClientId}, Opts}, rebinding}, + From, State) -> + dhcpv4_rebind(ClientId, IP, PrefixLen, Opts, From, State); + +handle_call({handle_event, {_, _, IP, SrvId, Opts}, Ev}, _From, State) -> + ?LOG(error, "unhandled DHCP event: ~p, (~p, ~p, ~p)", [Ev, IP, SrvId, Opts]), + {reply, ok, State}; + handle_call(Request, _From, State) -> ?LOG(warning, "handle_call: ~p", [Request]), {reply, error, State}. @@ -241,6 +261,16 @@ dhcpv4_init(ClientId, IP, PrefixLen, ReqOpts, From, #state{ipv4 = Pool} = State) ReqF = fun() -> dhcpv4_init_f(ClientId, IP, PrefixLen, ReqOpts, Pool) end, dhcpv4_spawn(ReqF, From, State). +%% dhcpv4_renew/6 +dhcpv4_renew(ClientId, IP, PrefixLen, Opts, Srv, From, #state{ipv4 = Pool} = State) -> + ReqF = fun() -> dhcpv4_renew_f(ClientId, IP, PrefixLen, Opts, Srv, Pool) end, + dhcpv4_spawn(ReqF, From, State). + +%% dhcpv4_rebind/6 +dhcpv4_rebind(ClientId, IP, PrefixLen, Opts, From, #state{ipv4 = Pool} = State) -> + ReqF = fun() -> dhcpv4_rebind_f(ClientId, IP, PrefixLen, Opts, Pool) end, + dhcpv4_spawn(ReqF, From, State). + dhcpv4_init_f(ClientId, ReqIP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) -> Srv = choose_server(Srvs), Opts = dhcpv4_opts(ClientId, ReqIP, PrefixLen, ReqOpts), @@ -251,6 +281,15 @@ dhcpv4_init_f(ClientId, ReqIP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) Other end. +dhcpv4_renew_f(ClientId, IP, PrefixLen, ReqOpts, Srv, Pool) -> + Opts = dhcpv4_opts(ClientId, IP, PrefixLen, ReqOpts), + dhcpv4_renew(Pool, Srv, ClientId, IP, Opts). + +dhcpv4_rebind_f(ClientId, IP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) -> + Srv = choose_server(Srvs), + Opts = dhcpv4_opts(ClientId, IP, PrefixLen, ReqOpts), + dhcpv4_rebind(Pool, Srv, ClientId, IP, Opts). + dhcpv4_discover(Pool, Srv, Opts) -> DHCP = #dhcp{ op = ?BOOTREQUEST, @@ -300,6 +339,51 @@ dhcpv4_request(Pool, ClientId, Opts, #dhcp{siaddr = SiAddr0} = Offer) -> Error end. +dhcpv4_renew(Pool, Srv, ClientId, IP, Opts) -> + DHCP = #dhcp{ + op = ?BOOTREQUEST, + ciaddr = IP, + yiaddr = {0, 0, 0, 0}, + siaddr = {0, 0, 0, 0}, + options = Opts#{?DHO_DHCP_MESSAGE_TYPE => ?DHCPREQUEST} + }, + ReqId = dhcpv4_send_request(Pool, Srv, DHCP), + + ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + case dhcpv4_answer(ReqId, {abs, ReqTimeOut}) of + #dhcp{yiaddr = IP, + options = #{?DHO_DHCP_MESSAGE_TYPE := ?DHCPACK} = RespOpts} = Answer -> + SrvId = choose_next(Answer, Srv), + {ok, {IP, 32}, {SrvId, ClientId}, dhcpv4_resp_opts(RespOpts)}; + #dhcp{} -> + {error, failed}; + {error, _} = Error -> + Error + end. + +%% TBD: in REBIND we should broadcast the request (or send to all configured servers) +dhcpv4_rebind(Pool, Srv, ClientId, IP, Opts) -> + DHCP = #dhcp{ + op = ?BOOTREQUEST, + ciaddr = IP, + yiaddr = {0, 0, 0, 0}, + siaddr = {0, 0, 0, 0}, + options = Opts#{?DHO_DHCP_MESSAGE_TYPE => ?DHCPREQUEST} + }, + ReqId = dhcpv4_send_request(Pool, Srv, DHCP), + + ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + case dhcpv4_answer(ReqId, {abs, ReqTimeOut}) of + #dhcp{yiaddr = IP, + options = #{?DHO_DHCP_MESSAGE_TYPE := ?DHCPACK} = RespOpts} = Answer -> + SrvId = choose_next(Answer, Srv), + {ok, {IP, 32}, {SrvId, ClientId}, dhcpv4_resp_opts(RespOpts)}; + #dhcp{} -> + {error, failed}; + {error, _} = Error -> + Error + end. + dhcpv4_release(IP, {Srv, ClientId}, #state{ipv4 = Pool}) -> Opts = #{?DHO_DHCP_MESSAGE_TYPE => ?DHCPRELEASE, ?DHO_DHCP_SERVER_IDENTIFIER => Srv, @@ -367,6 +451,14 @@ client_id(ClientId) when is_integer(ClientId) -> client_id(ClientId) when is_list(ClientId) -> iolist_to_binary(ClientId). +set_time(K, V, #{'Base-Time' := Now} = Opts) -> + Opts#{K => Now + V}. + +maybe_set_time(K, V, Opts) when not is_map_key(K, Opts) -> + set_time(K, V, Opts); +maybe_set_time(_, _, Opts) -> + Opts. + dhcpv4_resp_opts(?DHO_DOMAIN_NAME_SERVERS, [Prim, Sec | _], Opts) -> Opts#{'MS-Primary-DNS-Server' => Prim, 'MS-Secondary-DNS-Server' => Sec}; dhcpv4_resp_opts(?DHO_DOMAIN_NAME_SERVERS, [Prim | _], Opts) -> @@ -377,8 +469,17 @@ dhcpv4_resp_opts(?DHO_NETBIOS_NAME_SERVERS, [Prim, Sec | _], Opts) -> Opts#{'MS-Primary-NBNS-Server' => Prim}; dhcpv4_resp_opts(?DHO_SIP_SERVERS, V, Opts) -> Opts#{'SIP-Servers-IPv4-Address-List' => V}; +dhcpv4_resp_opts(?DHO_DHCP_LEASE_TIME, V, Opts0) -> + Opts1 = set_time(lease, V, Opts0), + Opts2 = maybe_set_time(renewal, round(V * 0.5), Opts1), + _Opts = maybe_set_time(rebinding, round(V * 0.875), Opts2); +dhcpv4_resp_opts(?DHO_DHCP_RENEWAL_TIME, V, Opts) -> + set_time(renewal, V, Opts); +dhcpv4_resp_opts(?DHO_DHCP_REBINDING_TIME, V, Opts) -> + set_time(rebinding, V, Opts); dhcpv4_resp_opts(_K, _V, Opts) -> Opts. dhcpv4_resp_opts(Opts) -> - maps:fold(fun dhcpv4_resp_opts/3, #{}, Opts). + Now = erlang:system_time(second), + maps:fold(fun dhcpv4_resp_opts/3, #{'Base-Time' => Now}, Opts). diff --git a/src/ergw_gsn_lib.erl b/src/ergw_gsn_lib.erl index 833b610c..264bfa49 100644 --- a/src/ergw_gsn_lib.erl +++ b/src/ergw_gsn_lib.erl @@ -39,6 +39,7 @@ reassign_tunnel_teid/1, assign_local_data_teid/5 ]). +-export([context_timeouts/1]). -include_lib("kernel/include/logger.hrl"). -include_lib("parse_trans/include/exprecs.hrl"). @@ -49,7 +50,7 @@ -include_lib("ergw_aaa/include/diameter_3gpp_ts32_299.hrl"). -include("include/ergw.hrl"). --export_records([context, tdf_ctx, tunnel, bearer, fq_teid]). +-export_records([context, tdf_ctx, tunnel, bearer, fq_teid, ue_ip]). -define(SECONDS_PER_DAY, 86400). -define(DAYS_FROM_0_TO_1970, 719528). @@ -960,3 +961,23 @@ assign_local_data_teid_5(_Key, #pfcp_ctx{ FqTEID = #fq_teid{ip = ergw_inet:to_ip(IP), teid = DataTEI}, return(Bearer#bearer{vrf = VRF, local = FqTEID}) ]). + +%%%=================================================================== +%%% Timeout helpers +%%%=================================================================== + +'#tolist-'(Tuple) -> + [Record|Fields] = tuple_to_list(Tuple), + lists:zip('#info-'(Record), Fields). + +context_ip_timeouts(Id, Key, Timeout, Actions) -> + [{{timeout, {ip, Id, Key}}, Timeout, Key, {abs, true}} | Actions]. + +context_ip_timeouts(_Id, undefined, Actions) -> + Actions; +context_ip_timeouts(Id, AI, Actions) -> + maps:fold(context_ip_timeouts(Id, _, _, _), Actions, ergw_ip_pool:timeouts(AI)). + +context_timeouts(#context{ms_ip = MsIP}) -> + lists:foldl( + fun({Id, AI}, Acc) -> context_ip_timeouts(Id, AI, Acc) end, [], '#tolist-'(MsIP)). diff --git a/src/ergw_ip_pool.erl b/src/ergw_ip_pool.erl index 281a57b2..77641caa 100644 --- a/src/ergw_ip_pool.erl +++ b/src/ergw_ip_pool.erl @@ -9,7 +9,7 @@ %% API -export([start_ip_pool/2, send_request/2, wait_response/1, release/1, - addr/1, ip/1, opts/1]). + addr/1, ip/1, opts/1, timeouts/1, handle_event/2]). -export([static_ip/2]). -export([validate_options/1, validate_name/2]). @@ -31,7 +31,9 @@ -callback wait_pool_response(ReqId :: term()) -> Result :: term(). -callback ip(AllocInfo :: tuple()) -> Result :: term(). -callback opts(AllocInfo :: tuple()) -> Result :: term(). +-callback timeouts(AllocInfo :: tuple()) -> Result :: term(). -callback release(AllocInfo :: tuple()) -> Result :: term(). +-callback handle_event(AllocInfo :: tuple(), Event :: term()) -> Result :: term(). %%==================================================================== %% API @@ -56,10 +58,16 @@ addr(AllocInfo) -> end. ip(AllocInfo) -> - alloc_info(AllocInfo, ip). + alloc_info(AllocInfo, ip, []). opts(AllocInfo) -> - alloc_info(AllocInfo, opts). + alloc_info(AllocInfo, opts, []). + +timeouts(AllocInfo) -> + alloc_info(AllocInfo, timeouts, []). + +handle_event(AllocInfo, Ev) -> + alloc_info(AllocInfo, handle_event, [Ev]). static_ip(IP, PrefixLen) -> {'$static', {IP, PrefixLen}}. @@ -92,16 +100,17 @@ validate_name(Opt, Name) -> %%% Internal functions %%%=================================================================== -alloc_info(Info, F) when element(1, Info) =:= '$static' -> +alloc_info(Info, F, _) when element(1, Info) =:= '$static' -> static_ip_info(F, Info); -alloc_info(Tuple, F) when is_tuple(Tuple) -> - apply(element(1, Tuple), F, [Tuple]); -alloc_info(_, _) -> +alloc_info(Tuple, F, A) when is_tuple(Tuple) -> + apply(element(1, Tuple), F, [Tuple | A]); +alloc_info(_, _, _) -> undefined. static_ip_info(ip, {_, Addr}) -> Addr; static_ip_info(opts, _) -> #{}; -static_ip_info(release, _) -> ok. +static_ip_info(release, _) -> ok; +static_ip_info(timeouts, _) -> {infinity, infinity, infinity}. with_pool(Pool, Fun) -> case application:get_env(ergw, ip_pools) of @@ -124,4 +133,4 @@ wait_pool_response({Handler, ReqId}) -> Handler:wait_pool_response(ReqId). pool_release(AI) -> - alloc_info(AI, release). + alloc_info(AI, release, []). diff --git a/src/ergw_local_pool.erl b/src/ergw_local_pool.erl index f0f677ec..1d3d6508 100644 --- a/src/ergw_local_pool.erl +++ b/src/ergw_local_pool.erl @@ -11,7 +11,8 @@ -behavior(ergw_ip_pool). %% API --export([start_ip_pool/2, send_pool_request/2, wait_pool_response/1, release/1, ip/1, opts/1]). +-export([start_ip_pool/2, send_pool_request/2, wait_pool_response/1, release/1, + ip/1, opts/1, timeouts/1, handle_event/2]). -export([start_link/3, start_link/4]). -export([validate_options/1]). @@ -114,6 +115,11 @@ wait_response(Mref, Timeout) ip({?MODULE, _, IP, _, _}) -> IP. opts({?MODULE, _, _, _, Opts}) -> Opts. +timeouts({?MODULE, _, _, _, _}) -> + #{}. + +handle_event(_, _) -> + ok. %%==================================================================== %%% Options Validation diff --git a/src/gtp_context.erl b/src/gtp_context.erl index 309c7bfc..d7e8a41f 100644 --- a/src/gtp_context.erl +++ b/src/gtp_context.erl @@ -609,6 +609,34 @@ handle_event(info, {'DOWN', _MonitorRef, Type, Pid, _Info}, State, handle_event({timeout, context_idle}, stop_session, State, Data) -> delete_context(undefined, normal, State, Data); +handle_event({timeout, {ip, _, lease}}, _Event, State, Data) -> + delete_context(undefined, normal, State, Data); +handle_event({timeout, {ip, Id, _}}, Event, _State, #{context := #context{ms_ip = MsIP}}) -> + AI = ergw_gsn_lib:'#get-'(Id, MsIP), + Server = self(), + Fun = + fun() -> + Result = (catch ergw_ip_pool:handle_event(AI, Event)), + gen_statem:cast(Server, {ip, Id, AI, Result}) + end, + spawn(Fun), + keep_state_and_data; + +handle_event(cast, {ip, Id, AI, {ok, NewAI}}, connected, + #{context := #context{ms_ip = MsIP} = Context0} = Data) -> + {GetF, SetF} = ergw_gsn_lib:'#lens-'(Id, element(1, MsIP)), + case GetF(MsIP) of + AI -> + Context = Context0#context{ms_ip = SetF(NewAI, MsIP)}, + Actions = ergw_gsn_lib:context_timeouts(Context), + {keep_state, Data#{context => Context}, Actions}; + _ -> + keep_state_and_data + end; + +handle_event(cast, {ip, _, _, _}, _State, _Data) -> + keep_state_and_data; + handle_event(Type, Content, State, #{interface := Interface} = Data) -> ?LOG(debug, "~w: handle_event: (~p, ~p, ~p)", [?MODULE, Type, Content, State]), diff --git a/src/pgw_s5s8.erl b/src/pgw_s5s8.erl index 4b2da3b2..370acb37 100644 --- a/src/pgw_s5s8.erl +++ b/src/pgw_s5s8.erl @@ -212,7 +212,8 @@ handle_request(ReqKey, Data#{context => Context, pfcp => PCtx, pcc => PCC4, left_tunnel => LeftTunnel, bearer => Bearer}, - Actions = context_idle_action([], Context), + Actions0 = ergw_gsn_lib:context_timeouts(Context), + Actions = context_idle_action(Actions0, Context), {next_state, connected, FinalData, Actions}; %% TODO: diff --git a/test/dhcp_pool_SUITE.erl b/test/dhcp_pool_SUITE.erl index e9ce6a6e..3ac9f822 100644 --- a/test/dhcp_pool_SUITE.erl +++ b/test/dhcp_pool_SUITE.erl @@ -55,11 +55,11 @@ {3, [{172,20,48,1}]}, %% Router, {15, "wlan"}, %% Domain Name, {6, [{172,20,48,1}]}, %% Domain Name Server, - {51, 3600}]} %% Address Lease Time, + {51, 3600}, %% Address Lease Time, + {58, 5}]} %% DHCP Renewal Time, ]} ]}, - {ergw, [{'$setup_vars', [{"ORIGIN", {value, "epc.mnc001.mcc001.3gppnetwork.org"}}]}, {sockets, @@ -195,7 +195,7 @@ end_per_group(Group, Config) ok = dhcp_end_per_suite(Config). groups() -> - [{ipv4, [], [dhcpv4]}]. + [{ipv4, [], [dhcpv4, v4_renew]}]. all() -> [{group, ipv4}]. @@ -230,6 +230,29 @@ dhcpv4(_Config) -> ct:sleep(100), ok. +%-------------------------------------------------------------------- +v4_renew() -> + [{doc, "Test simple dhcpv4 requests"}]. +v4_renew(_Config) -> + ClientId = <<"aaaaa">>, + Pool = <<"pool-A">>, + IP = ipv4, + PrefixLen = 32, + Opts = #{'MS-Primary-DNS-Server' => true, + 'MS-Secondary-DNS-Server' => true}, + + ReqId = ergw_ip_pool:send_request(ClientId, [{Pool, IP, PrefixLen, Opts}]), + [AllocInfo] = ergw_ip_pool:wait_response(ReqId), + ?match({ergw_dhcp_pool, _, {{_,_,_,_}, 32}, _, #{'MS-Primary-DNS-Server' := {_,_,_,_}}}, + AllocInfo), + + ergw_ip_pool:handle_event(AllocInfo, renewal), + ct:sleep({seconds, 1}), + + ergw_ip_pool:release([AllocInfo]), + ct:sleep(100), + ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/test/pgw_SUITE.erl b/test/pgw_SUITE.erl index c634a734..0065f1ec 100644 --- a/test/pgw_SUITE.erl +++ b/test/pgw_SUITE.erl @@ -60,7 +60,8 @@ {3, [{172,20,48,1}]}, %% Router, {15, "wlan"}, %% Domain Name, {6, [{172,20,48,1}, {172,20,48,1}]}, %% Domain Name Server, - {51, 3600}]} %% Address Lease Time, + {51, 3600}, %% Address Lease Time, + {58, 5}]} %% DHCP Renewal Time, ]} ]}, From 2bb4d66d741a13600cd09aaf16ba73b1cf3a377b Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Tue, 17 Nov 2020 11:34:49 +0100 Subject: [PATCH 5/8] DHCP v6 pool --- rebar.config | 1 + rebar.lock | 4 +++ src/ergw.app.src | 2 +- src/ergw_dhcp_socket.erl | 16 ++++++--- test/dhcp_pool_SUITE.erl | 74 ++++++++++++++++++++++++++++++++++++---- test/ergw_test_lib.hrl | 5 +-- test/pgw_SUITE.erl | 10 ++++++ 7 files changed, 98 insertions(+), 14 deletions(-) diff --git a/rebar.config b/rebar.config index 060a117a..e93aee69 100644 --- a/rebar.config +++ b/rebar.config @@ -12,6 +12,7 @@ {erlando, {git, "https://github.com/travelping/erlando.git", {tag, "1.0.3"}}}, {netdata, {git, "https://github.com/RoadRunnr/erl_netdata.git", {ref, "cbd6eaf"}}}, {dhcp, {git, "https://github.com/RoadRunnr/dhcp.git", {branch, "feature/modernize"}}}, + {dhcpv6, {git, "https://github.com/travelping/dhcpv6.git", {branch, "master"}}}, {gtplib, {git, "https://github.com/travelping/gtplib.git", {branch, "master"}}}, {pfcplib, {git, "https://github.com/travelping/pfcplib.git", {branch, "master"}}}, {ergw_aaa, {git, "git://github.com/travelping/ergw_aaa", {tag, "3.6.2"}}}, diff --git a/rebar.lock b/rebar.lock index 0718ce7c..577e983e 100644 --- a/rebar.lock +++ b/rebar.lock @@ -6,6 +6,10 @@ {git,"https://github.com/RoadRunnr/dhcp.git", {ref,"31c51ec0014d363f435055e58574e6f725703940"}}, 0}, + {<<"dhcpv6">>, + {git,"https://github.com/travelping/dhcpv6.git", + {ref,"7733e242ffec329576e931acf500f53ecc418679"}}, + 0}, {<<"eradius">>, {git,"https://github.com/travelping/eradius.git", {ref,"7147d879177f3a9ad88f909a12e41e1c565269b0"}}, diff --git a/src/ergw.app.src b/src/ergw.app.src index 55b833ab..6f2f5101 100644 --- a/src/ergw.app.src +++ b/src/ergw.app.src @@ -8,7 +8,7 @@ prometheus, cowboy, prometheus_cowboy, prometheus_diameter_collector, jsx, compiler, os_mon, jobs]}, - {included_applications, [dhcp]}, + {included_applications, [dhcp, dhcpv6]}, {mod, {ergw_app, []}}, {registered, []} ]}. diff --git a/src/ergw_dhcp_socket.erl b/src/ergw_dhcp_socket.erl index 5e80a5b4..ca3533cc 100644 --- a/src/ergw_dhcp_socket.erl +++ b/src/ergw_dhcp_socket.erl @@ -21,6 +21,7 @@ -include_lib("kernel/include/logger.hrl"). -include_lib("dhcp/include/dhcp.hrl"). +-include_lib("dhcpv6/include/dhcpv6.hrl"). -include("include/ergw.hrl"). -type xid() :: 0 .. 16#ffffffff. @@ -361,11 +362,14 @@ handle_err_input(Socket, State) -> %%% Sx Message functions %%%=================================================================== -handle_message(#{port := Port, addr := IP} = Source, +handle_message(#{family := Family, port := Port, addr := IP} = Source, Data, #state{name = _Name} = State0) -> ?LOG(debug, "handle message ~s:~w: ~p", [inet:ntoa(IP), Port, Data]), try - Msg = dhcp_lib:decode(Data, map), + Msg = case Family of + inet -> dhcp_lib:decode(Data, map); + inet6 -> dhcpv6_lib:decode(Data) + end, %% ergw_prometheus:dhcp(rx, Name, IP, Msg), handle_response(Source, Msg, State0) catch @@ -375,8 +379,12 @@ handle_message(#{port := Port, addr := IP} = Source, State0 end. -handle_response(_Source, #dhcp{xid = XId} = Msg, - #state{name = _Name} = State) -> +handle_response(_Source, Msg, #state{name = _Name} = State) -> + XId = case Msg of + #dhcp{xid = Id} -> Id; + #dhcpv6{xid = Id} -> Id + end, + case lookup_request(XId, State) of none -> %% late, drop silently %% ergw_prometheus:dhcp(rx, Name, IP, Msg, late), diff --git a/test/dhcp_pool_SUITE.erl b/test/dhcp_pool_SUITE.erl index 3ac9f822..45243ce7 100644 --- a/test/dhcp_pool_SUITE.erl +++ b/test/dhcp_pool_SUITE.erl @@ -60,6 +60,29 @@ ]} ]}, + {dhcpv6, + [{server_id, ?LOCALHOST_IPv6}, + {next_server, ?LOCALHOST_IPv6}, + {socket, #{netdev => "lo", + ip => ?LOCALHOST_IPv6, + join => {1, [all_dhcp_servers, all_dhcp_relay_agents_and_servers]}}}, + {authoritative, true}, + {lease_file, "/var/run/dhcpv6_leases"}, + {subnets, + [#{subnet => ?IPv6PoolNet, + options => [], + pools => + [#{pool => {?IPv6HostPoolStart, ?IPv6HostPoolEnd}, + options => []}], + pd_pools => + [#{prefix => ?IPv6PoolStart, + prefix_len => 48, + delegated_len => 64, + options => []}] + } + ]} + ]}, + {ergw, [{'$setup_vars', [{"ORIGIN", {value, "epc.mnc001.mcc001.3gppnetwork.org"}}]}, {sockets, @@ -90,6 +113,13 @@ {ip, {127,100,0,1}}, {port, random}, {reuseaddr, true} + ]}, + {'dhcp-v6', + [{type, dhcp}, + %%{ip, ?MUST_BE_UPDATED}, + {ip, ?LOCALHOST_IPv6}, + {port, random}, + {reuseaddr, true} ]} ]}, @@ -97,6 +127,9 @@ [{'pool-A', [{handler, ergw_dhcp_pool}, {ipv4, [{socket, 'dhcp-v4'}, {id, {172,20,48,1}}, + {servers, [broadcast]}]}, + {ipv6, [{socket, 'dhcp-v6'}, + {id, {16#8001, 0, 1, 0, 0, 0, 0, 0}}, {servers, [broadcast]}]} ]} ]}, @@ -158,12 +191,15 @@ suite() -> [{timetrap,{seconds,30}}]. -dhcp_init_per_suite(Config) -> +dhcp_init_per_suite(Group, Config) -> {_, AppCfg} = lists:keyfind(app_cfg, 1, Config), %% let it crash if undefined - [application:load(App) || App <- [cowboy, ergw, ergw_aaa, dhcp]], + [application:load(App) || App <- [cowboy, ergw, ergw_aaa, dhcp, dhcpv6]], load_config(AppCfg), - {ok, _} = application:ensure_all_started(dhcp), + case Group of + ipv4 -> {ok, _} = application:ensure_all_started(dhcp); + ipv6 -> {ok, _} = application:ensure_all_started(dhcpv6) + end, {ok, _} = application:ensure_all_started(ergw), Config. @@ -182,23 +218,25 @@ init_per_group(ipv6, Config0) -> case ergw_test_lib:has_ipv6_test_config() of true -> Config = update_app_config(ipv6, ?CONFIG_UPDATE, Config0), - dhcp_init_per_suite(Config); + dhcp_init_per_suite(ipv6, Config); _ -> {skip, "IPv6 test IPs not configured"} end; init_per_group(ipv4, Config0) -> Config = update_app_config(ipv4, ?CONFIG_UPDATE, Config0), - dhcp_init_per_suite(Config). + dhcp_init_per_suite(ipv4, Config). end_per_group(Group, Config) when Group == ipv4; Group == ipv6 -> ok = dhcp_end_per_suite(Config). groups() -> - [{ipv4, [], [dhcpv4, v4_renew]}]. + [{ipv4, [], [dhcpv4, v4_renew]}, + {ipv6, [], [dhcpv6]}]. all() -> - [{group, ipv4}]. + [{group, ipv4}, + {group, ipv6}]. %%%=================================================================== %%% Tests @@ -253,6 +291,28 @@ v4_renew(_Config) -> ct:sleep(100), ok. +%%-------------------------------------------------------------------- +dhcpv6() -> + [{doc, "Test simple dhcpv6 requests"}]. +dhcpv6(_Config) -> + ct:pal("All: ~p", [ergw_socket_reg:all()]), + inet:i(), + ClientId = <<"aaaaa">>, + Pool = <<"pool-A">>, + IP = ipv6, + PrefixLen = 64, + Opts = #{'MS-Primary-DNS-Server' => true, + 'MS-Secondary-DNS-Server' => true}, + + ReqId = ergw_ip_pool:send_request(ClientId, [{Pool, IP, PrefixLen, Opts}]), + [AllocInfo] = ergw_ip_pool:wait_response(ReqId), + ?match({ergw_dhcp_pool, _, {{_,_,_,_}, 32}, _, #{'MS-Primary-DNS-Server' := {_,_,_,_}}}, + AllocInfo), + + ergw_ip_pool:release([AllocInfo]), + ct:sleep(100), + ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/test/ergw_test_lib.hrl b/test/ergw_test_lib.hrl index 9f364c00..e889e713 100644 --- a/test/ergw_test_lib.hrl +++ b/test/ergw_test_lib.hrl @@ -84,8 +84,9 @@ -define(IPv4PoolEnd, {10, 180, 255, 254}). -define(IPv4StaticIP, {10, 180, 128, 128}). --define(IPv6PoolStart, {16#8001, 0, 1, 0, 0, 0, 0, 0}). --define(IPv6PoolEnd, {16#8001, 0, 1, 16#FFFF, 16#FFFF, 16#FFFF, 16#FFFF, 16#FFFF}). +-define(IPv6PoolNet, {{16#8001, 0, 1, 0, 0, 0, 0, 0}, 32}). +-define(IPv6PoolStart, {16#8001, 0, 1, 0, 0, 0, 0, 1}). +-define(IPv6PoolEnd, {16#8001, 0, 1, 16#FFFF, 16#FFFF, 16#FFFF, 16#FFFF, 16#FFFE}). -define(IPv6StaticIP, {16#8001, 0, 1, 16#0180, 1, 2, 3, 4}). %% for non-standard /128 assigments diff --git a/test/pgw_SUITE.erl b/test/pgw_SUITE.erl index 0065f1ec..23ecbb8c 100644 --- a/test/pgw_SUITE.erl +++ b/test/pgw_SUITE.erl @@ -95,6 +95,13 @@ {ip, {127,100,0,1}}, {port, random}, {reuseaddr, true} + ]}, + {'dhcp-v6', + [{type, dhcp}, + %%{ip, ?MUST_BE_UPDATED}, + {ip, ?LOCALHOST_IPv6}, + {port, random}, + {reuseaddr, true} ]} ]}, @@ -136,6 +143,9 @@ {'pool-DHCP', [{handler, ergw_dhcp_pool}, {ipv4, [{socket, 'dhcp-v4'}, {id, {172,20,48,1}}, + {servers, [broadcast]}]}, + {ipv6, [{socket, 'dhcp-v6'}, + {id, {16#8001, 0, 1, 0, 0, 0, 0, 0}}, {servers, [broadcast]}]} ]} ]}, From 7da09fff118e6563d09266a6003397e7e908a179 Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Tue, 17 Nov 2020 11:39:17 +0100 Subject: [PATCH 6/8] basic DHCPv6 client --- src/ergw_dhcp_pool.erl | 251 ++++++++++++++++++++++++++++++++++++++- src/ergw_dhcp_socket.erl | 135 ++++++++++++++------- test/dhcp_pool_SUITE.erl | 21 ++-- 3 files changed, 348 insertions(+), 59 deletions(-) diff --git a/src/ergw_dhcp_pool.erl b/src/ergw_dhcp_pool.erl index cda3ddf6..384f9696 100644 --- a/src/ergw_dhcp_pool.erl +++ b/src/ergw_dhcp_pool.erl @@ -30,6 +30,8 @@ -include_lib("kernel/include/logger.hrl"). -include_lib("dhcp/include/dhcp.hrl"). +-include_lib("dhcpv6/include/dhcpv6.hrl"). +-include("include/3gpp.hrl"). -define(IS_IPv4(X), (is_tuple(X) andalso tuple_size(X) == 4)). -define(IS_IPv6(X), (is_tuple(X) andalso tuple_size(X) == 8)). @@ -37,6 +39,7 @@ -define(ZERO_IPv4, {0,0,0,0}). -define(ZERO_IPv6, {0,0,0,0,0,0,0,0}). -define(UE_INTERFACE_ID, {0,0,0,0,0,0,0,1}). +-define(IAID, 1). -define(IPv4Opts, ['Framed-Pool', 'MS-Primary-DNS-Server', @@ -80,7 +83,7 @@ wait_pool_response(ReqId) -> Error end. -release({_, Server, {IP, _}, SrvId, _Opts}) -> +release({_, Server, {_, _} = IP, SrvId, _Opts}) -> %% see alloc_reply gen_server:cast(Server, {release, IP, SrvId}). @@ -142,6 +145,8 @@ validate_server(ipv6, IP) when ?IS_IPv6(IP) -> IP; validate_server(_, broadcast = Value) -> Value; +validate_server(ipv6, local = Value) -> + Value; validate_server(Type, Value) -> throw({error, {options, {Type, {servers, Value}}}}). @@ -185,18 +190,35 @@ handle_call({get, ClientId, ipv4, PrefixLen, ReqOpts}, From, State) -> handle_call({get, ClientId, {_,_,_,_} = IP, PrefixLen, ReqOpts}, From, State) -> dhcpv4_init(ClientId, IP, PrefixLen, ReqOpts, From, State); +handle_call({get, ClientId, ipv6, PrefixLen, ReqOpts}, From, State) -> + dhcpv6_init(ClientId, undefined, PrefixLen, ReqOpts, From, State); +handle_call({get, ClientId, {_,_,_,_,_,_,_,_} = IP, PrefixLen, ReqOpts}, From, State) -> + dhcpv6_init(ClientId, IP, PrefixLen, ReqOpts, From, State); + handle_call({get, _ClientId, IP, PrefixLen, _ReqOpts}, _From, State) -> Error = {unsupported, {IP, PrefixLen}}, {reply, {error, Error}, State}; handle_call({handle_event, {_, _, {IP, PrefixLen}, {Srv, ClientId}, Opts}, renewal}, - From, State) -> + From, State) + when ?IS_IPv4(IP) -> dhcpv4_renew(ClientId, IP, PrefixLen, Opts, Srv, From, State); handle_call({handle_event, {_, _, {IP, PrefixLen}, {_Srv, ClientId}, Opts}, rebinding}, - From, State) -> + From, State) + when ?IS_IPv4(IP) -> dhcpv4_rebind(ClientId, IP, PrefixLen, Opts, From, State); +handle_call({handle_event, {_, _, {IP, PrefixLen}, {Server, SrvId, ClientId}, Opts}, renewal}, + From, State) + when ?IS_IPv6(IP) -> + dhcpv6_renew(ClientId, IP, PrefixLen, Opts, SrvId, Server, From, State); + +handle_call({handle_event, {_, _, {IP, PrefixLen}, {_, _, ClientId}, Opts}, rebinding}, + From, State) + when ?IS_IPv6(IP) -> + dhcpv6_rebind(ClientId, IP, PrefixLen, Opts, From, State); + handle_call({handle_event, {_, _, IP, SrvId, Opts}, Ev}, _From, State) -> ?LOG(error, "unhandled DHCP event: ~p, (~p, ~p, ~p)", [Ev, IP, SrvId, Opts]), {reply, ok, State}; @@ -205,10 +227,16 @@ handle_call(Request, _From, State) -> ?LOG(warning, "handle_call: ~p", [Request]), {reply, error, State}. -handle_cast({release, IP, SrvId}, State) -> +handle_cast({release, {IP, 32}, SrvId}, State) + when ?IS_IPv4(IP) -> dhcpv4_release(IP, SrvId, State), {noreply, State}; +handle_cast({release, {IP, _} = Addr, SrvId}, State) + when ?IS_IPv6(IP) -> + dhcpv6_release(Addr, SrvId, State), + {noreply, State}; + handle_cast(Msg, State) -> ?LOG(debug, "handle_cast: ~p", [Msg]), {noreply, State}. @@ -302,7 +330,7 @@ dhcpv4_discover(Pool, Srv, Opts) -> dhcpv4_offer(ReqId, Timeout) -> case ergw_dhcp_socket:wait_response(ReqId, Timeout) of - {ok, #dhcp{options = #{?DHO_DHCP_MESSAGE_TYPE := ?DHCPOFFER}} = Answer} -> + {ok, _Srv, #dhcp{options = #{?DHO_DHCP_MESSAGE_TYPE := ?DHCPOFFER}} = Answer} -> Answer; {error, timeout} = Error -> Error; @@ -397,7 +425,7 @@ dhcpv4_release(IP, {Srv, ClientId}, #state{ipv4 = Pool}) -> dhcpv4_answer(ReqId, Timeout) -> case ergw_dhcp_socket:wait_response(ReqId, Timeout) of - {ok, #dhcp{options = #{?DHO_DHCP_MESSAGE_TYPE := Type}} = Answer} + {ok, _Srv, #dhcp{options = #{?DHO_DHCP_MESSAGE_TYPE := Type}} = Answer} when Type =:= ?DHCPDECLINE; Type =:= ?DHCPACK; Type =:= ?DHCPNAK -> @@ -483,3 +511,214 @@ dhcpv4_resp_opts(_K, _V, Opts) -> dhcpv4_resp_opts(Opts) -> Now = erlang:system_time(second), maps:fold(fun dhcpv4_resp_opts/3, #{'Base-Time' => Now}, Opts). + +%%%=================================================================== +%%% DHCPv6 functions +%%%=================================================================== + +time_default(0, Default) -> + Default; +time_default(Time, _) -> + Time. + +%% dhcpv6_spawn/3 +dhcpv6_spawn(Fun, From, #state{outstanding = OutS} = State) -> + ReqF = fun() -> exit({reply, Fun()}) end, + {_, Mref} = spawn_monitor(ReqF), + {noreply, State#state{outstanding = maps:put(Mref, From, OutS)}}. + +%% dhcpv6_init/6 +dhcpv6_init(ClientId, IP, PrefixLen, ReqOpts, From, #state{ipv6 = Pool} = State) + when is_record(Pool, pool) -> + ReqF = fun() -> dhcpv6_init_f(ClientId, IP, PrefixLen, ReqOpts, Pool) end, + dhcpv6_spawn(ReqF, From, State). + +%% dhcpv6_renew/6 +dhcpv6_renew(ClientId, IP, PrefixLen, Opts, SrvId, Srv, From, #state{ipv6 = Pool} = State) -> + ReqF = fun() -> dhcpv6_renew_f(ClientId, IP, PrefixLen, Opts, SrvId, Srv, Pool) end, + dhcpv6_spawn(ReqF, From, State). + +%% dhcpv6_rebind/6 +dhcpv6_rebind(ClientId, IP, PrefixLen, Opts, From, #state{ipv6 = Pool} = State) -> + ReqF = fun() -> dhcpv6_rebind_f(ClientId, IP, PrefixLen, Opts, Pool) end, + dhcpv6_spawn(ReqF, From, State). + +dhcpv6_init_f(ClientId, ReqIP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) -> + Srv = choose_server(Srvs), + Opts = dhcpv6_opts(ClientId, ReqIP, PrefixLen, ReqOpts), + case dhcpv6_solicit(Pool, Srv, Opts) of + {ok, Server, #dhcpv6{} = Advertise} -> + dhcpv6_request(Pool, ClientId, Opts, Server, Advertise); + Other -> + Other + end. + +dhcpv6_renew_f(ClientId, IP, PrefixLen, ReqOpts, SrvId, Srv, Pool) -> + Opts = dhcpv6_opts(ClientId, IP, PrefixLen, ReqOpts), + dhcpv6_renew(Pool, Srv, ClientId, IP, Opts, SrvId). + +dhcpv6_rebind_f(ClientId, IP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) -> + Srv = choose_server(Srvs), + Opts = dhcpv6_opts(ClientId, IP, PrefixLen, ReqOpts), + dhcpv6_rebind(Pool, Srv, ClientId, IP, Opts). + +dhcpv6_solicit(Pool, Srv, Opts) -> + DHCP = #dhcpv6{op = ?DHCPV6_SOLICIT, options = Opts}, + ReqId = dhcpv6_send_request(Pool, Srv, DHCP), + + TimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + dhcpv6_advertise(ReqId, {abs, TimeOut}). + +dhcpv6_advertise(ReqId, Timeout) -> + case ergw_dhcp_socket:wait_response(ReqId, Timeout) of + {ok, _, #dhcpv6{op = ?DHCPV6_ADVERTISE}} = Answer -> + Answer; + {error, timeout} = Error -> + Error; + {error, _} -> + dhcpv6_advertise(ReqId, Timeout) + end. + +dhcpv6_request(Pool, ClientId, Opts, Server, #dhcpv6{options = AdvOpts} = Advertise) -> + DHCP = Advertise#dhcpv6{op = ?DHCPV6_REQUEST, options = maps:merge(Opts, AdvOpts)}, + ReqId = dhcpv6_send_request(Pool, Server, DHCP), + + ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + case dhcpv6_reply(ReqId, {abs, ReqTimeOut}) of + {ok, Server, #dhcpv6{options = + #{?D6O_SERVERID := SrvId, + ?D6O_IA_PD := + #{?IAID := {T1, T2, + #{?D6O_IAPREFIX := IAPref}}} = IAOpts + } = RespOpts}} -> + case hd(maps:to_list(IAPref)) of + {{_,_} = PD, {_PrefLife, ValidLife, PDOpts}} -> + FinalOpts0 = + maps:merge( + maps:merge(dhcpv6_resp_opts(RespOpts), dhcpv6_resp_opts(IAOpts)), + dhcpv6_resp_opts(PDOpts)), + FinalOpts = + FinalOpts0#{lease => ValidLife, + renewal => time_default(T1, 3600), + rebinding => time_default(T2, 7200)}, + {ok, PD, {Server, SrvId, ClientId}, FinalOpts}; + _ -> + {error, failed} + end; + {error, _} = Error -> + Error + end. + +dhcpv6_renew(Pool, Srv, ClientId, IP, Opts, SrvId) -> + DHCP = #dhcpv6{op = ?DHCPV6_RENEW, options = Opts#{?D6O_SERVERID := SrvId}}, + ReqId = dhcpv6_send_request(Pool, Srv, DHCP), + + ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + case dhcpv6_reply(ReqId, {abs, ReqTimeOut}) of + {ok, Server, #dhcpv6{options = #{?D6O_SERVERID := SrvId} = RespOpts}} -> + IP = tbd, + {ok, {IP, 32}, {Server, SrvId, ClientId}, dhcpv6_resp_opts(RespOpts)}; + {error, _} = Error -> + Error + end. + +%% TBD: in REBIND we should broadcast the request (or send to all configured servers) +dhcpv6_rebind(Pool, Srv, ClientId, IP, Opts) -> + DHCP = #dhcpv6{op = ?DHCPV6_REBIND, options = Opts}, + ReqId = dhcpv6_send_request(Pool, Srv, DHCP), + + ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec + case dhcpv6_reply(ReqId, {abs, ReqTimeOut}) of + {ok, Server, #dhcpv6{options = #{?D6O_SERVERID := SrvId} = RespOpts}} -> + IP = tbd, + {ok, {IP, 32}, {Server, SrvId, ClientId}, dhcpv6_resp_opts(RespOpts)}; + {error, _} = Error -> + Error + end. + +dhcpv6_release({IP, PrefixLen}, {Srv, SrvId, ClientId}, #state{ipv6 = Pool}) -> + Opts0 = #{?D6O_SERVERID => SrvId, + ?D6O_CLIENTID => dhcpv6_client_id(ClientId)}, + Opts = dhcpv6_ia(IP, PrefixLen, [], Opts0), + DHCP = #dhcpv6{op = ?DHCPV6_RELEASE, options = Opts}, + _ReqId = dhcpv6_send_request(Pool, Srv, DHCP). + +dhcpv6_reply(ReqId, Timeout) -> + case ergw_dhcp_socket:wait_response(ReqId, Timeout) of + {ok, _, #dhcpv6{op = ?DHCPV6_REPLY}} = Answer -> + Answer; + {ok, _, #dhcpv6{} = Answer} -> + ?LOG(debug, "unexpected DHCP response ~p", [Answer]), + dhcpv6_reply(ReqId, Timeout); + {error, timeout} = Error -> + Error; + {error, _} -> + dhcpv6_reply(ReqId, Timeout) + end. + +dhcpv6_send_request(#pool{socket = Socket}, Srv, DHCP) -> + ergw_dhcp_socket:send_request(Socket, Srv, DHCP). + +%% 'Framed-IPv6-Pool' + +dhcpv6_req_list('DNS-Server-IPv6-Address', _, Opts) -> + ordsets:add_element(?D6O_NAME_SERVERS, Opts); +dhcpv6_req_list('3GPP-IPv6-DNS-Servers', _, Opts) -> + ordsets:add_element(?D6O_NAME_SERVERS, Opts); +dhcpv6_req_list('SIP-Servers-IPv6-Address-List', _, Opts) -> + ordsets:add_element(?D6O_SIP_SERVERS_ADDR, Opts); +dhcpv6_req_list(_, _, Opts) -> + Opts. + +dhcpv6_opts(ClientId, ReqIP, PrefixLen, ReqOpts) -> + ReqList0 = ordsets:from_list([?D6O_SOL_MAX_RT]), + ReqList = ordsets:to_list(maps:fold(fun dhcpv6_req_list/3, ReqList0, ReqOpts)), + Opts = + #{?D6O_CLIENTID => dhcpv6_client_id(ClientId), + ?D6O_ELAPSED_TIME => 0, + ?D6O_ORO => ReqList}, + dhcpv6_ia(ReqIP, PrefixLen, ReqOpts, Opts). + +dhcpv6_ia_na(?ZERO_IPv6, Opts) -> Opts; +dhcpv6_ia_na(IP, Opts) when ?IS_IPv6(IP) -> + Opts#{?D6O_IAADDR => #{IP => {0, 0, #{}}}}; +dhcpv6_ia_na(_, Opts) -> + Opts. + +dhcpv6_ia_pd(IP, PrefixLen, Opts) when PrefixLen /= 0, ?IS_IPv6(IP) -> + Opts#{?D6O_IAPREFIX => #{{IP, PrefixLen} => {0, 0, #{}}}}; +dhcpv6_ia_pd(_, _, Opts) -> + Opts. + +dhcpv6_ia(ReqIP, 128, _ReqOpts, Opts) -> + IAOpts = dhcpv6_ia_na(ReqIP, #{}), + IA = #{?IAID => {0, 0, IAOpts}}, + Opts#{?D6O_IA_NA => IA}; + +dhcpv6_ia(ReqIP, PrefixLen, _ReqOpts, Opts) -> + IAOpts = dhcpv6_ia_pd(ReqIP, PrefixLen, #{}), + IA = #{?IAID => {0, 0, IAOpts}}, + Opts#{?D6O_IA_PD => IA}. + +dhcpv6_client_id(ClientId) when is_binary(ClientId) -> + {2, ?VENDOR_ID_3GPP, ClientId}; +dhcpv6_client_id(ClientId) when is_integer(ClientId) -> + {2, ?VENDOR_ID_3GPP, integer_to_binary(ClientId)}; +dhcpv6_client_id(ClientId) when is_list(ClientId) -> + {2, ?VENDOR_ID_3GPP, iolist_to_binary(ClientId)}. + +dhcpv6_resp_opts(?D6O_NAME_SERVERS, V, Opts) -> + Opts#{'DNS-Server-IPv6-Address' => V}; +dhcpv6_resp_opts(?D6O_SIP_SERVERS_ADDR, V, Opts) -> + Opts#{'SIP-Servers-IPv6-Address-List' => V}; +dhcpv6_resp_opts(Opt, V, Opts) + when Opt =:= lease; + Opt =:= renewal; + Opt =:= rebinding -> + set_time(Opt, V, Opts); +dhcpv6_resp_opts(_K, _V, Opts) -> + Opts. + +dhcpv6_resp_opts(Opts) -> + Now = erlang:system_time(second), + maps:fold(fun dhcpv6_resp_opts/3, #{'Base-Time' => Now}, Opts). diff --git a/src/ergw_dhcp_socket.erl b/src/ergw_dhcp_socket.erl index ca3533cc..6a116990 100644 --- a/src/ergw_dhcp_socket.erl +++ b/src/ergw_dhcp_socket.erl @@ -28,7 +28,7 @@ -record(state, { name :: term(), - ip :: inet:ip_address(), + addr :: socket:sockaddr(), socket :: socket:socket(), xid :: xid(), @@ -36,7 +36,7 @@ }). -define(SERVER, ?MODULE). --define(DHCP_SERVER_PORT, 67). +-define(DHCPv4_SERVER_PORT, 67). -define(TIMEOUT, 10 * 1000). %%==================================================================== @@ -75,10 +75,12 @@ wait_response(Mref, Timeout) when is_integer(Timeout) -> receive {'DOWN', Mref, _, _, Reason} -> {error, Reason}; - {Mref, Reply} -> - {ok, Reply}; - Other -> - {error, Other} + {Mref, {error, _} = Error} -> + Error; + {Mref, {Source, Msg}} -> + {ok, Source, Msg}; + {Mref, Other} -> + Other after Timeout -> {error, timeout} end; @@ -95,7 +97,7 @@ wait_response(ReqId, {abs, Timeout}) -> %%%=================================================================== -define(SOCKET_OPTS, [netdev, netns, freebind, reuseaddr, rcvbuf]). --define(SocketDefaults, [{ip, invalid}, {port, dhcp}]). +-define(SocketDefaults, [{ip, invalid}]). validate_options(Name, Values) -> ergw_config:validate_options(fun validate_option/2, Values, @@ -105,9 +107,19 @@ validate_option(type, dhcp = Value) -> Value; validate_option(name, Value) when is_atom(Value) -> Value; -validate_option(ip, Value) - when is_tuple(Value) andalso - (tuple_size(Value) == 4 orelse tuple_size(Value) == 8) -> +validate_option(ip, Value) when is_tuple(Value) andalso tuple_size(Value) == 4 -> + #{family => inet, addr => Value, port => dhcp}; +validate_option(ip, Value) when is_tuple(Value) andalso tuple_size(Value) == 8 -> + #{family => inet6, addr => Value, port => dhcp}; +validate_option(ip, #{family := inet6, addr := _, port := _, scope := Scope} = Value) + when is_list(Scope) -> + case net:if_name2index(Scope) of + {ok, Idx} -> + Value#{scope => Idx}; + _ -> + throw({error, {options, {ip, Value}}}) + end; +validate_option(ip, #{family := _, addr := _, port := _} = Value) -> Value; validate_option(port, Value) when Value =:= dhcp; Value =:= random -> Value; @@ -136,19 +148,20 @@ validate_option(Opt, Value) -> %%% gen_server callbacks %%%=================================================================== -init(#{name := Name, ip := IP, port := PortOpt} = Opts) -> +init(#{name := Name} = Opts) -> process_flag(trap_exit, true), + Addr = socket_addr(Opts), SocketOpts = maps:with(?SOCKET_OPTS, Opts), - {ok, Socket} = make_dhcp_socket(IP, PortOpt, SocketOpts), + {ok, Socket} = make_dhcp_socket(Addr, SocketOpts), ergw_socket_reg:register('dhcp', Name, self()), State = #state{ name = Name, - ip = IP, + addr = Addr, socket = Socket, - xid = rand:uniform(16#ffffffff), + xid = init_xid(Addr), pending = gb_trees:empty() }, select(Socket), @@ -166,6 +179,11 @@ handle_cast({request, From, Srv, #dhcp{} = DHCP}, #state{xid = XId} = State) -> %% message_counter(tx, State, DHCP), send_request(Srv, Req, From, State#state{xid = (XId + 1) rem 16#100000000}); +handle_cast({request, From, Srv, #dhcpv6{} = DHCP}, #state{xid = XId} = State) -> + Req = DHCP#dhcpv6{xid = XId}, + %% message_counter(tx, State, DHCP), + send_request(Srv, Req, From, State#state{xid = (XId + 1) rem 16#1000000}); + handle_cast(Msg, State) -> ?LOG(error, "handle_cast: unknown ~p", [Msg]), {noreply, State}. @@ -195,37 +213,39 @@ code_change(_OldVsn, State, _Extra) -> %%% Socket functions %%%=================================================================== -family({_,_,_,_}) -> inet; -family({_,_,_,_,_,_,_,_}) -> inet6. +socket_addr(#{ip := #{family := Family, port := Port} = IP} = Opts) -> + IP#{port => port_opt(Family, maps:get(port, Opts, Port))}. -port_opt(dhcp) -> ?DHCP_SERVER_PORT; -port_opt(_) -> 0. +port_opt(inet, dhcp) -> ?DHCPv4_SERVER_PORT; +port_opt(inet6, dhcp) -> ?DHCPv6_SERVER_PORT; +port_opt(_, _) -> 0. -make_dhcp_socket(IP, Port, #{netns := NetNs} = Opts) +make_dhcp_socket(#{family := Family} = Addr, #{netns := NetNs} = Opts) when is_list(NetNs) -> - {ok, Socket} = socket:open(family(IP), dgram, udp, #{netns => NetNs}), - bind_dhcp_socket(Socket, IP, Port, Opts); -make_dhcp_socket(IP, Port, Opts) -> - {ok, Socket} = socket:open(family(IP), dgram, udp), - bind_dhcp_socket(Socket, IP, Port, Opts). + {ok, Socket} = socket:open(Family, dgram, udp, #{netns => NetNs}), + bind_dhcp_socket(Socket, Addr, Opts); +make_dhcp_socket(#{family := Family} = Addr, Opts) -> + {ok, Socket} = socket:open(Family, dgram, udp), + bind_dhcp_socket(Socket, Addr, Opts). -bind_dhcp_socket(Socket, {_,_,_,_} = IP, PortOpt, Opts) -> +bind_dhcp_socket(Socket, #{family := inet} = Addr, Opts) -> ok = socket_ip_freebind(Socket, Opts), ok = socket_netdev(Socket, Opts), - {ok, _} = socket:bind(Socket, #{family => inet, addr => IP, port => port_opt(PortOpt)}), + {ok, _} = socket:bind(Socket, Addr), ok = socket:setopt(Socket, socket, broadcast, true), ok = socket:setopt(Socket, ip, recverr, true), ok = socket:setopt(Socket, ip, mtu_discover, dont), maps:fold(fun(K, V, ok) -> ok = socket_setopts(Socket, K, V) end, ok, Opts), {ok, Socket}; -bind_dhcp_socket(Socket, {_,_,_,_,_,_,_,_} = IP, PortOpt, Opts) -> +bind_dhcp_socket(Socket, #{family := inet6} = Addr, Opts) -> ok = socket:setopt(Socket, ipv6, v6only, true), ok = socket_netdev(Socket, Opts), - {ok, _} = socket:bind(Socket, #{family => inet6, addr => IP, port => port_opt(PortOpt)}), + {ok, _} = socket:bind(Socket, Addr), ok = socket:setopt(Socket, socket, broadcast, true), ok = socket:setopt(Socket, ipv6, recverr, true), ok = socket:setopt(Socket, ipv6, mtu_discover, dont), + %%ok = socket:setopt(Socket, ipv6, multicast_if, 1), maps:fold(fun(K, V, ok) -> ok = socket_setopts(Socket, K, V) end, ok, Opts), {ok, Socket}. @@ -379,7 +399,7 @@ handle_message(#{family := Family, port := Port, addr := IP} = Source, State0 end. -handle_response(_Source, Msg, #state{name = _Name} = State) -> +handle_response(Source, Msg, #state{name = _Name} = State) -> XId = case Msg of #dhcp{xid = Id} -> Id; #dhcpv6{xid = Id} -> Id @@ -392,7 +412,7 @@ handle_response(_Source, Msg, #state{name = _Name} = State) -> {value, From} -> %% ergw_prometheus:dhcp(rx, Name, IP, Msg), - reply(From, Msg), + reply(From, Source, Msg), State end. @@ -400,13 +420,32 @@ handle_response(_Source, Msg, #state{name = _Name} = State) -> %%% Internal functions %%%=================================================================== +init_xid(#{family := inet}) -> + rand:uniform(16#ffffffff); +init_xid(#{family := inet6}) -> + rand:uniform(16#ffffff). + +reply(From, Source, Msg) -> + reply(From, {Source, Msg}). + reply({Pid, Ref}, Reply) -> Pid ! {Ref, Reply}. send_request(Srv, #dhcp{xid = XId} = Req0, From, State) -> Req = dhcp_req_opts(Req0, State), - SendTo = dhcp_server(Srv), + SendTo = dhcp_server(inet, Srv), Data = dhcp_lib:encode(Req), + case sendto(State, SendTo, Data) of + ok -> + {noreply, start_request(XId, Req, From, State)}; + {error, _} = Error -> + reply(From, Error), + {noreply, State} + end; +send_request(Srv, #dhcpv6{xid = XId} = Req0, From, State) -> + Req = dhcp_req_opts(Req0, State), + SendTo = dhcp_server(inet6, Srv), + Data = dhcpv6_lib:encode(Req), case sendto(State, SendTo, Data) of ok -> {noreply, start_request(XId, Req, From, State)}; @@ -425,25 +464,37 @@ lookup_request(Xid, #state{pending = Pending}) -> remove_request(Xid, #state{pending = Pending} = State) -> State#state{pending = gb_trees:delete_any(Xid, Pending)}. -sendto(#state{socket = Socket, ip = SrcIP}, DstIP, Data) -> - Dest = #{family => family(SrcIP), - addr => DstIP, - port => ?DHCP_SERVER_PORT}, - socket:sendto(Socket, Data, Dest, nowait). +sendto(#state{socket = Socket}, Dst, Data) -> + socket:sendto(Socket, Data, Dst, nowait). dhcp_req_opts(#dhcp{options = Opts} = Req, _State) -> %% when Port /= ?DHCP_SERVER_PORT -> - AgentOpts = [{?RAI_DHCPV4_RELAY_SOURCE_PORT, ?DHCP_SERVER_PORT} | + AgentOpts = [{?RAI_DHCPV4_RELAY_SOURCE_PORT, ?DHCPv4_SERVER_PORT} | dhcp_lib:get_opt(?DHO_DHCP_AGENT_OPTIONS, Opts, [])], Req#dhcp{options = dhcp_lib:put_opt(?DHO_DHCP_AGENT_OPTIONS, AgentOpts, Opts)}; dhcp_req_opts(Req, _) -> Req. -dhcp_server(broadcast) -> - broadcast; -dhcp_server({0,0,0,0}) -> - broadcast; -dhcp_server(SendTo) -> +dest_addr(inet, Dst) -> + #{family => inet, addr => Dst, port => ?DHCPv4_SERVER_PORT}; +dest_addr(inet6, Dst) -> + #{family => inet6, addr => Dst, port => ?DHCPv6_SERVER_PORT}. + +dhcp_server(inet, broadcast) -> + dest_addr(inet, broadcast); +dhcp_server(inet, {0,0,0,0}) -> + dest_addr(inet, broadcast); + +dhcp_server(inet6, local) -> + dest_addr(inet6, ?DHCPv6_NODE_LOCAL_ALL_ROUTERS); +dhcp_server(inet6, broadcast) -> + dest_addr(inet6, ?ALL_DHCPv6_RELAY_AGENTS_AND_SERVERS); +dhcp_server(inet6, {0,0,0,0,0,0,0,0}) -> + dest_addr(inet6, ?ALL_DHCPv6_RELAY_AGENTS_AND_SERVERS); + +dhcp_server(Family, SendTo) when is_tuple(SendTo) -> + dest_addr(Family, SendTo); +dhcp_server(Family, #{family := Family} = SendTo) -> SendTo. %%%=================================================================== diff --git a/test/dhcp_pool_SUITE.erl b/test/dhcp_pool_SUITE.erl index 45243ce7..1606a983 100644 --- a/test/dhcp_pool_SUITE.erl +++ b/test/dhcp_pool_SUITE.erl @@ -61,16 +61,16 @@ ]}, {dhcpv6, - [{server_id, ?LOCALHOST_IPv6}, + [{server_id, {4, <<112,239,124,234,172,81,85,194,223,29,48,191,218,176,68,242>>}}, {next_server, ?LOCALHOST_IPv6}, - {socket, #{netdev => "lo", - ip => ?LOCALHOST_IPv6, - join => {1, [all_dhcp_servers, all_dhcp_relay_agents_and_servers]}}}, + {socket, #{%%netdev => "lo", + ip => any, + join => {0, [local, all_dhcp_servers, all_dhcp_relay_agents_and_servers]}}}, {authoritative, true}, {lease_file, "/var/run/dhcpv6_leases"}, {subnets, [#{subnet => ?IPv6PoolNet, - options => [], + options => [{23, [?LOCALHOST_IPv6]}], %% Domain Name Server, pools => [#{pool => {?IPv6HostPoolStart, ?IPv6HostPoolEnd}, options => []}], @@ -117,8 +117,7 @@ {'dhcp-v6', [{type, dhcp}, %%{ip, ?MUST_BE_UPDATED}, - {ip, ?LOCALHOST_IPv6}, - {port, random}, + {ip, #{family => inet6, addr => any, port => random}}, {reuseaddr, true} ]} ]}, @@ -130,7 +129,7 @@ {servers, [broadcast]}]}, {ipv6, [{socket, 'dhcp-v6'}, {id, {16#8001, 0, 1, 0, 0, 0, 0, 0}}, - {servers, [broadcast]}]} + {servers, [local]}]} ]} ]}, @@ -301,12 +300,12 @@ dhcpv6(_Config) -> Pool = <<"pool-A">>, IP = ipv6, PrefixLen = 64, - Opts = #{'MS-Primary-DNS-Server' => true, - 'MS-Secondary-DNS-Server' => true}, + Opts = #{'DNS-Server-IPv6-Address' => true}, ReqId = ergw_ip_pool:send_request(ClientId, [{Pool, IP, PrefixLen, Opts}]), [AllocInfo] = ergw_ip_pool:wait_response(ReqId), - ?match({ergw_dhcp_pool, _, {{_,_,_,_}, 32}, _, #{'MS-Primary-DNS-Server' := {_,_,_,_}}}, + ?match({ergw_dhcp_pool, _, {{_,_,_,_,_,_,_,_}, 64}, _, + #{'DNS-Server-IPv6-Address' := [{_,_,_,_,_,_,_,_}|_]}}, AllocInfo), ergw_ip_pool:release([AllocInfo]), From 53a23ab9072c330149862000f4e148c44b9df07a Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Tue, 17 Nov 2020 11:41:39 +0100 Subject: [PATCH 7/8] implement DHCPv6 renew and rebind --- src/ergw_dhcp_pool.erl | 72 +++++++++++++++++++++------------------- test/dhcp_pool_SUITE.erl | 48 ++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 35 deletions(-) diff --git a/src/ergw_dhcp_pool.erl b/src/ergw_dhcp_pool.erl index 384f9696..270f58c7 100644 --- a/src/ergw_dhcp_pool.erl +++ b/src/ergw_dhcp_pool.erl @@ -555,12 +555,12 @@ dhcpv6_init_f(ClientId, ReqIP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) dhcpv6_renew_f(ClientId, IP, PrefixLen, ReqOpts, SrvId, Srv, Pool) -> Opts = dhcpv6_opts(ClientId, IP, PrefixLen, ReqOpts), - dhcpv6_renew(Pool, Srv, ClientId, IP, Opts, SrvId). + dhcpv6_renew(Pool, Srv, ClientId, Opts, SrvId). dhcpv6_rebind_f(ClientId, IP, PrefixLen, ReqOpts, #pool{servers = Srvs} = Pool) -> Srv = choose_server(Srvs), Opts = dhcpv6_opts(ClientId, IP, PrefixLen, ReqOpts), - dhcpv6_rebind(Pool, Srv, ClientId, IP, Opts). + dhcpv6_rebind(Pool, Srv, ClientId, Opts). dhcpv6_solicit(Pool, Srv, Opts) -> DHCP = #dhcpv6{op = ?DHCPV6_SOLICIT, options = Opts}, @@ -579,59 +579,39 @@ dhcpv6_advertise(ReqId, Timeout) -> dhcpv6_advertise(ReqId, Timeout) end. -dhcpv6_request(Pool, ClientId, Opts, Server, #dhcpv6{options = AdvOpts} = Advertise) -> +dhcpv6_request(Pool, ClientId, Opts, Server, + #dhcpv6{options = #{?D6O_SERVERID := SrvId} = AdvOpts} = Advertise) -> DHCP = Advertise#dhcpv6{op = ?DHCPV6_REQUEST, options = maps:merge(Opts, AdvOpts)}, ReqId = dhcpv6_send_request(Pool, Server, DHCP), ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec case dhcpv6_reply(ReqId, {abs, ReqTimeOut}) of - {ok, Server, #dhcpv6{options = - #{?D6O_SERVERID := SrvId, - ?D6O_IA_PD := - #{?IAID := {T1, T2, - #{?D6O_IAPREFIX := IAPref}}} = IAOpts - } = RespOpts}} -> - case hd(maps:to_list(IAPref)) of - {{_,_} = PD, {_PrefLife, ValidLife, PDOpts}} -> - FinalOpts0 = - maps:merge( - maps:merge(dhcpv6_resp_opts(RespOpts), dhcpv6_resp_opts(IAOpts)), - dhcpv6_resp_opts(PDOpts)), - FinalOpts = - FinalOpts0#{lease => ValidLife, - renewal => time_default(T1, 3600), - rebinding => time_default(T2, 7200)}, - {ok, PD, {Server, SrvId, ClientId}, FinalOpts}; - _ -> - {error, failed} - end; + {ok, Server, #dhcpv6{options = #{?D6O_SERVERID := SrvId}} = Reply} -> + dhcpv6_resp_ia(ClientId, Server, Reply); {error, _} = Error -> Error end. -dhcpv6_renew(Pool, Srv, ClientId, IP, Opts, SrvId) -> - DHCP = #dhcpv6{op = ?DHCPV6_RENEW, options = Opts#{?D6O_SERVERID := SrvId}}, +dhcpv6_renew(Pool, Srv, ClientId, Opts, SrvId) -> + DHCP = #dhcpv6{op = ?DHCPV6_RENEW, options = Opts#{?D6O_SERVERID => SrvId}}, ReqId = dhcpv6_send_request(Pool, Srv, DHCP), ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec case dhcpv6_reply(ReqId, {abs, ReqTimeOut}) of - {ok, Server, #dhcpv6{options = #{?D6O_SERVERID := SrvId} = RespOpts}} -> - IP = tbd, - {ok, {IP, 32}, {Server, SrvId, ClientId}, dhcpv6_resp_opts(RespOpts)}; + {ok, Server, #dhcpv6{options = #{?D6O_SERVERID := SrvId}} = Reply} -> + dhcpv6_resp_ia(ClientId, Server, Reply); {error, _} = Error -> Error end. -%% TBD: in REBIND we should broadcast the request (or send to all configured servers) -dhcpv6_rebind(Pool, Srv, ClientId, IP, Opts) -> - DHCP = #dhcpv6{op = ?DHCPV6_REBIND, options = Opts}, +dhcpv6_rebind(Pool, Srv, ClientId, Opts) -> + DHCP = #dhcpv6{op = ?DHCPV6_REBIND, options = maps:remove(?D6O_SERVERID, Opts)}, ReqId = dhcpv6_send_request(Pool, Srv, DHCP), ReqTimeOut = erlang:monotonic_time(millisecond) + 1000, %% 1 sec case dhcpv6_reply(ReqId, {abs, ReqTimeOut}) of - {ok, Server, #dhcpv6{options = #{?D6O_SERVERID := SrvId} = RespOpts}} -> - IP = tbd, - {ok, {IP, 32}, {Server, SrvId, ClientId}, dhcpv6_resp_opts(RespOpts)}; + {ok, Server, #dhcpv6{} = Reply} -> + dhcpv6_resp_ia(ClientId, Server, Reply); {error, _} = Error -> Error end. @@ -707,6 +687,30 @@ dhcpv6_client_id(ClientId) when is_integer(ClientId) -> dhcpv6_client_id(ClientId) when is_list(ClientId) -> {2, ?VENDOR_ID_3GPP, iolist_to_binary(ClientId)}. +dhcpv6_resp_ia(ClientId, Server, + #dhcpv6{options = + #{?D6O_SERVERID := SrvId, + ?D6O_IA_PD := + #{?IAID := {T1, T2, + #{?D6O_IAPREFIX := IAPref}}} = IAOpts + } = RespOpts}) -> + case hd(maps:to_list(IAPref)) of + {{_,_} = PD, {_PrefLife, ValidLife, PDOpts}} -> + FinalOpts0 = + maps:merge( + maps:merge(dhcpv6_resp_opts(RespOpts), dhcpv6_resp_opts(IAOpts)), + dhcpv6_resp_opts(PDOpts)), + FinalOpts = + FinalOpts0#{lease => ValidLife, + renewal => time_default(T1, 3600), + rebinding => time_default(T2, 7200)}, + {ok, PD, {Server, SrvId, ClientId}, FinalOpts}; + _ -> + {error, failed} + end; +dhcpv6_resp_ia(_, _, _) -> + {error, failed}. + dhcpv6_resp_opts(?D6O_NAME_SERVERS, V, Opts) -> Opts#{'DNS-Server-IPv6-Address' => V}; dhcpv6_resp_opts(?D6O_SIP_SERVERS_ADDR, V, Opts) -> diff --git a/test/dhcp_pool_SUITE.erl b/test/dhcp_pool_SUITE.erl index 1606a983..4f127c6b 100644 --- a/test/dhcp_pool_SUITE.erl +++ b/test/dhcp_pool_SUITE.erl @@ -231,7 +231,7 @@ end_per_group(Group, Config) groups() -> [{ipv4, [], [dhcpv4, v4_renew]}, - {ipv6, [], [dhcpv6]}]. + {ipv6, [], [dhcpv6, v6_renew, v6_rebind]}]. all() -> [{group, ipv4}, @@ -312,6 +312,52 @@ dhcpv6(_Config) -> ct:sleep(100), ok. +%-------------------------------------------------------------------- +v6_renew() -> + [{doc, "Test simple dhcpv6 requests"}]. +v6_renew(_Config) -> + ClientId = <<"aaaaa">>, + Pool = <<"pool-A">>, + IP = ipv6, + PrefixLen = 64, + Opts = #{'DNS-Server-IPv6-Address' => true}, + + ReqId = ergw_ip_pool:send_request(ClientId, [{Pool, IP, PrefixLen, Opts}]), + [AllocInfo] = ergw_ip_pool:wait_response(ReqId), + ?match({ergw_dhcp_pool, _, {{_,_,_,_,_,_,_,_}, 64}, _, + #{'DNS-Server-IPv6-Address' := [{_,_,_,_,_,_,_,_}|_]}}, + AllocInfo), + + ergw_ip_pool:handle_event(AllocInfo, renewal), + ct:sleep({seconds, 1}), + + ergw_ip_pool:release([AllocInfo]), + ct:sleep(100), + ok. + +%-------------------------------------------------------------------- +v6_rebind() -> + [{doc, "Test simple dhcpv6 requests"}]. +v6_rebind(_Config) -> + ClientId = <<"aaaaa">>, + Pool = <<"pool-A">>, + IP = ipv6, + PrefixLen = 64, + Opts = #{'DNS-Server-IPv6-Address' => true}, + + ReqId = ergw_ip_pool:send_request(ClientId, [{Pool, IP, PrefixLen, Opts}]), + [AllocInfo] = ergw_ip_pool:wait_response(ReqId), + ?match({ergw_dhcp_pool, _, {{_,_,_,_,_,_,_,_}, 64}, _, + #{'DNS-Server-IPv6-Address' := [{_,_,_,_,_,_,_,_}|_]}}, + AllocInfo), + + ergw_ip_pool:handle_event(AllocInfo, rebinding), + ct:sleep({seconds, 1}), + + ergw_ip_pool:release([AllocInfo]), + ct:sleep(100), + ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== From c9782c6b4b9c34e41ea08d2d97ac0de3d4765311 Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Tue, 17 Nov 2020 12:36:25 +0100 Subject: [PATCH 8/8] CI: add local IPv6 multicast route for DHCPv6 tests --- .gitlab-ci.yml | 1 + .travis.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dd031ad2..1cd7a35c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,6 +61,7 @@ build:otp-23.1: - ip addr add fd96:dcd2:efdb:41c3::30/64 dev lo - ip addr add fd96:dcd2:efdb:41c3::40/64 dev lo - ip addr add fd96:dcd2:efdb:41c3::50/64 dev lo + - ip -6 route add ff01::/16 dev lo table local - ./rebar3 do xref - ./rebar3 do ct diff --git a/.travis.yml b/.travis.yml index a3baa224..4f91a296 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,7 @@ before_script: sudo ip addr add fd96:dcd2:efdb:41c3::40/64 dev lo; sudo ip addr add fd96:dcd2:efdb:41c3::50/64 dev lo; sudo sh -c 'echo "::1 localhost ip6-localhost ip6-loopback" >> /etc/hosts'; + sudo ip -6 route add ff01::/16 dev lo table local; fi script: