diff --git a/README.md b/README.md index 7c46fd2..0ed1a18 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ service or service instance. - CI on Circle (Debian GNU/Linux), Travis (Ubuntu Xenial and OSX) and AppVeyor (Windows Server 2016) - Detects new and/or removed network interfaces - Supports multicasting on multiple network interfaces +- Handles legacy unicast queries, see #61 ## Getting started diff --git a/Spike/Program.cs b/Spike/Program.cs index 219c1d4..a500ed1 100644 --- a/Spike/Program.cs +++ b/Spike/Program.cs @@ -52,6 +52,7 @@ static void Main(string[] args) }; var sd = new ServiceDiscovery(mdns); + sd.Advertise(new ServiceProfile("ipfs1", "_ipfs-discovery._udp", 5010)); sd.Advertise(new ServiceProfile("x1", "_xservice._tcp", 5011)); sd.Advertise(new ServiceProfile("x2", "_xservice._tcp", 666)); var z1 = new ServiceProfile("z1", "_zservice._udp", 5012); diff --git a/src/MessageEventArgs.cs b/src/MessageEventArgs.cs index 9aaba5c..c9e5c54 100644 --- a/src/MessageEventArgs.cs +++ b/src/MessageEventArgs.cs @@ -24,6 +24,14 @@ public class MessageEventArgs : EventArgs /// The endpoint from the message was received. /// public IPEndPoint RemoteEndPoint { get; set; } + + /// + /// Determines if the sender is using legacy unicast DNS. + /// + /// + /// false if the sender is using port 5353. + /// + public bool IsLegacyUnicast => RemoteEndPoint.Port != MulticastClient.MulticastPort; } } diff --git a/src/MulticastClient.cs b/src/MulticastClient.cs index 5856232..4af58ac 100644 --- a/src/MulticastClient.cs +++ b/src/MulticastClient.cs @@ -19,7 +19,14 @@ class MulticastClient : IDisposable { static readonly ILog log = LogManager.GetLogger(typeof(MulticastClient)); - const int MulticastPort = 5353; + /// + /// The port number assigned to Multicast DNS. + /// + /// + /// Port number 5353. + /// + public static readonly int MulticastPort = 5353; + static readonly IPAddress MulticastAddressIp4 = IPAddress.Parse("224.0.0.251"); static readonly IPAddress MulticastAddressIp6 = IPAddress.Parse("FF02::FB"); static readonly IPEndPoint MdnsEndpointIp6 = new IPEndPoint(MulticastAddressIp6, MulticastPort); diff --git a/src/MulticastService.cs b/src/MulticastService.cs index 5d48fc3..2a9a348 100644 --- a/src/MulticastService.cs +++ b/src/MulticastService.cs @@ -30,6 +30,7 @@ public class MulticastService : IResolver, IDisposable const int packetOverhead = 48; const int maxDatagramSize = Message.MaxLength; + static readonly TimeSpan maxLegacyUnicastTTL = TimeSpan.FromSeconds(10); static readonly ILog log = LogManager.GetLogger(typeof(MulticastService)); static readonly IPNetwork[] LinkLocalNetworks = new[] { IPNetwork.Parse("169.254.0.0/16"), IPNetwork.Parse("fe80::/10") }; @@ -51,6 +52,16 @@ public class MulticastService : IResolver, IDisposable /// MulticastClient client; + /// + /// Use to send unicast IPv4 answers. + /// + UdpClient unicastClientIp4 = new UdpClient(AddressFamily.InterNetwork); + + /// + /// Use to send unicast IPv6 answers. + /// + UdpClient unicastClientIp6 = new UdpClient(AddressFamily.InterNetworkV6); + /// /// Function used for listening filtered network interfaces. /// @@ -79,7 +90,6 @@ static MulticastService() /// then forgotten. /// /// - /// public event EventHandler QueryReceived; /// @@ -446,8 +456,10 @@ public void SendQuery(Message msg) /// When the serialised is too large. /// /// + /// /// The flag is set to true, /// the set to zero and any questions are removed. + /// /// /// The is truncated /// if exceeds the maximum packet length. @@ -456,6 +468,10 @@ public void SendQuery(Message msg) /// should always be true except /// when answering a probe. /// + /// + /// If possible the + /// method should be used, so that legacy unicast queries are supported. + /// /// /// /// @@ -474,7 +490,77 @@ public void SendAnswer(Message answer, bool checkDuplicate = true) Send(answer, checkDuplicate); } - void Send(Message msg, bool checkDuplicate) + /// + /// Send an answer to a query. + /// + /// + /// The answer message. + /// + /// + /// The query that is being answered. + /// + /// + /// If true, then if the same was + /// recently sent it will not be sent again. + /// + /// + /// When the service has not started. + /// + /// + /// When the serialised is too large. + /// + /// + /// + /// If the is a standard multicast query (sent to port 5353), then + /// is called. + /// + /// + /// Otherwise a legacy unicast reponse is sent to sender's end point. + /// The flag is set to true, + /// the is set to query's ID, + /// the is set to the query's questions, + /// and all resource record TTLs have a max value of 10 seconds. + /// + /// + /// The is truncated + /// if exceeds the maximum packet length. + /// + /// + /// should always be true except + /// when answering a probe. + /// + /// + public void SendAnswer(Message answer, MessageEventArgs query, bool checkDuplicate = true) + { + if (!query.IsLegacyUnicast) + { + SendAnswer(answer, checkDuplicate); + return; + } + + answer.AA = true; + answer.Id = query.Message.Id; + answer.Questions.Clear(); + answer.Questions.AddRange(query.Message.Questions); + answer.Truncate(maxPacketSize); + + foreach (var r in answer.Answers) + { + r.TTL = (r.TTL > maxLegacyUnicastTTL) ? maxLegacyUnicastTTL : r.TTL; + } + foreach (var r in answer.AdditionalRecords) + { + r.TTL = (r.TTL > maxLegacyUnicastTTL) ? maxLegacyUnicastTTL : r.TTL; + } + foreach (var r in answer.AdditionalRecords) + { + r.TTL = (r.TTL > maxLegacyUnicastTTL) ? maxLegacyUnicastTTL : r.TTL; + } + + Send(answer, checkDuplicate, query.RemoteEndPoint); + } + + void Send(Message msg, bool checkDuplicate, IPEndPoint remoteEndPoint = null) { var packet = msg.ToByteArray(); if (packet.Length > maxPacketSize) @@ -487,7 +573,18 @@ void Send(Message msg, bool checkDuplicate) return; } - client?.SendAsync(packet).GetAwaiter().GetResult(); + // Standard multicast reponse? + if (remoteEndPoint == null) + { + client?.SendAsync(packet).GetAwaiter().GetResult(); + } + // Unicast response + else + { + var unicastClient = (remoteEndPoint.Address.AddressFamily == AddressFamily.InterNetwork) + ? unicastClientIp4 : unicastClientIp6; + unicastClient.SendAsync(packet, packet.Length, remoteEndPoint).GetAwaiter().GetResult(); + } } /// diff --git a/src/ServiceDiscovery.cs b/src/ServiceDiscovery.cs index 69bbaf8..4343a0e 100644 --- a/src/ServiceDiscovery.cs +++ b/src/ServiceDiscovery.cs @@ -330,11 +330,11 @@ void OnQuery(object sender, MessageEventArgs e) if (QU) { // TODO: Send a Unicast response if required. - Mdns.SendAnswer(response); + Mdns.SendAnswer(response, e); } else { - Mdns.SendAnswer(response); + Mdns.SendAnswer(response, e); } if (log.IsDebugEnabled) diff --git a/test/MulticastServiceTest.cs b/test/MulticastServiceTest.cs index 254c77e..d136cfa 100644 --- a/test/MulticastServiceTest.cs +++ b/test/MulticastServiceTest.cs @@ -40,8 +40,12 @@ public void SendQuery() mdns.NetworkInterfaceDiscovered += (s, e) => ready.Set(); mdns.QueryReceived += (s, e) => { - msg = e.Message; - done.Set(); + if ("some-service.local" == e.Message.Questions.First().Name) + { + msg = e.Message; + Assert.IsFalse(e.IsLegacyUnicast); + done.Set(); + } }; try { @@ -131,6 +135,54 @@ public void ReceiveAnswer() } } + [TestMethod] + public async Task ReceiveLegacyUnicastAnswer() + { + var service = Guid.NewGuid().ToString() + ".local"; + var ready = new ManualResetEvent(false); + + var query = new Message(); + query.Questions.Add(new Question + { + Name = service, + Type = DnsType.A + }); + var packet = query.ToByteArray(); + var client = new UdpClient(); + using (var mdns = new MulticastService()) + { + mdns.NetworkInterfaceDiscovered += (s, e) => ready.Set(); + mdns.QueryReceived += (s, e) => + { + var msg = e.Message; + if (msg.Questions.Any(q => q.Name == service)) + { + var res = msg.CreateResponse(); + res.Answers.Add(new ARecord + { + Name = service, + Address = IPAddress.Parse("127.1.1.1") + }); + mdns.SendAnswer(res, e); + } + }; + mdns.Start(); + Assert.IsTrue(ready.WaitOne(TimeSpan.FromSeconds(1)), "ready timeout"); + await client.SendAsync(packet, packet.Length, "224.0.0.251", 5353); + var r = await client.ReceiveAsync(); + var response = new Message(); + response.Read(r.Buffer, 0, r.Buffer.Length); + Assert.IsTrue(response.IsResponse); + Assert.AreEqual(MessageStatus.NoError, response.Status); + Assert.IsTrue(response.AA); + Assert.AreEqual(1, response.Questions.Count); + var a = (ARecord)response.Answers[0]; + Assert.AreEqual(IPAddress.Parse("127.1.1.1"), a.Address); + Assert.AreEqual(service, a.Name); + Assert.AreEqual(TimeSpan.FromSeconds(10), a.TTL); + } + } + [TestMethod] public void ReceiveAnswer_IPv4() {