Manuel Kukla's Blog

IT, Finanzen, Aktien, Kuriositäten und alltägliches

Einige PInvoke-Signaturen für dnsapi.dll

Kürzlich hatte ich das Bedürfnis meinem VPN-Client die Registrierung im DNS-Server beizubringen. Nachdem ein ipconfig /registerdns nicht zum Ziel führte (funktioniert anscheinend nicht für VPN-Adapter, oder nicht übers VPN, oder nicht mit den DHCP-Addressen vom VPN-Server, oder nur für den ersten DNS-Server, oder wie auch immer...) musste eine andere Lösung her.

Die Aufgabenstellung lautet: Einen DNS-Eintrag am internen DNS-Server im Remote-Netzwerk anlegen bzw. aktualisieren.

Deshalb kam ich in Verlegenheit dies selbst zu implementieren. Bald bin ich über die DnsQuery-Funktion gestolpert, welche jedoch ziemlich bescheiden dokumentiert ist. Wie ich dazu kam? Auf Codeproject gab es ein Projekt, welches das ganze via Extended T-SQL-Procedure macht. (http://www.codeproject.com/Articles/5535/Dynamic-DNS-Web-Service) Tja, "... It's not rocket science ..." dachte ich mir, wenn das mit einer extended-SP hinhaut. Leider war die halt nativ und benötigt keine PInvokes...

Ich war also auf der Suche nach den korrekten Deklarationen für C# für folgende Funktionen:

DnsQuery (bzw. DnsQuery_W)
DnsModifyRecordsInSet (bzw. DnsModifyRecordsInSet_W) DnsAcquireContextHandle (bzw. DnsAcquireContextHandle_W) DnsReleaseContextHandle

Die Basis dafür war die pinvoke.net-Seite, welche jedoch wie schon des öfteren bemerkt bestensfalls 80% der Lösung liefert und man die restlichen 20% hart erfrickeln muss. (http://www.pinvoke.net/default.aspx/dnsapi.dnsquery)

Nachdem ich nun die richtige Lösung für (mein) Problem gefunden habe, möchte ich es gerne für andere (und natürlich auch für mich :-)) archivieren.

    [DllImport("dnsapi", EntryPoint = "DnsQuery_W", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
    public static extern int DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)] ref string lpstrName, //_In_        PCTSTR      lpstrName,
                                       DnsRecordTypes wType,                                      //_In_        WORD        wType,      //Enum
                                       DnsQueryOptions Options,                                   //_In_        DWORD       Options,    //[Flags] Enum
                                       IntPtr pExtra,                                             //_Inout_opt_ PVOID       pExtra,
                                       ref IntPtr ppQueryResultsSet,                              //_Out_opt_   PDNS_RECORD *ppQueryResultsSet,
                                       IntPtr pReserved);                                         //_Out_opt_   PVOID       *pReserved

    [DllImport("dnsapi.dll", EntryPoint = "DnsModifyRecordsInSet_W", CharSet = CharSet.Unicode, SetLastError = false, ExactSpelling = true)]     public static extern int DnsModifyRecordsInSet(IntPtr pAddRecords, IntPtr pDeleteRecords, int Options, IntPtr hContext, IntPtr pExtra, IntPtr pReserved);     [DllImport("dnsapi.dll", EntryPoint = "DnsAcquireContextHandle_W", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]     public static extern int DnsAcquireContextHandle(int credentialsFlags, [InMarshalAs(UnmanagedType.LPStruct)] DnsApi.SecWinNtAuthIdentity credentials, out IntPtr handle);     [DllImport("dnsapi.dll", EntryPoint = "DnsReleaseContextHandle", CharSet = CharSet.Unicode, SetLastError = false, ExactSpelling = true)]     public static extern void DnsReleaseContextHandle(IntPtr handle);

    [DllImport("dnsapi.dll", CharSet = CharSet.Auto, SetLastError = true)]     public static extern void DnsRecordListFree(IntPtr pRecordList, DNS_FREE_TYPE FreeType);

Die structs
DnsQueryOptions, DnsUpdateOptions, DnsRecordTypes
können von der pinvoke-Seite übernommen werden.

Wichtig ist hingegen die korrekte Deklaration von DNS_RECORD, welche wie folgt aussieht. Ganz wichtig ist hier Charset.Unicode, da sonst nur das erste Zeichen des Strings vorhanden ist, und ein Update des Eintrags nicht funktioniert.
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct DNS_RECORD
    {
      public IntPtr pNext;
      public string pName;
      public short wType;
      public short wDataLength;
      public int flags;
      public int dwTtl;
      public int dwReserved;
      public DnsData DATA;
      public short wPreference;
      public short Pad;
      private int Pad1;
      private int Pad2;
      private int Pad3;
      private int Pad4;
      private int Pad5;
      private IntPtr Pad6;
      private IntPtr Pad7;
      private IntPtr Pad8;
    }
 Ebenfalls wichtig ist, dass der Record nicht bei Pad aufhört, sondern dahinter auch noch Pad1-Pad8 kommt. (Quelle: http://stackoverflow.com/questions/6662381/how-to-update-some-com-marshalling-code-to-work-on-a-64-bit-system)

Der Aufruf für DnsQuery funktioniert dann wie folgt. Ich verwende als 4. Parameter ein Array von IPs, welche die befragten DNS-Server darstellen. (Dies ist u.a. hier dokumentiert: https://support.microsoft.com/en-us/kb/831226, jedoch nicht im offiziellen Artikel zur Funktion: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682016%28v=vs.85%29.aspx)

        String domain = "www.mku.name";
        
//Anfrage an spezifischen DNS-Server (strDNSIP) richten uint address = BitConverter.ToUInt32(IPAddress.Parse(strDNSIP).GetAddressBytes(), 0);         uint[] ipArray = new uint[1];         ipArray.SetValue(address, 0);         DnsIntf.IP4_ARRAY dnsServerArray = new DnsIntf.IP4_ARRAY();         dnsServerArray.AddrCount = 1;         dnsServerArray.AddrArray = new uint[1];         dnsServerArray.AddrArray[0] = address;         IntPtr ptrDNSServer = Marshal.AllocHGlobal(Marshal.SizeOf(dnsServerArray));         Marshal.StructureToPtr(dnsServerArray, ptrDNSServer, false);         var result = DnsIntf.DnsQuery(ref domain, DnsIntf.DnsRecordTypes.DNS_TYPE_ALL, DnsIntf.DnsQueryOptions.DNS_QUERY_BYPASS_CACHE, ptrDNSServer, ref recordsArray, IntPtr.Zero);
Die Ergebnisse können wie folgt durchlaufen werden:
        DnsIntf.DNS_RECORD record = new DnsIntf.DNS_RECORD();
        var recordList = new List<string>();
        for (var recordPtr = recordsArray; !recordPtr.Equals(IntPtr.Zero); recordPtr = record.pNext)
        {
          record = (DnsIntf.DNS_RECORD)Marshal.PtrToStructure(recordPtr, typeof(DnsIntf.DNS_RECORD));
          if (record.wType == (int)DnsIntf.DnsRecordTypes.DNS_TYPE_A)
          {
            IPAddress addr = DnsIntf.ConvertUintToIpAddress(record.DATA.A.IpAddress);
            recordList.Add(addr.ToString());
          }
        }

Die IP zu uint-Konvertierung erfolgt mit dieser Funktion. Ich habe hier noch ein Array.Reverse eingebaut, weil es in meinem Fall genau verkehrt herum war. Bei Nicht-Funktionieren anpassen, ich hab nichts zum Gegentesten ;-)
 public static IPAddress ConvertUintToIpAddress(uint ipAddress)
    {
      // x86 is in little endian
      // Network byte order (what the IPAddress object requires) is big endian
      // Ex - 0x7F000001 is 127.0.0.1
      var addressBytes = new byte[4];
      addressBytes[0] = (byte)((ipAddress & 0xFF000000u) >> 24);
      addressBytes[1] = (byte)((ipAddress & 0x00FF0000u) >> 16);
      addressBytes[2] = (byte)((ipAddress & 0x0000FF00u) >> 8);
      addressBytes[3] = (byte)(ipAddress & 0x000000FFu);
 
      if (BitConverter.IsLittleEndian) // korrekt?
        Array.Reverse(addressBytes);
 
      return new IPAddress(addressBytes);
    }

Ein neuer A-Record mit TTL 10s kann (etwas russisch^^) wie folgt erzeugt werden:

        DnsIntf.DNS_RECORD recordNew = new DnsIntf.DNS_RECORD();
        recordNew.dwTtl = 10;
        recordNew.pName = domain;
        recordNew.DATA.A.IpAddress = BitConverter.ToUInt32(newIPaddr.GetAddressBytes(), 0);
        recordNew.wDataLength = 4;
        recordNew.wType = 1;
        recordNew.flags = 8201;

Zum Aktualisieren des Eintrags wird ein Domänen-User benötigt -> Es muss der richtige Kontext erzeugt werden:
IntPtr ptrContext = new IntPtr();
var resultContext = DnsIntf.DnsAcquireContextHandle(1 /*TRUE for UNICODE*/new DnsApi.SecWinNtAuthIdentity(strUser, strDomain, strPwd), out ptrContext);
SecWinNtAuthIdentity ist wie folgt definiert. Quelle: https://searchcode.com/codesearch/view/12799292/

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public class SecWinNtAuthIdentity
    {
      public string User;
      public int UserLength;
 
      public string Domain;
      public int DomainLength;
 
      public string Password;
      public int PasswordLength;
 
      public uint Flags;
 
      public SecWinNtAuthIdentity(string user, string domain, string password)
      {
        User = user;
        UserLength = user != null ? user.Length : 0;
 
        Domain = domain;
        DomainLength = domain != null ? domain.Length : 0;
 
        Password = password;
        PasswordLength = password != null ? password.Length : 0;
 
        Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
      }
    }


Und dann noch zu guter Letzt das Aktualisieren des Eintrags. Das funktioniert so, dass eine (via pNext verkettete) Liste von Neuen sowie zu löschenden Einträgen übergeben werden muss. Im konkreten Fall ptrNew (welcher aus recordNew erzeugt wird), sowie recordsArray aus der oben durchgeführten Abfrage.

IntPtr ptrNew = Marshal.AllocHGlobal(Marshal.SizeOf(recordNew));
Marshal.StructureToPtr(recordNew, ptrNew, false);
 
IntPtr ptrOut = new IntPtr();
var result2 = DnsIntf.DnsModifyRecordsInSet(ptrNew, recordsArray, (int)DnsIntf.DnsUpdateOptions.DNS_UPDATE_SECURITY_USE_DEFAULT, ptrContext, ptrDNSServer, ptrOut);

Am Schluss sollte man noch etwas aufräumen:
 if (recordsArray != IntPtr.Zero)
 {
     DnsIntf.DnsRecordListFree(recordsArray, DnsIntf.DNS_FREE_TYPE.DnsFreeFlat);
 }
Vermutlich gibt es auch noch etwas anderes zu bereinigen, aber das hebe ich mir fürs Neue Jahr auf :-) Die Saubermacher unter den Lesern dürfen gerne in den Kommentaren posten.