Skip to content

Commit d56cb12

Browse files
PR for Duplicated UDP responses from DNS confuse LookupClient MichaCo#140 (MichaCo#142)
Xid mismatch validation added. Co-authored-by: Boris <[email protected]>
1 parent 2b38644 commit d56cb12

File tree

8 files changed

+389
-2
lines changed

8 files changed

+389
-2
lines changed

src/DnsClient/DnsMessageHandler.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ public static bool IsTransientException<T>(T exception) where T : Exception
4848
return false;
4949
}
5050

51+
protected static void ValidateResponse(DnsRequestMessage request, DnsResponseMessage response)
52+
{
53+
if (request != null && response != null && request.Header.Id != response.Header.Id)
54+
{
55+
throw new DnsXidMismatchException(request.Header.Id, response.Header.Id);
56+
}
57+
}
58+
5159
public virtual void GetRequestData(DnsRequestMessage request, DnsDatagramWriter writer)
5260
{
5361
var question = request.Question;

src/DnsClient/DnsTcpMessageHandler.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public override async Task<DnsResponseMessage> QueryAsync(
8080
}
8181
}
8282

83+
ValidateResponse(request, response);
84+
8385
return response;
8486
}
8587

src/DnsClient/DnsUdpMessageHandler.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Concurrent;
3-
using System.Linq;
43
using System.Net;
54
using System.Net.Sockets;
65
using System.Threading;
@@ -52,6 +51,8 @@ public override DnsResponseMessage Query(
5251

5352
var response = GetResponseMessage(new ArraySegment<byte>(memory.Buffer, 0, received));
5453

54+
ValidateResponse(request, response);
55+
5556
Enqueue(server.AddressFamily, udpClient);
5657

5758
return response;
@@ -121,6 +122,8 @@ public override async Task<DnsResponseMessage> QueryAsync(
121122
var response = GetResponseMessage(new ArraySegment<byte>(result.Buffer, 0, result.Buffer.Length));
122123
#endif
123124

125+
ValidateResponse(request, response);
126+
124127
Enqueue(endpoint.AddressFamily, udpClient);
125128

126129
return response;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
3+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
4+
namespace DnsClient
5+
{
6+
#if !NETSTANDARD1_3
7+
[Serializable]
8+
#endif
9+
10+
public class DnsXidMismatchException : Exception
11+
{
12+
public int RequestXid { get; }
13+
public int ResponseXid { get; }
14+
15+
public DnsXidMismatchException(int requestXid, int responseXid)
16+
: base()
17+
{
18+
RequestXid = requestXid;
19+
ResponseXid = responseXid;
20+
}
21+
}
22+
}
23+
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

src/DnsClient/LookupClient.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,21 @@ private IDnsQueryResponse ResolveQuery(
907907

908908
return lastQueryResponse;
909909
}
910+
catch (DnsXidMismatchException ex)
911+
{
912+
var handle = HandleDnsXidMismatchException(ex, request, settings, handler.Type, isLastServer, isLastTry, tries);
913+
914+
if (handle == HandleError.RetryCurrentServer)
915+
{
916+
continue;
917+
}
918+
else if (handle == HandleError.RetryNextServer)
919+
{
920+
break;
921+
}
922+
923+
throw;
924+
}
910925
catch (DnsResponseParseException ex)
911926
{
912927
var handle = HandleDnsResponeParseException(ex, request, handler.Type, isLastServer: isLastServer);
@@ -1167,6 +1182,21 @@ private async Task<IDnsQueryResponse> ResolveQueryAsync(
11671182

11681183
return lastQueryResponse;
11691184
}
1185+
catch (DnsXidMismatchException ex)
1186+
{
1187+
var handle = HandleDnsXidMismatchException(ex, request, settings, handler.Type, isLastServer: isLastServer, isLastTry: isLastTry, currentTry: tries);
1188+
1189+
if (handle == HandleError.RetryCurrentServer)
1190+
{
1191+
continue;
1192+
}
1193+
else if (handle == HandleError.RetryNextServer)
1194+
{
1195+
break;
1196+
}
1197+
1198+
throw;
1199+
}
11701200
catch (DnsResponseParseException ex)
11711201
{
11721202
var handle = HandleDnsResponeParseException(ex, request, handler.Type, isLastServer: isLastServer);
@@ -1373,6 +1403,53 @@ private HandleError HandleDnsResponseException(DnsResponseException ex, DnsReque
13731403
return handle;
13741404
}
13751405

1406+
private HandleError HandleDnsXidMismatchException(DnsXidMismatchException ex, DnsRequestMessage request, DnsQuerySettings settings, DnsMessageHandleType handleType, bool isLastServer, bool isLastTry, int currentTry)
1407+
{
1408+
// No more retries
1409+
if (isLastServer && isLastTry)
1410+
{
1411+
_logger.LogError(
1412+
LogEventQueryFail,
1413+
ex,
1414+
"Query {0} via {1} => {2} xid mismatch {3}. Throwing the error.",
1415+
ex.RequestXid,
1416+
handleType,
1417+
request.Question,
1418+
ex.ResponseXid);
1419+
1420+
return HandleError.Throw;
1421+
}
1422+
1423+
// Last try on the current server, try the nextServer
1424+
if (isLastTry)
1425+
{
1426+
_logger.LogError(
1427+
LogEventQueryRetryErrorNextServer,
1428+
ex,
1429+
"Query {0} via {1} => {2} xid mismatch {3}. Trying next server.",
1430+
ex.RequestXid,
1431+
handleType,
1432+
request.Question,
1433+
ex.ResponseXid);
1434+
1435+
return HandleError.RetryNextServer;
1436+
}
1437+
1438+
// Next try
1439+
_logger.LogWarning(
1440+
LogEventQueryRetryErrorNextServer,
1441+
ex,
1442+
"Query {0} via {1} => {2} xid mismatch {3}. Re-trying {4}/{5}...",
1443+
ex.RequestXid,
1444+
handleType,
1445+
request.Question,
1446+
ex.ResponseXid,
1447+
currentTry,
1448+
settings.Retries + 1);
1449+
1450+
return HandleError.RetryCurrentServer;
1451+
}
1452+
13761453
private HandleError HandleDnsResponeParseException(DnsResponseParseException ex, DnsRequestMessage request, DnsMessageHandleType handleType, bool isLastServer)
13771454
{
13781455
// Don't try to fallback to TCP if we already are on TCP

test/DnsClient.Tests/LookupClientRetryTest.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,88 @@ public async Task DnsResponseParseException_ShouldTryNextServer_ThenThrow_Async(
10831083
Assert.Equal(3, calledIps.Count);
10841084
}
10851085

1086+
[Theory]
1087+
[InlineData(1, 1)]
1088+
[InlineData(1, 3)]
1089+
[InlineData(2, 1)]
1090+
[InlineData(3, 1)]
1091+
[InlineData(4, 4)]
1092+
public void DnsDnsXidMismatchException_ShouldRetry_ThenThrow(int serversCount, int retriesCount)
1093+
{
1094+
var nameServers = Enumerable.Range(1, serversCount)
1095+
.Select(i => new NameServer(IPAddress.Parse($"127.0.10.{i}")))
1096+
.ToArray();
1097+
1098+
var options = new LookupClientOptions(nameServers)
1099+
{
1100+
EnableAuditTrail = true,
1101+
ContinueOnDnsError = false,
1102+
ThrowDnsErrors = false,
1103+
UseCache = true,
1104+
Retries = retriesCount,
1105+
UseRandomNameServer = false,
1106+
UseTcpFallback = false
1107+
};
1108+
1109+
var calledIps = new List<IPAddress>();
1110+
var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) =>
1111+
{
1112+
calledIps.Add(ip.Address);
1113+
1114+
throw new DnsXidMismatchException(req.Header.Id, req.Header.Id + 1);
1115+
});
1116+
1117+
var lookup = new LookupClient(options, udpHandler: udpMessageHandler);
1118+
var result = Assert.ThrowsAny<DnsXidMismatchException>(() => lookup.Query(new DnsQuestion("test.com", QueryType.SRV, QueryClass.IN)));
1119+
1120+
var expectedIps = nameServers
1121+
.SelectMany(ns => Enumerable.Repeat(ns.IPEndPoint.Address, retriesCount + 1))
1122+
.ToArray();
1123+
1124+
Assert.Equal(expectedIps, calledIps);
1125+
}
1126+
1127+
[Theory]
1128+
[InlineData(1, 1)]
1129+
[InlineData(1, 3)]
1130+
[InlineData(2, 1)]
1131+
[InlineData(3, 1)]
1132+
[InlineData(4, 4)]
1133+
public async Task DnsDnsXidMismatchException_ShouldRetry_ThenThrow_Async(int serversCount, int retriesCount)
1134+
{
1135+
var nameServers = Enumerable.Range(1, serversCount)
1136+
.Select(i => new NameServer(IPAddress.Parse($"127.0.10.{i}")))
1137+
.ToArray();
1138+
1139+
var options = new LookupClientOptions(nameServers)
1140+
{
1141+
EnableAuditTrail = true,
1142+
ContinueOnDnsError = false,
1143+
ThrowDnsErrors = false,
1144+
UseCache = true,
1145+
Retries = retriesCount,
1146+
UseRandomNameServer = false,
1147+
UseTcpFallback = false
1148+
};
1149+
1150+
var calledIps = new List<IPAddress>();
1151+
var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) =>
1152+
{
1153+
calledIps.Add(ip.Address);
1154+
1155+
throw new DnsXidMismatchException(req.Header.Id, req.Header.Id + 1);
1156+
});
1157+
1158+
var lookup = new LookupClient(options, udpHandler: udpMessageHandler);
1159+
var result = await Assert.ThrowsAnyAsync<DnsXidMismatchException>(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.SRV, QueryClass.IN)));
1160+
1161+
var expectedIps = nameServers
1162+
.SelectMany(ns => Enumerable.Repeat(ns.IPEndPoint.Address, retriesCount + 1))
1163+
.ToArray();
1164+
1165+
Assert.Equal(expectedIps, calledIps);
1166+
}
1167+
10861168
/* Normal truncated response (TC flag) */
10871169

10881170
[Fact]

test/DnsClient.Tests/LookupTest.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Linq;
33
using System.Net;
4-
using System.Runtime.InteropServices;
54
using System.Threading;
65
using System.Threading.Tasks;
76
using DnsClient.Protocol;
@@ -1113,5 +1112,71 @@ public void Lookup_SettingsFallback_KeepProvidedServers3()
11131112
Assert.NotEqual(client.Settings, settings);
11141113
Assert.NotEqual(client.NameServers, settings.NameServers);
11151114
}
1115+
1116+
[Theory]
1117+
[InlineData(0, true)]
1118+
[InlineData(1, true)]
1119+
[InlineData(3, true)]
1120+
[InlineData(0, false)]
1121+
[InlineData(1, false)]
1122+
[InlineData(3, false)]
1123+
public async Task Lookup_XidMismatch(int mismatchResponses, bool sync)
1124+
{
1125+
var serverEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 54321);
1126+
var options = new LookupClientOptions(new NameServer(serverEndpoint))
1127+
{
1128+
Retries = 20,
1129+
UseCache = false
1130+
};
1131+
1132+
using var server = new UdpServerMistmatchXid(serverEndpoint, mismatchResponses);
1133+
var client = new LookupClient(options);
1134+
1135+
var dnsQuestion = new DnsQuestion("someservice", QueryType.TXT, QueryClass.IN);
1136+
var response = sync ? client.Query(dnsQuestion) : await client.QueryAsync(dnsQuestion);
1137+
1138+
Assert.Equal(2, response.Answers.TxtRecords().Count());
1139+
Assert.Equal("example.com.", response.Answers.TxtRecords().First().DomainName.Value);
1140+
Assert.Equal(mismatchResponses, server.MistmatchedResponsesCount);
1141+
Assert.Equal(mismatchResponses + 1, server.RequestsCount);
1142+
}
1143+
1144+
[Theory]
1145+
[InlineData(0, true)]
1146+
[InlineData(1, true)]
1147+
[InlineData(3, true)]
1148+
[InlineData(5, true)]
1149+
[InlineData(0, false)]
1150+
[InlineData(1, false)]
1151+
[InlineData(3, false)]
1152+
[InlineData(5, false)]
1153+
public async Task Lookup_DuplicateUDPResponses(int duplicatesCount, bool sync)
1154+
{
1155+
var serverEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 54321);
1156+
var options = new LookupClientOptions(new NameServer(serverEndpoint))
1157+
{
1158+
Retries = 20,
1159+
UseCache = false,
1160+
Timeout = TimeSpan.FromSeconds(5)
1161+
};
1162+
1163+
using var server = new UdpServerDuplicateResponses(serverEndpoint, duplicatesCount);
1164+
var client = new LookupClient(options);
1165+
1166+
var dnsQuestion = new DnsQuestion("someservice", QueryType.TXT, QueryClass.IN);
1167+
var response1 = sync ? client.Query(dnsQuestion) : await client.QueryAsync(dnsQuestion);
1168+
var response2 = sync ? client.Query(dnsQuestion) : await client.QueryAsync(dnsQuestion);
1169+
1170+
Assert.Equal(2, response1.Answers.TxtRecords().Count());
1171+
Assert.Equal("example.com.", response1.Answers.TxtRecords().First().DomainName.Value);
1172+
1173+
Assert.Equal(2, response2.Answers.TxtRecords().Count());
1174+
Assert.Equal("example.com.", response2.Answers.TxtRecords().First().DomainName.Value);
1175+
1176+
Assert.True(server.RequestsCount >= 2, "At least 2 requests are expected");
1177+
1178+
// Validate that duplicate response was not picked up
1179+
Assert.NotEqual(response1.Header.Id, response2.Header.Id);
1180+
}
11161181
}
11171182
}

0 commit comments

Comments
 (0)