File 6579-inet_dns-support-UPDATE.patch of Package erlang
From 89f6c65a4af3a21da0ecf268b0982b6fe158873b Mon Sep 17 00:00:00 2001
From: Alexander Clouter <alex@digriz.org.uk>
Date: Thu, 20 Apr 2023 08:22:11 +0100
Subject: [PATCH 09/10] inet_dns: support UPDATE
---
lib/kernel/src/inet_dns.erl | 79 +++++++++++--------
lib/kernel/src/inet_dns.hrl | 13 ++-
lib/kernel/src/inet_res.erl | 5 ++
lib/kernel/test/inet_res_SUITE.erl | 62 ++++++++++++++-
.../inet_res_SUITE_data/otptest/knot_inc.conf | 6 ++
5 files changed, 129 insertions(+), 36 deletions(-)
diff --git a/lib/kernel/src/inet_dns.erl b/lib/kernel/src/inet_dns.erl
index fc38e437aa..6228ca9790 100644
--- a/lib/kernel/src/inet_dns.erl
+++ b/lib/kernel/src/inet_dns.erl
@@ -24,6 +24,7 @@
%% RFC 1035: Domain Names - Implementation and Specification
%% RFC 1995: Incremental Zone Transfer in DNS
%% RFC 1996: A Mechanism for Prompt Notification of Zone Changes (DNS NOTIFY)
+%% RFC 2136: Dynamic Updates in the Domain Name System (DNS UPDATE)
%% RFC 2181: Clarifications to the DNS Specification
%% RFC 2782: A DNS RR for specifying the location of services (DNS SRV)
%% RFC 2915: The Naming Authority Pointer (NAPTR) DNS Resource Rec
@@ -160,9 +161,9 @@ do_decode(<<Id:16,
QdCount:16,AnCount:16,NsCount:16,ArCount:16,
QdBuf/binary>>=Buffer) ->
{AnBuf,QdList,QdTC} = decode_query_section(QdBuf,QdCount,Buffer),
- {NsBuf,AnList,AnTC} = decode_rr_section(AnBuf,AnCount,Buffer),
- {ArBuf,NsList,NsTC} = decode_rr_section(NsBuf,NsCount,Buffer),
- {Rest,ArList,ArTC} = decode_rr_section(ArBuf,ArCount,Buffer),
+ {NsBuf,AnList,AnTC} = decode_rr_section(Opcode,AnBuf,AnCount,Buffer),
+ {ArBuf,NsList,NsTC} = decode_rr_section(Opcode,NsBuf,NsCount,Buffer),
+ {Rest,ArList,ArTC} = decode_rr_section(Opcode,ArBuf,ArCount,Buffer),
?MATCH_ELSE_DECODE_ERROR(
Rest,
<<>>,
@@ -220,14 +221,14 @@ decode_query_section(Bin, N, Buffer, Qs) ->
decode_query_section(Rest, N-1, Buffer, [DnsQuery|Qs])
end).
-decode_rr_section(Bin, N, Buffer) ->
- decode_rr_section(Bin, N, Buffer, []).
+decode_rr_section(Opcode, Bin, N, Buffer) ->
+ decode_rr_section(Opcode, Bin, N, Buffer, []).
-decode_rr_section(<<>>=Rest, N, _Buffer, RRs) ->
+decode_rr_section(_Opcode, <<>>=Rest, N, _Buffer, RRs) ->
{Rest,reverse(RRs),N =/= 0};
-decode_rr_section(Rest, 0, _Buffer, RRs) ->
+decode_rr_section(_Opcode, Rest, 0, _Buffer, RRs) ->
{Rest,reverse(RRs),false};
-decode_rr_section(Bin, N, Buffer, RRs) ->
+decode_rr_section(Opcode, Bin, N, Buffer, RRs) ->
?MATCH_ELSE_DECODE_ERROR(
decode_name(Bin, Buffer),
{<<T:16/unsigned,C:16/unsigned,TTL:4/binary,
@@ -254,7 +255,13 @@ decode_rr_section(Bin, N, Buffer, RRs) ->
do = DnssecOk};
_ ->
{Class,CacheFlush} = decode_class(C),
- Data = decode_data(D, Class, Type, Buffer),
+ Data = if
+ %% RFC 2136: 2.4. Allow length zero data for UPDATE
+ Opcode == ?UPDATE, D == <<>> ->
+ #dns_rr{}#dns_rr.data;
+ true ->
+ decode_data(D, Class, Type, Buffer)
+ end,
<<TimeToLive:32/signed>> = TTL,
#dns_rr{
domain = Name,
@@ -264,7 +271,7 @@ decode_rr_section(Bin, N, Buffer, RRs) ->
data = Data,
func = CacheFlush}
end,
- decode_rr_section(Rest, N-1, Buffer, [RR|RRs])
+ decode_rr_section(Opcode, Rest, N-1, Buffer, [RR|RRs])
end).
%%
@@ -276,12 +283,13 @@ encode(Q) ->
AnCount = length(Q#dns_rec.anlist),
NsCount = length(Q#dns_rec.nslist),
ArCount = length(Q#dns_rec.arlist),
+ OC = Q#dns_rec.header#dns_header.opcode,
B0 = encode_header(Q#dns_rec.header, QdCount, AnCount, NsCount, ArCount),
C0 = gb_trees:empty(),
{B1,C1} = encode_query_section(B0, C0, Q#dns_rec.qdlist),
- {B2,C2} = encode_res_section(B1, C1, Q#dns_rec.anlist),
- {B3,C3} = encode_res_section(B2, C2, Q#dns_rec.nslist),
- {B,_} = encode_res_section(B3, C3, Q#dns_rec.arlist),
+ {B2,C2} = encode_res_section(OC, B1, C1, Q#dns_rec.anlist),
+ {B3,C3} = encode_res_section(OC, B2, C2, Q#dns_rec.nslist),
+ {B,_} = encode_res_section(OC, B3, C3, Q#dns_rec.arlist),
B.
@@ -313,9 +321,9 @@ encode_query_section(Bin0, Comp0, [#dns_query{domain=DName}=Q | Qs]) ->
%% RFC 1035: 4.1.3. Resource record format
%% RFC 6891: 6.1.2, 6.1.3, 6.2.3 Opt RR format
%%
-encode_res_section(Bin, Comp, []) -> {Bin,Comp};
+encode_res_section(_Opcode, Bin, Comp, []) -> {Bin,Comp};
encode_res_section(
- Bin, Comp,
+ Opcode, Bin, Comp,
[#dns_rr{
domain = DName,
type = Type,
@@ -324,10 +332,10 @@ encode_res_section(
ttl = TTL,
data = Data} | Rs]) ->
encode_res_section_rr(
- Bin, Comp, Rs, DName, Type, Class, CacheFlush,
+ Opcode, Bin, Comp, Rs, DName, Type, Class, CacheFlush,
<<TTL:32/signed>>, Data);
encode_res_section(
- Bin, Comp,
+ Opcode, Bin, Comp,
[#dns_rr_opt{
domain = DName,
udp_payload_size = UdpPayloadSize,
@@ -338,18 +346,24 @@ encode_res_section(
do = DnssecOk} | Rs]) ->
DO = case DnssecOk of true -> 1; false -> 0 end,
encode_res_section_rr(
- Bin, Comp, Rs, DName, ?S_OPT, UdpPayloadSize, false,
+ Opcode, Bin, Comp, Rs, DName, ?S_OPT, UdpPayloadSize, false,
<<ExtRCode,Version,DO:1,Z:15>>, Data).
encode_res_section_rr(
- Bin0, Comp0, Rs, DName, Type, Class, CacheFlush, TTL, Data) ->
+ Opcode, Bin0, Comp0, Rs, DName, Type, Class, CacheFlush, TTL, Data) ->
T = encode_type(Type),
C = encode_class(Class, CacheFlush),
{Bin,Comp1} = encode_name(Bin0, Comp0, byte_size(Bin0), DName),
Pos = byte_size(Bin)+(2+2)+byte_size(TTL)+2,
- {DataBin,Comp} = encode_data(Comp1, Pos, Type, Class, Data),
+ {DataBin,Comp} = if
+ Opcode == update, Data == #dns_rr{}#dns_rr.data ->
+ {<<>>,Comp1};
+ true ->
+ encode_data(Comp1, Pos, Type, Class, Data)
+ end,
DataSize = byte_size(DataBin),
encode_res_section(
+ Opcode,
<<Bin/binary,T:16,C:16,TTL/binary,DataSize:16,DataBin/binary>>,
Comp, Rs).
@@ -450,6 +464,7 @@ decode_class(C0) ->
?C_IN -> in;
?C_CHAOS -> chaos;
?C_HS -> hs;
+ ?C_NONE -> none;
?C_ANY -> any;
_ -> C %% raw unknown class
end,
@@ -469,6 +484,7 @@ encode_class(Class) ->
in -> ?C_IN;
chaos -> ?C_CHAOS;
hs -> ?C_HS;
+ none -> ?C_NONE;
any -> ?C_ANY;
Class when is_integer(Class) -> Class %% raw unknown class
end.
@@ -479,6 +495,7 @@ decode_opcode(Opcode) ->
?IQUERY -> iquery;
?STATUS -> status;
?NOTIFY -> notify;
+ ?UPDATE -> update;
_ when is_integer(Opcode) -> Opcode %% non-standard opcode
end.
@@ -488,6 +505,7 @@ encode_opcode(Opcode) ->
iquery -> ?IQUERY;
status -> ?STATUS;
notify -> ?NOTIFY;
+ update -> ?UPDATE;
_ when is_integer(Opcode) -> Opcode %% non-standard opcode
end.
@@ -715,17 +733,6 @@ decode_name_label(Label, Name, N) ->
%%
%% Data field -> {binary(),NewCompressionTable}
%%
-%% Class IN RRs
-encode_data(Comp, _, ?S_A, in, Addr) ->
- {A,B,C,D} = Addr,
- {<<A,B,C,D>>,Comp};
-encode_data(Comp, _, ?S_AAAA, in, Addr) ->
- {A,B,C,D,E,F,G,H} = Addr,
- {<<A:16,B:16,C:16,D:16,E:16,F:16,G:16,H:16>>,Comp};
-encode_data(Comp, _, ?S_WKS, in, Data) ->
- {{A,B,C,D},Proto,BitMap} = Data,
- BitMapBin = iolist_to_binary(BitMap),
- {<<A,B,C,D,Proto,BitMapBin/binary>>,Comp};
%% OPT pseudo-RR (of no class) - should not take this way;
%% this must be a #dns_rr{type = ?S_OPT} instead of a #dns_rr_opt{},
%% so good luck getting in particular Class and TTL right...
@@ -742,6 +749,16 @@ encode_data(Comp, Pos, Type, Class, Data) ->
%%
%%
%% Standard RRs (any class)
+encode_data(Comp, _, ?S_A, Addr) ->
+ {A,B,C,D} = Addr,
+ {<<A,B,C,D>>,Comp};
+encode_data(Comp, _, ?S_AAAA, Addr) ->
+ {A,B,C,D,E,F,G,H} = Addr,
+ {<<A:16,B:16,C:16,D:16,E:16,F:16,G:16,H:16>>,Comp};
+encode_data(Comp, _, ?S_WKS, Data) ->
+ {{A,B,C,D},Proto,BitMap} = Data,
+ BitMapBin = iolist_to_binary(BitMap),
+ {<<A,B,C,D,Proto,BitMapBin/binary>>,Comp};
encode_data(Comp, Pos, ?S_SOA, Data) ->
{MName,RName,Serial,Refresh,Retry,Expiry,Minimum} = Data,
{B1,Comp1} = encode_name(Comp, Pos, MName),
diff --git a/lib/kernel/src/inet_dns.hrl b/lib/kernel/src/inet_dns.hrl
index 39cdd4252f..17151fbb6a 100644
--- a/lib/kernel/src/inet_dns.hrl
+++ b/lib/kernel/src/inet_dns.hrl
@@ -28,6 +28,7 @@
-define(IQUERY, 16#1). %% inverse query
-define(STATUS, 16#2). %% nameserver status query
-define(NOTIFY, 16#4). %% notify
+-define(UPDATE, 16#5). %% dynamic update
%%
%% Currently defined response codes
@@ -38,6 +39,11 @@
-define(NXDOMAIN, 3). %% non existent domain
-define(NOTIMP, 4). %% not implemented
-define(REFUSED, 5). %% query refused
+-define(YXDOMAIN, 6). %% name exists when it should not (DDNS)
+-define(YXRRSET, 7). %% RR set exists when it should not (DDNS)
+-define(NXRRSET, 8). %% RR set that should exist does not (DDNS)
+-define(NOTAUTH, 9). %% server not authoritative for zone (DDNS)
+-define(NOTZONE, 10). %% name not contained in zone (DDNS)
-define(BADVERS, 16). %% bad version EDNS pseudo-rr RFC6891: 6.1.3
%%
@@ -134,6 +140,7 @@
-define(C_IN, 1). %% the arpa internet
-define(C_CHAOS, 3). %% for chaos net at MIT
-define(C_HS, 4). %% for Hesiod name server at MIT
+-define(C_NONE, 254). %% for DDNS (RFC2136, section 2.4)
-define(C_ANY, 255). %% wildcard match
%%
@@ -161,9 +168,9 @@
-record(dns_rec,
{
header, %% dns_header record
- qdlist = [], %% list of question entries
- anlist = [], %% list of answer entries
- nslist = [], %% list of authority entries
+ qdlist = [], %% list of question (for UPDATE 'zone') entries
+ anlist = [], %% list of answer (for UPDATE 'prequisites') entries
+ nslist = [], %% list of authority (for UPDATE 'update') entries
arlist = [] %% list of resource entries
}).
diff --git a/lib/kernel/src/inet_res.erl b/lib/kernel/src/inet_res.erl
index 0fb46332f2..f0d2f21364 100644
--- a/lib/kernel/src/inet_res.erl
+++ b/lib/kernel/src/inet_res.erl
@@ -1106,6 +1106,11 @@ decode_answer(Answer, Q_Msg, Verbose) ->
?NXDOMAIN -> {error,{nxdomain,Msg}};
?NOTIMP -> {error,{notimp,Msg}};
?REFUSED -> {error,{refused,Msg}};
+ ?YXDOMAIN -> {error,{yxdomain,Msg}};
+ ?YXRRSET -> {error,{yxrrset,Msg}};
+ ?NXRRSET -> {error,{nxrrset,Msg}};
+ ?NOTAUTH -> {error,{noauth,Msg}};
+ ?NOTZONE -> {error,{nozone,Msg}};
?BADVERS -> {error,{badvers,Msg}};
_ -> {error,{unknown,Msg}}
end;
diff --git a/lib/kernel/test/inet_res_SUITE.erl b/lib/kernel/test/inet_res_SUITE.erl
index c620e6d93e..c69cf934ce 100644
--- a/lib/kernel/test/inet_res_SUITE.erl
+++ b/lib/kernel/test/inet_res_SUITE.erl
@@ -35,7 +35,7 @@
edns0/1, edns0_multi_formerr/1, txt_record/1, files_monitor/1,
nxdomain_reply/1, last_ms_answer/1, intermediate_error/1,
servfail_retry_timeout_default/1, servfail_retry_timeout_1000/1,
- label_compression_limit/1
+ label_compression_limit/1, update/1
]).
-export([
gethostbyaddr/0, gethostbyaddr/1,
@@ -77,7 +77,7 @@ all() ->
nxdomain_reply, last_ms_answer,
intermediate_error,
servfail_retry_timeout_default, servfail_retry_timeout_1000,
- label_compression_limit,
+ label_compression_limit, update,
gethostbyaddr, gethostbyaddr_v6, gethostbyname,
gethostbyname_v6, getaddr, getaddr_v6, ipv4_to_ipv6,
host_and_addr].
@@ -133,6 +133,7 @@ zone_dir(TC) ->
files_monitor -> otptest;
nxdomain_reply -> otptest;
last_ms_answer -> otptest;
+ update -> otptest;
intermediate_error ->
{internal,
#{rcode => ?REFUSED}};
@@ -1412,6 +1413,63 @@ incr_domain([Char | Domain]) ->
[Char+1 | Domain].
+%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Test that the data portion can only be zero bytes for UPDATEs
+%% and that a real DNS server (Knot DNS) accepts our packet
+
+update(Config) when is_list(Config) ->
+ {NSIP,NSPort} = ns(Config),
+ Domain = "otptest",
+
+ % test that empty data for a query fails
+ QueryRec = #dns_rec{
+ header = #dns_header{ opcode = query },
+ anlist = [
+ #dns_rr{ domain = "test-update." ++ Domain, type = a }
+ ]
+ },
+ true = try inet_dns:encode(QueryRec) of
+ _ ->
+ false
+ catch
+ error:{badmatch,[]}:_ ->
+ true
+ end,
+
+ % test that empty data for an update
+ UpdateRec = #dns_rec{
+ header = #dns_header{ opcode = update },
+ % Zone
+ qdlist = [
+ #dns_query{ domain = Domain, class = in, type = soa }
+ ],
+ % Update
+ nslist = [
+ #dns_rr{
+ domain = "update-test." ++ Domain,
+ ttl = 300,
+ class = in,
+ type = a,
+ data = {192,0,2,1}
+ }
+ ]
+ },
+ UpdatePkt = inet_dns:encode(UpdateRec),
+ true = is_binary(UpdatePkt),
+
+ % check if an actual DNS server accepts it
+ SockOpts = [binary,{active,false}],
+ {ok,Sock} = gen_udp:open(0, SockOpts),
+ ok = gen_udp:connect(Sock, NSIP, NSPort),
+ ok = gen_udp:send(Sock, UpdatePkt),
+ {ok,{NSIP,NSPort,ResponsePkt}} = gen_udp:recv(Sock, 0),
+ ok = gen_udp:close(Sock),
+ {ok,ResponseRec} = inet_dns:decode(ResponsePkt),
+ #dns_rec{ header = #dns_header{ rcode = ?NOERROR } } = ResponseRec,
+
+ ok.
+
+
%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Compatibility tests. Call the inet_SUITE tests, but with
%% lookup = [file,dns] instead of [native]
diff --git a/lib/kernel/test/inet_res_SUITE_data/otptest/knot_inc.conf b/lib/kernel/test/inet_res_SUITE_data/otptest/knot_inc.conf
index 6dd8c6dedf..50c9f0d977 100644
--- a/lib/kernel/test/inet_res_SUITE_data/otptest/knot_inc.conf
+++ b/lib/kernel/test/inet_res_SUITE_data/otptest/knot_inc.conf
@@ -1,4 +1,10 @@
+acl:
+ - id: update_rule
+ address: 127.0.0.1
+ action: update
+
zone:
- domain: otptest
+ acl: [ update_rule ]
- domain: 0.0.127.in-addr.arpa.
- domain: 0.0.0.0.f.7.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.
--
2.35.3