Recent

Author Topic: Anyone into mDNS?  (Read 3021 times)

bobby100

  • Sr. Member
  • ****
  • Posts: 274
    • Malzilla
Anyone into mDNS?
« on: January 09, 2022, 09:09:15 pm »
Hi @all,

last month, I've completed a task to implement mDNS/Zeroconfig search into an app.
My target was to get the IP addresses of the PLCs in the network (I am working as an instrumentation and controlling engineer). The PLCs we're using, are responding to mDNS queries from the engineering software and I wanted to have this same functionality in my app.
The app is not open source, but I would like to share all my experience worth a month of internet search (not much info found in internet anyway) for someone like me, who didn't have any idea about how is mDNS working (and still has not the whole picture, just the part he needs). The app I wrote can also get and set the parameters of the network adapters (IP, Gateway etc.) and I would also like to share this code in another thread.

I have used two components from Indy 10 for mDNS/zeroconf/Avahi/Bonjour communication:
- IdIPMCastServer to send the mDNS query
- IdUDPServer to catch the answers

The basics of the multicast is that the IdIPMCastServer sends a query to the group with specifying the UDP port (Bound port in Indy terminology) where your app is waiting for the answers. After sending the query, our IdIPMCastServer needs to be deactivated.
Waiting for answers belongs to IdUDPServer, which should now be activated. Because of using the same port, you can't have them both active at the same time.
More instructions are in the code comments.

Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.mnuMulticastClick(Sender: TObject);
  2. var
  3.   //basic communication as captured by Wireshark
  4.   {00 00 - Transaction ID
  5.    00 00 - Standard query
  6.    00 01 - Question count
  7.    00 00 - Answer RRs count
  8.    00 00 - Authority RRs count
  9.    00 00 - Additional RRs count
  10.    xx xx - query string/question string
  11.    00 0c - Type PTR
  12.    00 01 - Class IN}
  13.   query: TIdBytes = ($00, $00, $00, $00, $00, $01, $00, $00, $00, $00,
  14.     $00, $00, $09, $5f, $73, $65, $72, $76, $69, $63, $65, $73, $07,
  15.     $5f, $64, $6e, $73, $2d, $73, $64, $04, $5f, $75, $64, $70, $05,
  16.     $6c, $6f, $63, $61, $6c, $00, $00, $0c, $00, $01);
  17. begin
  18.   //set CastServer
  19.   IdIPMCastServer.MulticastGroup := Id_IPMC_mDNS;
  20.   IdIPMCastServer.BoundIP := String_IP_Address; //Here goes the IP address of your network adapter
  21.   IdIPMCastServer.BoundPort := 49152; //my favorite port. Pick one that is not used by other apps in your network. Other devices will use this port to answer your query
  22.   IdIPMCastServer.Port := 5353; //this is standard
  23.  
  24.   //set UDPServer
  25.   IdUDPServer.Bindings.Add;
  26.   IdUDPServer.Bindings[0].IP := String_IP_Address; //Here goes the IP address of your network adapter
  27.   IdUDPServer.Bindings[0].Port := 49152;
  28.  
  29.   //send Query
  30.   IdIPMCastServer.Active := True;
  31.   IdIPMCastServer.Send(query);
  32.  
  33.   //deactivate CastServer, activate UDPServer and deactivation timer for UDPServer
  34.   IdIPMCastServer.Active := False;
  35.   tmUDPdeactivate.Interval := IdIPMCastServer.TimeToLive * 1000;
  36.   tmUDPdeactivate.Enabled := True; //I use a timer to deactivate the IdUDPServer after 5 seconds of waiting for messages. You need this if you want to send another query with IdIPMCastServer because of shared port
  37.   IdUDPServer.Active := True;
  38. end;    

My IdUDPServer.OnUDPRead looks like this:
Code: Pascal  [Select][+][-]
  1. procedure TfrmMain.IdUDPServerUDPRead(AThread: TIdUDPListenerThread;
  2.   const AData: TIdBytes; ABinding: TIdSocketHandle);
  3. begin
  4.   sgMachines.InsertRowWithValues(sgMachines.RowCount, [IPAddrToName(ABinding.PeerIP), ABinding.PeerIP]);
  5.  ...
  6. end;
I am just adding to a StringGrid the IP address of devices that responded to my query.
The responses from the devices are pretty much alike to the query-structure that I've sent to multicast.
Except for the PLCs that I wanted to search for, my code also finds my printers and NAS in my home network. It means - the query string is not specific for the PLCs, but it seems to be pretty much a universal query (anyone here with some more info about the query strings?).
This is the point where I am asking for help from more experienced programmers - let's make together a unit for this mDNS queries!

For the beginning, I have prepared the basic types:
Code: Pascal  [Select][+][-]
  1. type
  2.  
  3.   TmDNSQuestion = record
  4.     Query: string; //null terminated
  5.     QueryType: word;
  6.     QueryClass: word;
  7.   end;
  8.  
  9.   TmDNSQuery = record
  10.     TransactionID: word;
  11.     Flags: word;
  12.     Questions: word;
  13.     AnswerRRs: word;
  14.     AuthorityRRs: word;
  15.     AdditionslRRs: word;
  16.     Q: array of TmDNSQuestion
  17.   end;
  18.  
  19.   TmDNSAnswer = record
  20.     AnswerName: word;
  21.     AnswerType: word;
  22.     AnswerClass: word;
  23.     CacheFlush: word;
  24.     TimeToLive: word; //in seconds
  25.     DataLength: word; //for DomainName
  26.     DomainName: array of char; //Not null-terminated
  27.   end;
  28.  
  29.   TmDNSResponse = record
  30.     TransactionID: word;
  31.     Flags: word;
  32.     Questions: word;
  33.     AnswerRRs: word;
  34.     AuthorityRRs: word;
  35.     AdditionslRRs: word;
  36.     Q: array of TmDNSQuestion;
  37.     A: array of TmDNSAnswer;
  38.   end;
This is what I've got till now from reverse-engineering the Wireshark captures.
TmDNSAuthority and TmDNSAdditional are of unknown structure for me because I don't have any capture containing these.

Anyone interested?
Or anyone with some more knowledge, who may eventually find flaws or mistakes here?

I have my app done. I would like to gather here some info for the benefit of wider public.
« Last Edit: January 09, 2022, 09:12:51 pm by bobby100 »

MarkMLl

  • Hero Member
  • *****
  • Posts: 8393
Re: Anyone into mDNS?
« Reply #1 on: January 09, 2022, 09:49:21 pm »
Thanks for updating us. I don't think it's immediately useful to the sort of thing I do but I think everybody here appreciates the feedback.

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Logitech, TopSpeed & FTL Modula-2 on bare metal (Z80, '286 protected mode).
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

engkin

  • Hero Member
  • *****
  • Posts: 3112
Re: Anyone into mDNS?
« Reply #2 on: January 09, 2022, 11:07:41 pm »
In your records you cannot use dynamic types like String and array of byte. Typically you reserve enough memory for, let's say, your query. Then you use pointers and record pointer types to fill the memory with intended values.

Also, you need to take care of endianness. IIRC, network records are big endian. That's why, for instance, your third field in your query, Question count, is $00 $01, while 1 on Intel CPUs is $01 $00

For details, you can rely on DNS documentation/implementation as mDNS is based on DNS.

MaxM74

  • New Member
  • *
  • Posts: 30
Re: Anyone into mDNS?
« Reply #3 on: May 09, 2025, 10:08:01 am »
Can you post the full demo?
Thanks

Thaddy

  • Hero Member
  • *****
  • Posts: 16959
  • Ceterum censeo Trump esse delendam
Re: Anyone into mDNS?
« Reply #4 on: May 09, 2025, 10:53:00 am »
@enkin
In system we have BtoN, NtoLE and NtoBE to maniplulate byte order/network byte order (BE), of course.
Due to censorship, I changed this to "Nelly the Elephant". Keeps the message clear.

Remy Lebeau

  • Hero Member
  • *****
  • Posts: 1506
    • Lebeau Software
Re: Anyone into mDNS?
« Reply #5 on: May 09, 2025, 07:53:52 pm »
I have used two components from Indy 10 for mDNS/zeroconf/Avahi/Bonjour communication:
- IdIPMCastServer to send the mDNS query
- IdUDPServer to catch the answers

The basics of the multicast is that the IdIPMCastServer sends a query to the group with specifying the UDP port (Bound port in Indy terminology) where your app is waiting for the answers. After sending the query, our IdIPMCastServer needs to be deactivated.
Waiting for answers belongs to IdUDPServer, which should now be activated.

Your approach has a race condition, where the response may arrive before the TIdUDPServer is ready to receive it.

You SHOULD use the same socket for both sending the query AND receiving the response, then there is no race.  Although TIdIPMCastServer is intended for sending only, you CAN read incoming data with it too. You just have to read it manually using the Binding property directly, as TIdIPMCastServer has no reading logic of its own. TIdIPMCastServer.Send() simply calls TIdIPMCastServer.Binding.SendTo(), you can call TIdIPMCastServer.Binding.RecvFrom() afterwards.

Alternatively you could just use TIdUDPServer by itself.  TIdIPMCastServer isn't really doing anything that can't also be done with TIdUDPServerTIdIPMCastServer just sends packets to the multicast group IP/port. You can do that with TIdUDPServer too, by calling SendTo() on the desired Binding.

Because of using the same port, you can't have them both active at the same time.

Actually, you can.  Simply set TIdIPMCastServer.ReuseSocket and TIdUDPServer.ReuseSocket both to rsTrue, and then activate them both.  TIdIPMCastServer does not read from its socket by default, so the TIdUDPServer should be able to receive the response to the query that TIdIPMCastServer sends.
« Last Edit: May 14, 2025, 05:31:02 pm by Remy Lebeau »
Remy Lebeau
Lebeau Software - Owner, Developer
Internet Direct (Indy) - Admin, Developer (Support forum)

MaxM74

  • New Member
  • *
  • Posts: 30
Re: Anyone into mDNS?
« Reply #6 on: May 14, 2025, 04:02:53 pm »
in TmDNSQuestion Query is NOT a null terminated string (the same for answers) is an Array in the form:
Code: Pascal  [Select][+][-]
  1. Query:
  2. //Start of Array
  3.   StringLength: Byte;
  4.   StringData: array of char;
  5.   ...
  6.   StringLength: Byte;
  7.   StringData: array of char;  
  8. //$00 to terminate the Array.
  9.  
  10.   QueryType: word;
  11.   QueryClass: word;
  12.  

The Response is:
Code: Pascal  [Select][+][-]
  1.    TransactionID: word;
  2.     Flags: word;
  3.     Questions: word;
  4.     AnswerRRs: word;
  5.     AuthorityRRs: word;
  6.     AdditionslRRs: word;
  7.     Q: array; //Your Query
  8.     A: Answer;
  9.  

Code: Pascal  [Select][+][-]
  1. //Answer
  2. AnswerName: word;
  3.     AnswerType: word;
  4.     AnswerClass: word;
  5.     CacheFlush: word;
  6.     TimeToLive: word; //in seconds
  7.     DataLength: word;
  8.     DomainName:
  9. //Start of Array
  10.   StringLength: Byte;
  11.   StringData: array of char;
  12.   ...
  13.   StringLength: Byte;
  14.   StringData: array of char;  
  15. //End of Array.
  16.   Data: array[DataLength-2] of Byte; //? or always 14 bytes
  17.  
  18. //Start of Array
  19.   StringLength: Byte;
  20.   StringData: array of char;
  21.   ...
  22.   StringLength: Byte;
  23.   StringData: array of char;  
  24. //End of Array.
  25.   Data: array[DataLength-2] of Byte; //? or always 14 bytes
  26.  
  27.  
  28.  

this is a Response from a Kyocera Printer/Scanner (is an Hex File)


MaxM74

  • New Member
  • *
  • Posts: 30
Re: Anyone into mDNS?
« Reply #7 on: May 14, 2025, 04:51:42 pm »
this test search for scanners

bobby100

  • Sr. Member
  • ****
  • Posts: 274
    • Malzilla
Re: Anyone into mDNS?
« Reply #8 on: May 14, 2025, 08:31:52 pm »
in TmDNSQuestion Query is NOT a null terminated string (the same for answers) is an Array in the form:
Code: Pascal  [Select][+][-]
  1. Query:
  2. //Start of Array
  3.   StringLength: Byte;
  4.   StringData: array of char;
  5.   ...
  6.   StringLength: Byte;
  7.   StringData: array of char;  
  8. //$00 to terminate the Array.
  9.  
  10.   QueryType: word;
  11.   QueryClass: word;
  12.  
I didn't see any byte that would be the string length. How did you get to this conclusion?

MaxM74

  • New Member
  • *
  • Posts: 30
Re: Anyone into mDNS?
« Reply #9 on: May 15, 2025, 08:29:16 am »
Look the Data

Code: Pascal  [Select][+][-]
  1.   query: TIdBytes = (
  2.     $00, $00,
  3.     $00, $00,
  4.     $00, $01,
  5.     $00, $00,
  6.     $00, $00,
  7.     $00, $00,
  8.       $09, $5F, $73, $65, $72, $76, $69, $63, $65, $73,  //_services
  9.       $07, $5f, $64, $6e, $73, $2d, $73, $64,                 //_dns-sd
  10.       $04, $5f, $75, $64, $70,                                       //_udp
  11.       $05, $6c, $6f, $63, $61, $6c,                                //local
  12.       $00,
  13.     $00, $0c,
  14.     $00, $01);
  15.  

anyway i found this on github, when i have time i'll try it

https://github.com/marsupilami79/mdns4Delphi

MaxM74

  • New Member
  • *
  • Posts: 30
Re: Anyone into mDNS?
« Reply #10 on: May 16, 2025, 08:27:51 am »
Updated Version to work with new FPC and Delphi

https://github.com/maxm74/mdns4Delphi

 

TinyPortal © 2005-2018